了解Class文件结构对了解JVM运行机制大有裨益,同时,对于想要使用BCEL来动态改变Class字节码指令的工作也很有帮助(示例:JVM Class字节码之三-使用BCEL改变类属性)。
1.Class文件总体描述
1.1 Class文件概述
- Java字节码文件(.class)是Java源文件(.java)编译后产生的目标文件;
- 是一种8位(1字节)为基础的二进制流文件;
- 每个数据项按严格的结构紧密排列在一起,字节间没有空隙分隔符(一个字节一个字节都有其所在位置的定义)
1.2 Class文件组成结构
Class文件由一种类似C语言结构体的结构来存储数据,主要由无符号数和表组成。
无符号数:基本数据类型,可以描述数字,索引符号,数量值,或UTF8编码组成的字符串值;大小有1个字节,2个字节,4个字节,8个字节四类,用(u1,u2,u4,u8)表示。
表:由无符号数和其他表组成的复杂数据类型;习惯以“_info”结尾;描述由层次关系的复杂结构(方法,字段等),其中每个数据项位置都有严格定义。
Class文件大致可分10个部分:魔数(MagicNumber)、Class文件版本(Version)、常量池(Constant_Pool)、访问标记(Access_flag)、本类(This_class)、父类(Super_class)、接口(Interfaces)、字段集合(Fields)、方法集合(Methods )、属性集合(Attributes)。
具体顺序定义如图:
2.详细结构
2.1 示例Java源码及编译后的字节码
Java:
public class ByteCodeT { private String name; String sayWords; int m; public String getName() { return name; } public void setName(String name) { this.name = name; } public int getM() { return m; } public void setM(int m) { this.m = m; } public void hi() { String s12 = "hi ".concat(name); hello(s12); } public void hello(String s2) { sayWords = s2; } public static void main(String[] args) { ByteCodeT byteCodeT = new ByteCodeT(); byteCodeT.setName("lisa"); byteCodeT.hi(); } }
字节码:
警告: 二进制文件ByteCodeT包含com.lims.pracpro.jdkprac.ByteCodeT Classfile /E:/Code/flickeringproject/pracpro/target/classes/com/lims/pracpro/jdkprac/ByteCodeT.class Last modified 2020-9-17; size 1267 bytes MD5 checksum bb431381e128c261c3d6a9f4dcfb0c23 Compiled from "ByteCodeT.java" public class com.lims.pracpro.jdkprac.ByteCodeT minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #13.#45 // java/lang/Object."<init>":()V #2 = Fieldref #8.#46 // com/lims/pracpro/jdkprac/ByteCodeT.name:Ljava/lang/String; #3 = Fieldref #8.#47 // com/lims/pracpro/jdkprac/ByteCodeT.m:I #4 = String #48 // hi #5 = Methodref #49.#50 // java/lang/String.concat:(Ljava/lang/String;)Ljava/lang/String; #6 = Methodref #8.#51 // com/lims/pracpro/jdkprac/ByteCodeT.hello:(Ljava/lang/String;)V #7 = Fieldref #8.#52 // com/lims/pracpro/jdkprac/ByteCodeT.sayWords:Ljava/lang/String; #8 = Class #53 // com/lims/pracpro/jdkprac/ByteCodeT #9 = Methodref #8.#45 // com/lims/pracpro/jdkprac/ByteCodeT."<init>":()V #10 = String #54 // lisa #11 = Methodref #8.#55 // com/lims/pracpro/jdkprac/ByteCodeT.setName:(Ljava/lang/String;)V #12 = Methodref #8.#56 // com/lims/pracpro/jdkprac/ByteCodeT.hi:()V #13 = Class #57 // java/lang/Object #14 = Utf8 name #15 = Utf8 Ljava/lang/String; #16 = Utf8 sayWords #17 = Utf8 m #18 = Utf8 I #19 = Utf8 <init> #20 = Utf8 ()V #21 = Utf8 Code #22 = Utf8 LineNumberTable #23 = Utf8 LocalVariableTable #24 = Utf8 this #25 = Utf8 Lcom/lims/pracpro/jdkprac/ByteCodeT; #26 = Utf8 getName #27 = Utf8 ()Ljava/lang/String; #28 = Utf8 setName #29 = Utf8 (Ljava/lang/String;)V #30 = Utf8 getM #31 = Utf8 ()I #32 = Utf8 setM #33 = Utf8 (I)V #34 = Utf8 hi #35 = Utf8 s12 #36 = Utf8 hello #37 = Utf8 s2 #38 = Utf8 main #39 = Utf8 ([Ljava/lang/String;)V #40 = Utf8 args #41 = Utf8 [Ljava/lang/String; #42 = Utf8 byteCodeT #43 = Utf8 SourceFile #44 = Utf8 ByteCodeT.java #45 = NameAndType #19:#20 // "<init>":()V #46 = NameAndType #14:#15 // name:Ljava/lang/String; #47 = NameAndType #17:#18 // m:I #48 = Utf8 hi #49 = Class #58 // java/lang/String #50 = NameAndType #59:#60 // concat:(Ljava/lang/String;)Ljava/lang/String; #51 = NameAndType #36:#29 // hello:(Ljava/lang/String;)V #52 = NameAndType #16:#15 // sayWords:Ljava/lang/String; #53 = Utf8 com/lims/pracpro/jdkprac/ByteCodeT #54 = Utf8 lisa #55 = NameAndType #28:#29 // setName:(Ljava/lang/String;)V #56 = NameAndType #34:#20 // hi:()V #57 = Utf8 java/lang/Object #58 = Utf8 java/lang/String #59 = Utf8 concat #60 = Utf8 (Ljava/lang/String;)Ljava/lang/String; { java.lang.String sayWords; descriptor: Ljava/lang/String; flags: int m; descriptor: I flags: public com.lims.pracpro.jdkprac.ByteCodeT(); 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 9: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/lims/pracpro/jdkprac/ByteCodeT; public java.lang.String getName(); descriptor: ()Ljava/lang/String; flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field name:Ljava/lang/String; 4: areturn LineNumberTable: line 15: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/lims/pracpro/jdkprac/ByteCodeT; public void setName(java.lang.String); descriptor: (Ljava/lang/String;)V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: aload_1 2: putfield #2 // Field name:Ljava/lang/String; 5: return LineNumberTable: line 19: 0 line 20: 5 LocalVariableTable: Start Length Slot Name Signature 0 6 0 this Lcom/lims/pracpro/jdkprac/ByteCodeT; 0 6 1 name Ljava/lang/String; public int getM(); descriptor: ()I flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #3 // Field m:I 4: ireturn LineNumberTable: line 23: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/lims/pracpro/jdkprac/ByteCodeT; public void setM(int); descriptor: (I)V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: iload_1 2: putfield #3 // Field m:I 5: return LineNumberTable: line 27: 0 line 28: 5 LocalVariableTable: Start Length Slot Name Signature 0 6 0 this Lcom/lims/pracpro/jdkprac/ByteCodeT; 0 6 1 m I public void hi(); descriptor: ()V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=1 0: ldc #4 // String hi 2: aload_0 3: getfield #2 // Field name:Ljava/lang/String; 6: invokevirtual #5 // Method java/lang/String.concat:(Ljava/lang/String;)Ljava/lang/String; 9: astore_1 10: aload_0 11: aload_1 12: invokevirtual #6 // Method hello:(Ljava/lang/String;)V 15: return LineNumberTable: line 31: 0 line 32: 10 line 33: 15 LocalVariableTable: Start Length Slot Name Signature 0 16 0 this Lcom/lims/pracpro/jdkprac/ByteCodeT; 10 6 1 s12 Ljava/lang/String; public void hello(java.lang.String); descriptor: (Ljava/lang/String;)V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: aload_1 2: putfield #7 // Field sayWords:Ljava/lang/String; 5: return LineNumberTable: line 36: 0 line 37: 5 LocalVariableTable: Start Length Slot Name Signature 0 6 0 this Lcom/lims/pracpro/jdkprac/ByteCodeT; 0 6 1 s2 Ljava/lang/String; public static void main(java.lang.String[]); descriptor: ([Ljava/lang/String;)V flags: ACC_PUBLIC, ACC_STATIC Code: stack=2, locals=2, args_size=1 0: new #8 // class com/lims/pracpro/jdkprac/ByteCodeT 3: dup 4: invokespecial #9 // Method "<init>":()V 7: astore_1 8: aload_1 9: ldc #10 // String lisa 11: invokevirtual #11 // Method setName:(Ljava/lang/String;)V 14: aload_1 15: invokevirtual #12 // Method hi:()V 18: return LineNumberTable: line 40: 0 line 41: 8 line 42: 14 line 43: 18 LocalVariableTable: Start Length Slot Name Signature 0 19 0 args [Ljava/lang/String; 8 11 1 byteCodeT Lcom/lims/pracpro/jdkprac/ByteCodeT; } SourceFile: "ByteCodeT.java"
Notepad用十六进制查看:
2.2 魔数
每个Class文件头4个字节为魔数,用于确定此文件是否是虚拟机能接受的Class文件,魔数值为 0xCAFEBABE ,如果不是0xCAFEBABE 开头,则不是Java的class文件。
2.3 Class文件版本号
版本号用于JVM确定能否支持兼容,紧接魔数的4个字节是版本号:
- 次版本号(minor_version): 前2字节用于表示次版本号
- 主版本号(major_version): 后2字节用于表示主版本号。
0X0034(对应十进制的50):JDK1.8
0X0033(对应十进制的50):JDK1.7
0X0032(对应十进制的50):JDK1.6
0X0031(对应十进制的49):JDK1.5
0X0030(对应十进制的48):JDK1.4
0X002F(对应十进制的47):JDK1.3
0X002E(对应十进制的46):JDK1.2
ps:0X表示16进制
2.4 常量池
紧接魔数和版本号后的是常量池,理解为Java文件的资源库。它是Java文件中与其他项目关联最多的数据项目,占用空间最大的数据项目之一,文件中第一个出现表类型的数据项目。
主要存有符号引用和字面量。
字面量:比较接近于Java语言层面的常量概念,如文本字符串、被声明为final的常量值等。
符号引用:属于编译原理方面的概念,包括了下面三类常量
- 类和接口的全限定名
- 字段的名称和描述符
- 方法的名称和描述符
Java编译时并不像C、C++有链接这一步操作,而是虚拟机加载Class文件的时候“动态链接”。也就是说,不报存方法字段在内存的布局信息,这些字段和方法的符号引用不经过转换,则无法使用。虚拟机运行时,需要从常量池中获取对应的符号引用,在类创建或者运行时解析到具体的内存地址中。
由于常量池数量不固定,所以常量池入口处有u2类型的数据(constant_pool_count)表示常量池数量。常量池计数从1开始,索引0的位置留个JVM自己,如constant_pool一共14项,则constant_pool_count就为15。
Class类中有很多部分都是对常量池的引用,如this_class,super_class, field_info, attribute_info等;字节码指令也会对常量池引用(当做指令操作数),常量池各项也会相互引用。
常量池维护着经过编译“梳理”之后的相对固定的数据索引,它是站在整个JVM(进程)层面的共享池。
constant_pool_count:2个字节,常量池数目。
如图,常量池总共有61(0x3d)项,从1计数的常量有60项。
constant_pool:常量池中表类型数据集合,每一项常量都是一个表。共有14种(JDK1.7前只有11种)结构各不相同的表结构数据。这14种表都有一个共同的特点,即均由一个u1类型的标志位开始,可以通过这个标志位来判断这个常量属于哪种常量类型:
譬如utf-8类型的表结构数据:
譬如fieldref类型的表结构数据:
譬如class类型的表结构数据:
譬如nameandtype类型的表结构数据:
ps:什么是描述符?
成员变量(包括静态成员变量和实例变量) 和方法都有各自的描述符。
对于字段而言,描述符用于描述字段的数据类型;
对于方法而言,描述符用于描述字段的数据类型、参数列表、返回值。
在描述符中,基本数据类型用大写字母表示,对象类型用“L对象类型的全限定名”表示,数组用“[数组类型的全限定名”表示。
描述方法时,将参数根据上述规则放在()中,()右侧按照上述方法放置返回值。而且参数之间无需任何符号。
实例说明:
2.1字节码中,Methodref类型的常量(第1项)
u1标记位10(0x0a),表示Methodref类型常量:
u2为13(0x0d)号引用——class index(类索引):
u2为45(0x2d)号索引引用——NameAndType index:
2.1字节码中,utf-8类型的常量(第14项)
u1标记位01,表示为utf-8类型常量:
u2长度为4(0x04),后面4个字节为该常量;
4个字节长度的常量 —— “name”
2.5 访问标记(2字节)
访问标志(access_flags),这个标志主要用于识别一些类或接口层次的访问信息,主要包括:
- 是否final
- 是否public,否则是private
- 是否是接口
- 是否可用invokespecial字节码指令
- 是否是abstact
- 是否是注解
- 是否是枚举
access_flags一共有16个标志位可以使用,当前只定义了其中8个(JDK1.5增加后面3种),没有使用到标志位一律为0。
JVM访问标记规范:
2.6 类/父类/接口索引集合
这三项数据主要用于确定这个类的继承关系。
其中类索引(this_class)和父类索引(super_class)都是一个u2类型的数据,而接口索引(interface)集合是一组u2类型的数据。(多实现单继承)
2.7 Fields字段表集合
fields_count:字段表计数器(2字节)
fields:字段表集合
字段表结构:
类型 |
名称 |
数量 |
说明 |
u2 |
access_flags |
1 |
修饰符标记位 |
u2 |
name_index |
1 |
代表字段的简单名称,占2字节,是一个对常量池的引用 |
u2 |
descriptor_index |
1 |
代表字段的类型,占2个字节,是一个对常量池的引用 |
u2 |
attributes_count |
1 |
属性计数器 |
attribute_info |
attributes |
attributes_count |
属性表集合 |
field_info { u2 access_flags; u2 name_index; u2 descriptor_index; u2 attributes_count; attribute_info attributes[attributes_count]; }
2.8 方法表集合
methods_count:方法表计数器(2字节)
methods:方法表集合
方法表计数器,表示有8个方法:
方法表结构:
method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
u2的01访问控制:
u2的19(0x13)为名称索引——常量池19号(<init>):
u2的20(0x14)号描述索引——常量池20号:
u2位属性计数器——此方法有1个属性:
属性的结构:
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
u2的21(0x15)为属性名索引——常量池21号(Code),表示Code属性
u4表示Code属性长度为47(0x2f),后面47个字节为Code属性详细信息
Code属性的结构:
Code_attribute {
u2 attribute_name_index;
u4 attribute_length;
u2 max_stack;
u2 max_locals;
u4 code_length;
u1 code[code_length];
u2 exception_table_length;
{
u2 start_pc;
u2 end_pc;
u2 handler_pc;
u2 catch_type;
} exception_table[exception_table_length];
u2 attributes_count;
attribute_info attributes[attributes_count];
}
code属性详细信息,可依照上面的结构分析
2.9 属性表集合
起始2个字节为0x0001,说明有一个类属性。
接下来2个字节为属性的名称,0x0010,指向常量池中第16个常量:SourceFile。
接下来4个字节为0x00000002,说明属性体长度为2字节。
最后2个字节为0x0011,指向常量池中第27个常量:TestClass.java,即这个Class文件的源码文件名为TestClass.java
与Class文件中其它数据项对长度、顺序、格式的严格要求不同,属性表集合不要求其中包含的属性表具有严格的顺序,并且只要属性的名称不与已有的属性名称重复,任何人实现的编译器可以向属性表中写入自己定义的属性信息。虚拟机在运行时会忽略不能识别的属性,为了能正确解析Class文件,虚拟机规范中预定义了虚拟机实现必须能够识别的9项属性(预定义属性已经增加到21项):
属性名称 |
使用位置 |
含义 |
Code |
方法表 |
Java代码编译成的字节码指令 |
ConstantValue |
字段表 |
final关键字定义的常量值 |
Deprecated |
类文件、字段表、方法表 |
被声明为deprecated的方法和字段 |
Exceptions |
方法表 |
方法抛出的异常 |
InnerClasses |
类文件 |
内部类列表 |
LineNumberTale |
Code属性 |
Java源码的行号与字节码指令的对应关系 |
LocalVariableTable |
Code属性 |
方法的局部变量描述(局部变量作用域) |
SourceFile |
类文件 |
源文件名称 |
Synthetic |
类文件、方法表、字段表 |
标识方法或字段是由编译器自动生成的 |
ps:在调试是可以通过SourceFile来关联相关的类。
PS:
1,全限定名:将类全名中的“.”替换为“/”,为了保证多个连续的全限定名之间不产生混淆,在最后加上“;”表示全限定名结束。例如:"com.test.Test"类的全限定名为"com/test/Test;"
2,简单名称:没有类型和参数修饰的方法或字段名称。例如:"public void add(int a,int b){...}"该方法的简单名称为"add","int a = 123;"该字段的简单名称为"a"
3,描述符:描述字段的数据类型、方法的参数列表(包括数量、类型和顺序)和返回值。根据描述符规则,基本数据类型和代表无返回值的void类型都用一个大写字符表示,而对象类型则用字符L加对象全限定名表示
标识字符 |
含义 |
B |
基本类型byte |
C |
基本类型char |
D |
基本类型double |
F |
基本类型float |
I |
基本类型int |
J |
基本类型long |
S |
基本类型short |
Z |
基本类型boolean |
V |
特殊类型void |
L |
对象类型,如:Ljava/lang/Object; |
对于数组类型,每一维将使用一个前置的“[”字符来描述,如:"int[]"将被记录为"[I","String[][]"将被记录为"[[Ljava/lang/String;"
用描述符描述方法时,按照先参数列表,后返回值的顺序描述,参数列表按照参数的严格顺序放在一组"()"之内,如:方法"String getAll(int id,String name)"的描述符为"(I,Ljava/lang/String;)Ljava/lang/String;"
4,Slot,虚拟机为局部变量分配内存所使用的最小单位,长度不超过32位的数据类型占用1个Slot,64位的数据类型(long和double)占用2个Slot