Java的.class文件剖析

测试Java类

public class TestDemo {
    public static final String SUCCESS = "success";
    public static void main(String[] args) {
        System.out.println("main");
    }
    private String printText(String str) {
        System.out.println(str);
        return SUCCESS;
    }
}

编译

javac DemoTest.java

.class文件结构

cafe babe 0000 0034 0023 0a00 0700 1509
0016 0017 0800 0f0a 0018 0019 0700 1a08
001b 0700 1c01 0007 5355 4343 4553 5301
0012 4c6a 6176 612f 6c61 6e67 2f53 7472
696e 673b 0100 0d43 6f6e 7374 616e 7456
616c 7565 0100 063c 696e 6974 3e01 0003
2829 5601 0004 436f 6465 0100 0f4c 696e
654e 756d 6265 7254 6162 6c65 0100 046d
6169 6e01 0016 285b 4c6a 6176 612f 6c61
6e67 2f53 7472 696e 673b 2956 0100 0970
7269 6e74 5465 7874 0100 2628 4c6a 6176
612f 6c61 6e67 2f53 7472 696e 673b 294c
6a61 7661 2f6c 616e 672f 5374 7269 6e67
3b01 000a 536f 7572 6365 4669 6c65 0100
0d54 6573 7444 656d 6f2e 6a61 7661 0c00
0b00 0c07 001d 0c00 1e00 1f07 0020 0c00
2100 2201 0017 636f 6d2f 6a65 7375 732f
7574 696c 2f54 6573 7444 656d 6f01 0007
7375 6363 6573 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 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 0500 0700 0000 0100 1900
0800 0900 0100 0a00 0000 0200 0600 0300
0100 0b00 0c00 0100 0d00 0000 1d00 0100
0100 0000 052a b700 01b1 0000 0001 000e
0000 0006 0001 0000 0003 0009 000f 0010
0001 000d 0000 0025 0002 0001 0000 0009
b200 0212 03b6 0004 b100 0000 0100 0e00
0000 0a00 0200 0000 0800 0800 0900 0200
1100 1200 0100 0d00 0000 2600 0200 0200
0000 0ab2 0002 2bb6 0004 1206 b000 0000
0100 0e00 0000 0a00 0200 0000 0c00 0700
0d00 0100 1300 0000 0200 14

.class文件组成

以上可知,.class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在.class文件之中,中间没有添加任何分隔符;

根据Java虚拟机规范的规定,.class文件格式采用一种类似于C语言的伪结构来存储数据,包含无符号数和表:

无符号数:以u1、u2、u4、u8来分别代表1个字节、2个字节、4个字节和8个字节的无符号数,无符号数可以用来描述数字、索引引用、数量值或者按照UTF-8编码构成的字符串值;
表:由多个无符号数或者其他表作为数据项构成的复合数据类型,所有表都习惯性的以”_info“结尾。

Class文件的结构没有分隔符,无论你是数量还是顺序,都是严格规定的,哪个字节代表什么含义、长度多少、先后顺序如何,都不允许改变。

.class文件结构

ClassFile {
  u4             magic; // 魔数,固定值 0xCAFEBABE
  u2             minor_version; // 副版本号
  u2             major_version; // 主版本号
  u2             constant_pool_count; // 常量池计数器
  cp_info        constant_pool[constant_pool_count-1]; // 常量池
  u2             access_flags; // 访问标志
  u2             this_class; // 类索引
  u2             super_class; // 父类索引
  u2             interfaces_count; // 接口计数器
  u2             interfaces[interfaces_count]; // 接口表
  u2             fields_count; // 字段计数器
  field_info     fields[fields_count]; // 字段表
  u2             methods_count; // 方法计数器
  method_info    methods[methods_count]; // 方法表
  u2             attributes_count; // 属性计数器
  attribute_info attributes[attributes_count]; // 属性表
}

1、魔数
魔数(Magic Number),u4,即class文件中用4个字节表示,他的唯一作用就是确定这个文件是否为一个能否被虚拟机所识别的class文件,魔数的固定值为0xCAFEBABE,不会改变;

2、文件版本号
紧挨着魔数的2个字节为副版本号minor_version(上面class文件例子中0x0000),接下来的2个字节为主版本号major_version(上面例子中的0x0034,换算成10进制为52,查询jdk版本对应关系,主版本52为jdk 1.8);

3、常量池计数器
接下来的2个字节表示常量池计数器constant_pool_count,常量池中的数量不固定,constant_pool_count的值 = 常量池中的成员数 + 1,常量池的索引从1开始;

例如上面class文件中的16进制数为0x0023,换算成10进制为35,通过命令“javap -v DemoTest.class"来查看常量池,上面class文件的常量池内容如下:

xxxMacBook-Pro:JVM_test xxx$ javap -v TestDemo.class 
Classfile /Users/xxx/xxx/TestDemo.class
  Last modified 2019-12-4; size 603 bytes
  MD5 checksum 47bd6066637c077873b14b73e810b1e2
  Compiled from "TestDemo.java"
public class com.jesus.util.TestDemo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#21         // java/lang/Object."<init>":()V
   #2 = Fieldref           #22.#23        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #15            // main
   #4 = Methodref          #24.#25        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #26            // com/jesus/util/TestDemo
   #6 = String             #27            // success
   #7 = Class              #28            // java/lang/Object
   #8 = Utf8               SUCCESS
   #9 = Utf8               Ljava/lang/String;
  #10 = Utf8               ConstantValue
  #11 = Utf8               <init>
  #12 = Utf8               ()V
  #13 = Utf8               Code
  #14 = Utf8               LineNumberTable
  #15 = Utf8               main
  #16 = Utf8               ([Ljava/lang/String;)V
  #17 = Utf8               printText
  #18 = Utf8               (Ljava/lang/String;)Ljava/lang/String;
  #19 = Utf8               SourceFile
  #20 = Utf8               TestDemo.java
  #21 = NameAndType        #11:#12        // "<init>":()V
  #22 = Class              #29            // java/lang/System
  #23 = NameAndType        #30:#31        // out:Ljava/io/PrintStream;
  #24 = Class              #32            // java/io/PrintStream
  #25 = NameAndType        #33:#34        // println:(Ljava/lang/String;)V
  #26 = Utf8               com/jesus/util/TestDemo
  #27 = Utf8               success
  #28 = Utf8               java/lang/Object
  #29 = Utf8               java/lang/System
  #30 = Utf8               out
  #31 = Utf8               Ljava/io/PrintStream;
  #32 = Utf8               java/io/PrintStream
  #33 = Utf8               println
  #34 = Utf8               (Ljava/lang/String;)V
{
  public static final java.lang.String SUCCESS;
    descriptor: Ljava/lang/String;
    flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL
    ConstantValue: String success

  public com.jesus.util.TestDemo();
    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 3: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
         3: ldc           #3                  // String main
         5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
         8: return
      LineNumberTable:
        line 8: 0
        line 9: 8
}
SourceFile: "TestDemo.java"

以上Constant pool中的常量有34个,+1正好就等于constant_pool_count;

4、常量池
常量池是class文件中占用空间最大的数据项目之一,由于常量池中的常量数量不固定,所以在常量池前面需要一个u2类型的数据,代表其大小,Java虚拟机指令不依赖于类、接口、类实例或数组的运行时布局,而是依赖常量池表中的符号信息,常量池的表结构如下(常量池中的每一项常量都是一个表):

cp_info {
  u1 tag; // 标志位,用于区分常量类型
  u1 info[];
}

常量池表中,开始都是由一个u1类型的tag标志位开始,代表这个常量的类型,后面的info[]数组内容有tag的值决定,tag值对应关系如下:

        类型                              标志              描述
CONSTANT_utf8_info                         1          UTF-8编码的字符串
CONSTANT_Integer_info                      3          整形字面量
CONSTANT_Float_info                        4          浮点型字面量
CONSTANT_Long_info                         5          长整型字面量
CONSTANT_Double_info                       6          双精度浮点型字面量
CONSTANT_Class_info                        7          类或接口的符号引用
CONSTANT_String_info                       8          字符串类型字面量
CONSTANT_Fieldref_info                     9          字段的符号引用
CONSTANT_Methodref_info                   10          类中方法的符号引用
CONSTANT_InterfaceMethodref_info          11          接口中方法的符号引用
CONSTANT_NameAndType_info                 12          字段或方法的符号引用
CONSTANT_MothodType_info                  16          标志方法类型
CONSTANT_MethodHandle_info                15          表示方法句柄
CONSTANT_InvokeDynamic_info               18          表示一个动态方法调用点

5、访问标志
access_flags为访问标志,用于表示某个类或者接口的访问权限及属性,是类还是接口等,取值对应如下:

  标志名                值          含义
ACC_PUBLIC           0x0001     声明为public,可以包外访问
ACC_FINAL            0x0010     声明为final,不允许有子类
ACC_SUPER            0x0020     当用到invokespecial指令时,需要对父类方法做特殊处理
ACC_INTERFACE        0x0200class文件定义的是接口而不是类
ACC_ABSTRACT         0x0400     声明为abstract,不能被实例化
ACC_SYNTHETIC        0x1000     声明为synthetic,表示该class文件并非由Java源代码所生成
ACC_ANNOTATION       0x2000     标识注解类型
ACC_ENUM             0x4000     标识枚举类型

取后面2个字节u2类型,即0x0021,发现并未找到对应的值,其实该值为所有访问标志通过或运算得来的值,0x0021=0x0001|0x0020,即public和super,通过javap命令打印出的类标志符也为:flags: ACC_PUBLIC, ACC_SUPER,正好对应;

对于Java SE 8及后续版本,无论class文件中的这个标志的实际值是什么,也不管class文件的版本号是多少,Java虚拟机都认为每个class文件均设置了ACC_SUPER标志;

6、类索引
类索引,u2类型,this_class的值必须是常量池中某个有效索引,并且索引处的成员必须为CONSTANT_Class_info结构,表示class文件所定义的类或者接口,如上面class文件,继续取后面2个字节的u2类型,0x0005,对应常量池中的#5,即Class类型;

7、父类索引
父类索引,u2类型,super_class值要么是0,要么是对常量池中某项的一个有效索引,如果不为0,在常量池中的引用处成员必须是CONSTANT_Class_info类型,表示这class文件所定义的类的直接超类,如果为0,那么这个class只可能用来表示Object类,因为他是唯一没有父类的类;

取上面class文件的2个字节u2类型,0x0007,十进制7,常量池中#7,即Class类型;

8、接口计数器
接口计数器,u2类型,interfaces_count的值表示接口索引表的容量,即当前类或者接口的直接超接口数量,取上面class文件的2个字节u2类型,0x0000,即0,没有接口,该值为0时,接口表不占用任何字节;

9、接口表
接口表,interfaces[]中每个成员的值必须是常量池中的某个有效索引,长度为interfaces_count,每个成员必须是CONSTANT_Class_info结构,由于接口数量为0,所以这里没有接口;

10、字段计数器
字段计数器,u2类型,fields_count表示当前class文件的字段数量,如上class文件,取2个字节u2类型,0x0001,即该类有1个成员字段,即java文件中定义了一个String类型的成员字段SUCCESS;

11、字段表
字段表fields[fields_count],用于描述接口或类中声明的变量,其中每个成员的结构均为field_info,用于描述当前类或者接口中某个字段的完整描述,只描述当前类或接口,不包括父类或父接口集成的字段,结构如下:

field_info {
	u2              access_flags;
	u2              name_index;
	u2              descriptor_index;
	u2              attributes_count;
	attributes_info attributes[attributes_count];
}

其中access_flags表示子弹的访问权限和基本属性,对应的含义如下:

   标志名            值            说明
ACC_PUBLIC        0x0001       声明为public,可以包外访问
ACC_PRIVATE       0x0002       声明为private,只能在定义该字段的类访问
ACC_PROTECTED     0x0004       声明为protected,子类可以访问
ACC_STATIC        0x0008       声明为static
ACC_FINAL         0x0010       声明为final
ACC_VOLATILE      0x0040       声明为volatile,被表识的字段无法缓存  
ACC_TRANSIENT     0x0080       声明为transient,被标识的字段不会为持久化对象管理器所写入或读取
ACC_SYNTHETIC     0x1000       被标识的字段由编译器产生,而没有写源代码中
ACC_ENUM          0x4000       枚举的成员

如上class文件中,取字段数量后面的u2类型的access_flags,即0x0019,即0x0019=0x0001|0x0008|0x0010,对应PUBLIC,STATIC,FINAL,正对应java文件的SUCCESS字段的访问限定符”public static final“;

name_index的值必须为常量池中的一个有效索引,该处的成员必须是CONSTANT_Utf8_info结构,表示一个有效的非限定名,这个名词对应于本字段,取2个字节的u2类型,即0x0008,对应常量池中的#8,即Utf8类型,对应值为”SUCCESS“,即字段名;

descriptor_index的值必须为常量池的一个有效索引,类型必须是CONSTANT_Utf8_info,表示一个有效的字段描述符,用来描述字段的数据类型、方法的参数列表(包括数量、类型及顺序)和返回值,取2个字节的u2类型,0x0009,即对应常量池的#9,Utf8类型,返回值用一个大写字符来表示,如下:

标识字符        含义
   B        基本类型byte
   C        基本类型char
   D        基本类型double
   F        基本类型float
   I        基本类型int
   J        基本类型long
   S        基本类型short
   Z        基本类型boolean
   V        特殊类型void
   L        对象类型,如Ljava/lang/Object

另外,对于数组类型,每一位度将使用一个前置”[“字符来描述,例如定义一个String[][]的二维数组,将被表示为”[[Ljava/lang/String;“,整型数组为”[I“;

方法描述的顺序为:先参数列表后返回值,参数列表按照参数的严格顺序放在一组小括号”()“内,例如:

int indexOf(char[] source, int sourceOffset, int sourceCount, char[] target, int targetOffset, int targetCount, int fromIndex)的描述为:”([CII{CIII)I“;

接着取后面u2类型的2个字节0x0009,十进制为9,常量池中#9为Utf8,值为”Ljava/lang/String;“,即该变量为String类型;

attributes_count的值表示当前字段的附加属性的数量,取2个字节的u2类型,0x0001,即有一个附加属性;

attributes[]属性表中的每个成员,一个字段可以关联任意多个属性(属性见下文2.2.15);

取2个字节的attribute_name_index,0x000a,十进制为10,对应常量池有效索引#10,对应Utf8类型,即”ConstantValue“(属性见后文2.2.15.1);

ConstantValue的结构为(后面分析):

{
	u2 attribute_name_index; // 标识
	u4 attribute_length; // 固定为2
	u2 constantvalue_index; // 常量池中的一个有效索引
}

根据ConstantValue的结构,attribute_length固定为2,取u4类型的4个字节,0x0000 0002(见后文2.2.15.1);

继续取u2类型的2个字节ConstantValue的constantvalue_index,即0x0006,对应常量池#6,即String类型,值为”success“;

12、方法计数器
方法计数器的结构为u2类型,即表示该类或接口中定义了多少个方法;

如上class文件中,取u2类型的2个字节0x0003,即对应该类中定了三个方法,观察java文件对应的方法为2个,类中未显示定义无参构造方法时,编译器会默认加上,所以为3个方法;

13、方法表
方法表中的方法描述和字段描述几乎一样,同上,方法吧的结构为:

{
	u2             access_flags; // 访问限定符
	u2             name_index; // 名称索引
	u2             descripotr_index; // 描述符索引
	u2             attributes_count; // 属性数量
	attribute_info attributes; // 属性表集合
}

方法表访问标志符如下表:

  标志名称             标志值          含义
ACC_PUBLIC           0x0001     方法是否为public
ACC_PRIVATE          0x0002     方法是否为private
ACC_PROTECTED        0x0004     方法是否为protected
ACC_STATIC           0x0008     方法是否为static
ACC_FINAL            0x0010     方法是否为final
ACC_SYNCHRONIZED     0x0020     方法是否为synchronized
ACC_BRIDGE           0x0040     方法是否由编译器产生的桥接方法
ACC_VARARGS          0x0080     方法是否接受不定参数
ACC_NATIVE           0x0100     方法是否为native
ACC_ABSTRACT         0x0400     方法是否为abstract
ACC_STRICTFP         0x0800     方法是否为strictfp
ACC_SYNTHETIC        0x1000     方法是否是由编译器自动产生的

同字段表描述一样,取u2类型2个字节0x0001的access_flags,即对应PUBLIC;

u2类型的2个字节的name_index,0x000b,十进制为11,常量池中#11为Utf8类型,对应值为,即实例构造器;

u2类型的2个字节的descripotr_index,0x000c,十进制为12,常量池中#12为Utf8,值为”()V“,即无参的返回值为void;

u2类型的2个字节的attributes_count,0x0001,即一个属性值;

取u2类型的2个字节的属性标识,0x000d,十进制为13,常量池中#13为Utf8,值为”Code“,该值为固定值(见下文属性2.2.15.2);

u4类型4个字节attribute_length,0x0000 001d,对应十进制29,属性值的长度为29,往后29个字节都是该方法的描述即0x0001 0001 0000 0005 2a b7 00 01 b1 0000 0001 000e 0000 0006 0001 0000 0003;

u2类型max_stacks,0x0001,操作数栈深度为1,u2类型max_locals,0x0001,局部变量空间为1(对应javap生成的内容可验证);

u4类型字节码长度,0x0000 0005,即字节码长度为5;

连续取5个u1类型的字节码,0x2a、0xb7、0x00、0x01、0xb1,查询虚拟机字节码指令表可知分别对应:aload_0、invokespecial、return;

字节码指令详见《深入理解Java虚拟机·附录B 虚拟机字节码指令表》

aload_0:将第一个引用类型本地变量推送至栈顶;
invokespecial:调用超类构造方法,实例初始化方法,私有方法,该方法有一个u2类型的参数说明,即后面0x0001,结构为CONSTANT_Utf8_info,常量池中#1值为”java/lang/Object.""😦)V“,由此可知为无参的构造方法;
return:从当前方法返回void,执行这条指令后,当前方法结束;

由上分析可对应javap生成的内容验证,对应3条指令:

Code:
	stack=1, locals=1, args_size=1
		0: aload_0
		1: invokespecial #1                  // Method java/lang/Object."<init>":()V
		4: return

备注:上面locals=1,args_size=1,但是方法没有参数,在非static方法中,默认有一个this参数;

备注:字节码指令解析时,需要注意的是有些字节码指令是带参数的,如上面的invokespecial,后面跟着两个参数;

接着后面是u2类型的异常数量以及异常表,0x0000,即该方法没有异常表;

继续取u2类型的attribute_count,0x0001,即1个属性;

取u2类型2个字节属性名,0x000e,十进制为14,常量池中#14为Utf8,值为:”LineNumberTable“(见后文2.2.15.3);

根据下文中的LineNumberTable结构,取u4类型的4个字节的attribute_length,0x0000 0006,即属性值的长度为6,继续取u2类型的2个字节的line_number_table_length,0x0001,即line_number_table集合长度为1,line_number_table结构为2个u2类型,分别对应start_pc和line_number,即0x0000、0x0003,分别对应字节码指令行号为0,Java源码行号为3,与javap生成的内容对比验证一致:

LineNumberTable:
	line 3: 0

后面其他方法的分析,main方法:

0x0009 000f 0010 0001 000d 0000 0025 0002 0001 0000 0009 b200 0212 03b6 0004 b100 0000 0100 0e00 0000 0a00 0200 0000 0800 0800 09

printText方法:

0x0002 0011 0012 0001 000d 0000 0026 0002 0002 0000 000a b200 022b b600 0412 06b0 0000 0001 000e 0000 000a 0002 0000 000c 0007 000d

14、属性计数器
属性计数器,u2类型,表明该类的属性数量,例如本class文件的0x0001,即1个属性描述;

15、属性表
Class文件、字段表、方法表都可以携带自己的属性表集合,用于描述某些场景专有的信息;

例如上面class文件中,分析到最后,字节码文件只剩下”0x0013 0000 0002 0014“,根据属性表的结构,取u2类型的2个字节的属性名,0x0013,十进制为19,常量池中#19为Utf8,值为”SourceFile“(下文2.2.15.4),接着看后面的SourceFile的结构分析,取u4类型的4个字节的属性长度,0x0000 0002,即两个长度,取u2类型的2个字节属性描述,0x0014,十进制为20,常量池中#20为Utf8,值为”TestDemo.java“;

通过验证javap生成的文件可知正好对应:

SourceFile: "TestDemo.java"

属性表的关键字常用部分如下(详见《深入理解Java虚拟机》):

属性名称              使用位置              含义
Code                 方发表          Java代码编译成的字节码指令
ConstantValue        字段表          final关键字定义的常量值
InnerClasses         类文件          内部类列表
LineNumberTable     Code属性        Java源码的行号与字节码指令的对应关系
localVariableTable  Code属性        方法的局部变量描述
...

对于每个属性,名称即标志位表示的,需要从常量池中引用一个CONSTANT_Utf8_info类型的常量表示,属性值的结构则由每个不同的属性自己独有的结构,需要一个u4长度的属性去说明属性值所占用的位数即可;

属性表的结构:

{
	u2 attribute_name_index; // 属性名
	u4 attribute_length; // 属性值的长度
	u1 info; // 属性
}

如有雷同,纯属抄袭,谢谢,谢谢

原文链接Java编译后的class文件解析

评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值