深入理解Class类文件的结构

逅弈 欢迎转载,注明原创出处即可,谢谢!

java能够实现“Write Once,Run Anywhere”的目标,跟JVM和字节码文件密不可分。而字节码文件是平台无关的,java代码或者其他语言的代码(如Groovy,JRuby,Scala等)由该语言对应的编译器编译成Class文件,再由JVM进行解释执行。

本文将深入理解并分析Class类文件的结构,以期望能揭示出JVM是如何根据Class文件翻译解释出类的实例,属性,方法,访问标识等等信息的。

Class文件是一组以8位的字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑的排列在Class文件中,中间没有添加任何分隔符。当遇到需要占用8位字节以上空间的数据项时,会按照高位在前的方式分割成若干个8位字节进行存储。

Class文件中只有两种数据类型:无符号数和表。无符号数属于基本的数据类型:u1,u2,u4,u8分别代表1个字节,2个字节,4个字节,8个字节,无符号数可以用来描述数字、索引引用、数量值、字符串值(UTF-8编码)。表由多个无符号数或者其他表组成,所有的表都以_info结尾。

整个Class文件就是一张表,包括以下数据项:魔数,次版本号,主版本号,常量池容量计数器,常量池,访问标志,类索引,父类索引,接口计数器,接口索引,字段个数,字段表集合,方法个数,方法表集合,属性个数,属性表集合。


以下是Class文件的表结构:

类型 名称 数量 描述
u4 magic 1 魔数,第1-4字节,值为CAFEBABE,用以确定一个文件是为一个能被虚拟机接受的Class文件
u2 minor_version 1 次版本号,第5-6字节
u2 major_version 1 主版本号,第7-8字节
u2 constant_pool_count 1 常量池容量计数器
cp_info constant_pool constant_pool_count-1 常量池,主要有字面量和符号引用,数量等于constant_pool_count-1,因为常量在常量池中的索引从1开始,0被用来表示该常量无引用
u2 access_flags 1 访问标志
u2 this_class 1 类索引,值为类的全限定名
u2 super_class 1 父类索引,值为父类的全限定名,由于不允许有多重继承,所以父类索引只有一个
u2 interfaces_count 1 接口计数器
u2 interfaces interfaces_count 该类实现的接口的索引,可以有多个接口
u2 fields_count 1 字段个数
field_info fields fields_count 字段表集合
u2 methods_count 1 方法个数
method_info methods methods_count 方法表集合
u2 attributes_count 1 属性个数
attribute_info attributes attributes_count 属性表集合

以下通过一个简单的类来了解java代码编译成Class文件后,class文件中的内容

1.定义一个简单的类

/**
 * Test Class
 * 1. compile the TestClass.java into TestClass.class
 * 2. use javap -verbose TestClass > TestClass.txt
 * 3. analyze the class
 * @author gris.wang
 * @since 2018/1/22
 **/
public class TestClass {

    private static int id;

    private String name;

    public static void showId(){
        System.out.println("id="+id);
    }
}

2.TestClass.class的文件内容如下:

CAFE BABE 0000 0034 0033 0A00 0C00 1B09
001C 001D 0700 1E0A 0003 001B 0800 1F0A
0003 0020 0900 0B00 210A 0003 0022 0A00
0300 230A 0024 0025 0700 2607 0027 0100
0269 6401 0001 4901 0004 6E61 6D65 0100
124C 6A61 7661 2F6C 616E 672F 5374 7269
6E67 3B01 0006 3C69 6E69 743E 0100 0328
2956 0100 0443 6F64 6501 000F 4C69 6E65
4E75 6D62 6572 5461 626C 6501 0012 4C6F
6361 6C56 6172 6961 626C 6554 6162 6C65
0100 0474 6869 7301 001A 4C63 6F6D 2F6C
656D 656D 6F2F 6A76 6D2F 5465 7374 436C
6173 733B 0100 0673 686F 7749 6401 000A
536F 7572 6365 4669 6C65 0100 0E54 6573
7443 6C61 7373 2E6A 6176 610C 0011 0012
0700 280C 0029 002A 0100 176A 6176 612F
6C61 6E67 2F53 7472 696E 6742 7569 6C64
6572 0100 0369 643D 0C00 2B00 2C0C 000D
000E 0C00 2B00 2D0C 002E 002F 0700 300C
0031 0032 0100 1863 6F6D 2F6C 656D 656D
6F2F 6A76 6D2F 5465 7374 436C 6173 7301
0010 6A61 7661 2F6C 616E 672F 4F62 6A65
6374 0100 106A 6176 612F 6C61 6E67 2F53
7973 7465 6D01 0003 6F75 7401 0015 4C6A
6176 612F 696F 2F50 7269 6E74 5374 7265
616D 3B01 0006 6170 7065 6E64 0100 2D28
4C6A 6176 612F 6C61 6E67 2F53 7472 696E
673B 294C 6A61 7661 2F6C 616E 672F 5374
7269 6E67 4275 696C 6465 723B 0100 1C28
4929 4C6A 6176 612F 6C61 6E67 2F53 7472
696E 6742 7569 6C64 6572 3B01 0008 746F
5374 7269 6E67 0100 1428 294C 6A61 7661
2F6C 616E 672F 5374 7269 6E67 3B01 0013
6A61 7661 2F69 6F2F 5072 696E 7453 7472
6561 6D01 0007 7072 696E 746C 6E01 0015
284C 6A61 7661 2F6C 616E 672F 5374 7269
6E67 3B29 5600 2100 0B00 0C00 0000 0200
0A00 0D00 0E00 0000 0200 0F00 1000 0000
0200 0100 1100 1200 0100 1300 0000 2F00
0100 0100 0000 052A B700 01B1 0000 0002
0014 0000 0006 0001 0000 000B 0015 0000
000C 0001 0000 0005 0016 0017 0000 0009
0018 0012 0001 0013 0000 0038 0003 0000
0000 001C B200 02BB 0003 59B7 0004 1205
B600 06B2 0007 B600 08B6 0009 B600 0AB1
0000 0001 0014 0000 000A 0002 0000 0012
001B 0013 0001 0019 0000 0002 001A 

从上可以发现一些简单的信息:

  • 第1-4字节:CAFE BABE,表示这是一个合法的Class文件
  • 第5-6字节:0000,表示次版本号为0
  • 第7-8字节:0034,表示主版本号为52,笔者安装的JDK版本为:
java version "1.8.0_152"
Java(TM) SE Runtime Environment (build 1.8.0_152-b16)
Java HotSpot(TM) 64-Bit Server VM (build 25.152-b16, mixed mode)
  • 第9-10字节:0033,表示该类中共有51-1=50个常量
  • 第11字节:0A,这是第一个常量,常量tag为0A(tag标志值为十进制中的10),查表得知该常量为CONSTANT_Methodref_info类型,而CONSTANT_Methodref_info类型的常量规定,该常量tag后面紧跟着的是两个u2类型的常量:
    • 第一个常量指向声明方法的类描述符CONSTANT_Class_info的索引,该常量为第12-13字节:000C,表示指向第12(0x0C)个常量,通过查找常量池得知第12个常量为:java/lang/Object
    • 第二个常量指向名称及类型描述符CONSTANT_NameAndType的索引,该常量为第14-15字节:001B,表示指向第27(0x1B)个常量,而CONSTANT_NameAndType类型的常量tag后面也紧跟着两个u2类型的常量:
      • 第一个常量指向字段或方法的名称,是一个CONSTANT_Utf8_info类型的常量,通过查找常量池得知该常量指向的是第17个常量,值为:<init>
      • 第二个常量指向字段或方法的描述符,也是一个CONSTANT_Utf8_info类型的常量,通过查找常量池得知该常量指向的是第18个常量,值为:()V
  • 后面的常量也依次根据查表的方式获得具体的值

3.通过javap对编译好的TestClass.class文件进行分析

javap -verbose TestClass > TestClass.txt

4.生成的TestClass.txt的内容如下:

Classfile ~/workspace/lememo/out/production/classes/com/lememo/jvm/TestClass.class
  Last modified 2018-1-22; size 750 bytes
  MD5 checksum b90886860e2bf374d0a26c33f08e41e5
  Compiled from "TestClass.java"
public class com.lememo.jvm.TestClass
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #12.#27        // java/lang/Object."<init>":()V
   #2 = Fieldref           #28.#29        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = Class              #30            // java/lang/StringBuilder
   #4 = Methodref          #3.#27         // java/lang/StringBuilder."<init>":()V
   #5 = String             #31            // id=
   #6 = Methodref          #3.#32         // java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
   #7 = Fieldref           #11.#33        // com/lememo/jvm/TestClass.id:I
   #8 = Methodref          #3.#34         // java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
   #9 = Methodref          #3.#35         // java/lang/StringBuilder.toString:()Ljava/lang/String;
  #10 = Methodref          #36.#37        // java/io/PrintStream.println:(Ljava/lang/String;)V
  #11 = Class              #38            // com/lememo/jvm/TestClass
  #12 = Class              #39            // java/lang/Object
  #13 = Utf8               id
  #14 = Utf8               I
  #15 = Utf8               name
  #16 = Utf8               Ljava/lang/String;
  #17 = Utf8               <init>
  #18 = Utf8               ()V
  #19 = Utf8               Code
  #20 = Utf8               LineNumberTable
  #21 = Utf8               LocalVariableTable
  #22 = Utf8               this
  #23 = Utf8               Lcom/lememo/jvm/TestClass;
  #24 = Utf8               showId
  #25 = Utf8               SourceFile
  #26 = Utf8               TestClass.java
  #27 = NameAndType        #17:#18        // "<init>":()V
  #28 = Class              #40            // java/lang/System
  #29 = NameAndType        #41:#42        // out:Ljava/io/PrintStream;
  #30 = Utf8               java/lang/StringBuilder
  #31 = Utf8               id=
  #32 = NameAndType        #43:#44        // append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
  #33 = NameAndType        #13:#14        // id:I
  #34 = NameAndType        #43:#45        // append:(I)Ljava/lang/StringBuilder;
  #35 = NameAndType        #46:#47        // toString:()Ljava/lang/String;
  #36 = Class              #48            // java/io/PrintStream
  #37 = NameAndType        #49:#50        // println:(Ljava/lang/String;)V
  #38 = Utf8               com/lememo/jvm/TestClass
  #39 = Utf8               java/lang/Object
  #40 = Utf8               java/lang/System
  #41 = Utf8               out
  #42 = Utf8               Ljava/io/PrintStream;
  #43 = Utf8               append
  #44 = Utf8               (Ljava/lang/String;)Ljava/lang/StringBuilder;
  #45 = Utf8               (I)Ljava/lang/StringBuilder;
  #46 = Utf8               toString
  #47 = Utf8               ()Ljava/lang/String;
  #48 = Utf8               java/io/PrintStream
  #49 = Utf8               println
  #50 = Utf8               (Ljava/lang/String;)V
{
  public com.lememo.jvm.TestClass();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 11: 0
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0       5     0  this   Lcom/lememo/jvm/TestClass;

  public static void showId();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=3, locals=0, args_size=0
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: new           #3                  // class java/lang/StringBuilder
         6: dup
         7: invokespecial #4                  // Method java/lang/StringBuilder."<init>":()V
        10: ldc           #5                  // String id=
        12: invokevirtual #6                  // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
        15: getstatic     #7                  // Field id:I
        18: invokevirtual #8                  // Method java/lang/StringBuilder.append:(I)Ljava/lang/StringBuilder;
        21: invokevirtual #9                  // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
        24: invokevirtual #10                 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
        27: return
      LineNumberTable:
        line 18: 0
        line 19: 27
}
SourceFile: "TestClass.java"

下面对Class文件中的各数据进行分析

魔数与版本号

  • 魔数占4个字节,值为CAFEBABE,共32bit(4个二进制数可以表示一个十六进制数,则32bit二进制可以用8个十六进制数表示)
  • 魔数唯一的作用是确定这个文件是否为一个能被虚拟机接受的Class文件
  • 使用魔数而不是扩展名进行识别,主要是出于安全方面的考虑,因为扩展名可以随意更改
  • 次版本号占2个字节,主版本号占2个字节
  • java版本号从45开始,所以主版本号最小为0x002D
  • 高版本的JDK能向下兼容低版本的Class文件,但不能运行更高版本的Class文件
  • 因为主、次版本号各占2个字节,所以最大可以为65535(2^{16}-1)

常量池

  • 可以将常量池理解为Class文件中的资源仓库
  • 常量池中的常量的个数不是固定的,所以在常量池前面有一个u2类型的数据,表示常量池的容量constant_pool_count,但是由于常量的计数索引是从1开始的,因此常量的个数=constant_pool_count-1
  • 常量池中存放着两类常量:字面量和符号引用,字面量类似于字符串、数值等常量,符号引用包括三类常量:
    • 类和接口的全限定名
    • 字段的名称和描述符
    • 方法的名称和描述符
  • 每一个常量可以理解为一个tag-value对,tag用来表示该常量的类型,并且每一个tag都是一个u1类型的无符号数,value表示该常量的具体的行为
  • JDK1.7中共有14种不同类型的常量
  • CONSTANT_Utf8_info类型的常量主要用来表示字符串,而该类型的常量的length是一个u2类型的无符号数,最大长度为65535,所以如果一个类的方法名或者字段名的长度超过64KB,将会无法编译

访问标志

  • 访问标志占2个字节
  • 这个标志用于标识一些类或者接口层次的访问信息
  • access_flags中一共有16个标志位可以使用,当前只定义了其中8个,没有使用到的标志位都为0
  • 把8种标志位的值进行或运算,运算结果就是access_flags的值

以下是具体的标志位和含义:

标志名称 标志值 含义
ACC_PUBLIC 0x0001 是否为public类型
ACC_FINAL 0x0010 是否为final类型,只有类可以设置
ACC_SUPER 0x0020 是否允许使用invokespecial字节码指令的新语意,JDK1.0.2之后类的这个标志都必须为真
ACC_INTERFACE 0x0200 是否为一个接口
ACC_ABSTRACT 0x0400 是否为abstract类型,接口或抽象类该值为真,其他为假
ACC_SYNTHETIC 0x1000 标识这个类并非由用户代码产生
ACC_ANNOTATION 0x2000 标识这是一个注解
ACC_ENUM 0x4000 标识这是一个枚举

类索引,父类索引,接口索引集合

  • 类索引和父类索引都是一个u2类型的数据,除了java.lang.Object类,所有类的父类索引都不为空,并且有且只有一个父类索引
  • 类索引和父类索引都指向一个CONSTANT_Class_info的类描述符常量,通过CONSTANT_Class_info常量中的索引值可以找到在CONSTANT_Utf8_info常量中定义的全限定名字符串
  • 接口索引集合是一组u2类型的数据的集合,用来标识该类实现了哪些接口

字段表集合

  • 字段表用以描述类或者接口中定义的变量
  • 字段包括类变量和实例变量,但不包括方法内部的局部变量
  • 一个字段可以有以下信息:
    • 变量的作用域,是public,private还是protected的
    • 属于类变量还是实例变量,即是否有static修饰符
    • 是否是不可变变量,即是否有final修饰符
    • 是否是并发可见的,即是否有valatile修饰符
    • 是否可以被序列化,即是否有transient修饰符
    • 变量的数据类型,是基本类型、对象还是数组
    • 变量的名称

字段表的结构如下:

类型 名称 数量 描述
u2 access_flags 1 字段修饰符,与类的access_flags非常相似
u2 name_index 1 字段的简单名称,是没有修饰符修饰的字段名称,是对常量池的引用
u2 descriptor_index 1 字段的描述符,用以描述字段的数据类型。基本数据类型以及无返回值void类型都用一个大写字符表示,对象类型用字符L加对象的全限定名表示,一维数组用[表示,二维数组用[[表示
u2 attributes_count 1 字段的属性表计数器
attribute_info attributes attributes_count 属性表集合,用于保存字段的一些额外的信息

方法表集合

  • 方法表用以描述类或者接口中定义的方法

方法表的结构如下:

类型 名称 数量 描述
u2 access_flags 1 方法修饰符,与类的access_flags非常相似,不包括volatile,transient修饰符
u2 name_index 1 方法的简单名称,是指没有类型和参数修饰的方法名称,是对常量池的引用
u2 descriptor_index 1 方法的描述符,用以描述方法的参数列表,包括数量,类型以及顺序
u2 attributes_count 1 方法的属性表计数器
attribute_info attributes attributes_count 属性表集合,用于保存方法的一些额外的信息,比如方法里的java代码将会保存在方法属性表集合中一个名为“Code”的属性里面

属性表集合

  • 在Class文件,字段表,方法表中都携带了自己的属性表集合,用以描述某些场景特有的信息
  • 属性表中的顺序不再是严格要求不变的,只要不予已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息
  • java虚拟机会忽略掉它不认识的属性
  • 《java虚拟机规范(第2版)》中预定义了9项虚拟机能识别的属性
  • 《java虚拟机规范(Java SE 7)》中预定义的属性增加到21项

以下列举一些常见的虚拟机规范预定义的属性:

属性名称 使用位置 含义
Code 方法表 java代码编译成的字节码指令
ConstantValue 字段表 final关键字定义的常量值
Exceptions 方法表 方法抛出的异常
InnerClasses 类文件 内部类列表
LineNumberTable Code属性 Java源码的行号与字节码指令的对应关系
LocalVariableTable Code属性 方法的局部变量描述
SourceFile 类文件 纪录源文件名称
更多原创好文,请关注「逅弈逐码」
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容