“一次编写,处处运行(Write Once,Run Anywhere)“,这是 Java 诞生之时一个很是著名的口号。在学习 Java 之初,就了解到了咱们所写的.java
会被编译期编译成.class
文件以后被 JVM 加载运行。JVM 全称为 Java Virtual Machine
,一直觉得 JVM 执行 Java 程序是一件理所固然的事情,但随着工做过程当中接触到了愈来愈多的基于 JVM 实现的语言如Groovy
Kotlin
Scala
等,就深入的理解到了 JVM 和 Java 的无关性,JVM 运行的不是 Java 程序,而是符合 JVM 规范的.class
字节码文件。字节码是各类不一样平台的虚拟机与全部平台都统一使用的程序储存格式。是构成Run Anywhere
的基石。所以了解 Class 字节码文件对于咱们开发、逆向都是十分有帮助的。html
Class文件是一组以 8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在 Class 文件中,中间没有添加任何分隔符,这使得整个 Class 文件中存储的内容几乎所有是程序运行的必要数据,没有空隙存在。当遇到须要占用 8 位字节以上空间的数据项时,则会按照Big-Endian
的方式分割成若干个 8 字节进行存储。Big-Endian
具体是指最高位字节在地址最低位、最低位字节在地址最高位的顺序来存储数据。SPARC
、PowerPC
等处理器默认使用Big-Endian
字节存储顺序,而x86
等处理器则是使用了相反的Little-Endian
顺序来存储数据。所以为了Class文件的保证平台无关性,JVM必须对其规范统一。java
在讲解Class类文件结构以前须要先介绍两个概念:无符号数和表。一种相似 C 语言结构体的伪结构。web
_info
结尾,用于描述有层次关系的复合结构的数据。当须要描述同一类型但数量不定的多个数据时,常常会使用一个前置的容量计数器加若干个连续的数据项的形式,这时就表明此类型的集合。整个 Class文件本质上就是一张表,其数据项以下伪代码所示:数组
ClassFile { u4 magic; 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]; }
每项数据项的含义咱们能够对照下图参照表:oracle
同时咱们将根据一个具体的 Java 类来分析 Class 文件结构jvm
public class ByteCode { private String username; public String getUsername() { return username; } public void setUsername(String username) { this.username = username; } }
其.class 文件内容以下:工具
使用 javap
命令能够获得反汇编代码:post
Classfile /Users/chenjianyuan/IdeaProjects/blog/blog-web/target/test-classes/tech/techstack/blog/ByteCode.class Last modified 2020-8-8; size 581 bytes MD5 checksum 43eb79f48927d9c5bbecfa5507de0f3c Compiled from "ByteCode.java" public class tech.techstack.blog.ByteCode minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #4.#21 // java/lang/Object."<init>":()V #2 = Fieldref #3.#22 // tech/techstack/blog/ByteCode.username:Ljava/lang/String; #3 = Class #23 // tech/techstack/blog/ByteCode #4 = Class #24 // java/lang/Object #5 = Utf8 username #6 = Utf8 Ljava/lang/String; #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Ltech/techstack/blog/ByteCode; #14 = Utf8 getUsername #15 = Utf8 ()Ljava/lang/String; #16 = Utf8 setUsername #17 = Utf8 (Ljava/lang/String;)V #18 = Utf8 MethodParameters #19 = Utf8 SourceFile #20 = Utf8 ByteCode.java #21 = NameAndType #7:#8 // "<init>":()V #22 = NameAndType #5:#6 // username:Ljava/lang/String; #23 = Utf8 tech/techstack/blog/ByteCode #24 = Utf8 java/lang/Object { public tech.techstack.blog.ByteCode(); 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 7: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Ltech/techstack/blog/ByteCode; public java.lang.String getUsername(); descriptor: ()Ljava/lang/String; flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field username:Ljava/lang/String; 4: areturn LineNumberTable: line 11: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Ltech/techstack/blog/ByteCode; public void setUsername(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 username:Ljava/lang/String; 5: return LineNumberTable: line 15: 0 line 16: 5 LocalVariableTable: Start Length Slot Name Signature 0 6 0 this Ltech/techstack/blog/ByteCode; 0 6 1 username Ljava/lang/String; MethodParameters: Name Flags username } SourceFile: "ByteCode.java"
每一个 Class 文件的头 4 个字节0xCAFEBABE
称为魔数(Magic Number),用来肯定这个文件是否为能被虚拟机接受的 Class 文件格式。学习
第 五、6 个字节为次版本号(minor_version),第 六、7 个字节是主版本号(major version)上图次版本号 00 00
转换为 10 进制为 0,主版本号 00 34
转换为十进制为 52,表明 JDK 1.8。观察反汇编代码也能获得次版本和主版本信息。高版本的 JDK 向下兼容低版本的 Class 文件,但低版本不能运行高版本的 Class 文件,即便文件格式没有发生任何变化,虚拟机也拒绝执行高于其版本号的 Class 文件。ui
后面紧跟着的 2 个字节为常量池个数(constant_pool_count),而后后面紧跟 constant_pool_count 个数的常量。constant_pool_count 是从 1 开始而不是从 0 开始,是为了将 0 项空出来标识后面某些指向常量池的索引值的数据在特定状况下不引用常量池,这种状况下就能够把索引值置为 0 来表示。(除常量池计数外,对于其余类型集合包括接口索引集合、字段表集合、方法表集合等的容量计数都与通常习惯相同,是从0开始的)
常量池(constant_pool)主要存放两大类常量:
常量池中的每个常量都是一个常量表,常量表开始的第一位是一个u1类型的标志位(tag),来区分常量表的类型。在JDK 1.7以前共有11种结构各不相同的表结构数据,在JDK 1.7中为了更好地支持动态语言调用,又额外增长了3种(CONSTANT_MethodHandle_info、CONSTANT_MethodType_info和CONSTANT_InvokeDynamic_info),14 中常量类型所表明的具体含义以下:
咱们对其按照字面量和符号引用类型分类的话能够入下图所示
Class文件中的常量池结构经过上例汇编代码可看出:
Constant pool: #1 = Methodref #4.#21 // java/lang/Object."<init>":()V #2 = Fieldref #3.#22 // tech/techstack/blog/ByteCode.username:Ljava/lang/String; #3 = Class #23 // tech/techstack/blog/ByteCode #4 = Class #24 // java/lang/Object #5 = Utf8 username #6 = Utf8 Ljava/lang/String; #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 LocalVariableTable #12 = Utf8 this #13 = Utf8 Ltech/techstack/blog/ByteCode; #14 = Utf8 getUsername #15 = Utf8 ()Ljava/lang/String; #16 = Utf8 setUsername #17 = Utf8 (Ljava/lang/String;)V #18 = Utf8 MethodParameters #19 = Utf8 SourceFile #20 = Utf8 ByteCode.java #21 = NameAndType #7:#8 // "<init>":()V #22 = NameAndType #5:#6 // username:Ljava/lang/String; #23 = Utf8 tech/techstack/blog/ByteCode #24 = Utf8 java/lang/Object
观察上面Class文件00 19
表示有 25 个常量,依次日后数 24(25-1)个常量则为常量池中的常量。紧随其后的一个字节为第一个常量表的 tag 位 0A
-> 10
,经过常量表类型查询可知 10 为 CONSTANT_Methodref_info
,表内数据项为u1: tag
u2: class_info
u2: name_and_type_index
,结合Class文件分析,这表示从第一个常量CONSTANT_Methodref_info
占用 5 个字节,其中第一个字节0A
为标志位,其后两个字节00 04
-> 4
以后两个字节为 class_info,紧随 2 个字节00 15
-> 21
为 name_and_type_index。咱们经过查询汇编代码常量池中的一个常量表为#1 = Methodref #4.#21
得出一个常量表正是方法引用,其数据项索引也是#4
和#21
。剩下的 24 种常量分析也是如此。也是由于这 14 中常量类型各自均有本身的结构,因此说常量池是最繁琐的数据。
小知识:
因为Class文件中方法、字段等都须要引用CONSTANT_Utf8_info型常量来描述名称,因此CONSTANT_Utf8_info型常量的最大长度也就是Java中方法、字段名的最大长度。而这里的最大长度就是length的最大值,既u2类型能表达的最大值65535。因此Java程序中若是定义了超过64KB英文字符的变量或方法名,将会没法编译。
在常量池结束以后,紧接着两个字节表明访问标志(access_flag)这个标志用于识别一些类或接口层次的访问信息。具体标志位以及标志的含义见下表:
invokeSpecial 指令语义在 JDK1.0.2发生过改变,为了区别这条指令使用哪一种语意,在 JDK1.0.2以后编译出来的类的这个标志都必须为真。
分析[Class]文件咱们得出 access_flag 为 00 21
,可是查询上表确没有查询到对应的标志,这是由于 ByteCode
是一个普通的 Java 类,不是接口、枚举或者注解,被public关键字修饰但没有被声明为final和abstract,而且它使用了JDK 1.2以后的编译器进行编译,所以它的ACC_PUBLIC、ACC_SUPER标志应当为真,而其他 6 个标志应当为假,所以它的access_flags的值应为:0x0001|0x0020=0x0021
。而咱们经过 ByteCode
汇编代码查看获得 flags: ACC_PUBLIC, ACC_SUPER
也证实了的确为上述所言。
类索引(this_class)、父类索引(super_class)和 接口数量(interface_count)是一个 u2类型的数据,而接口索引集合 interfaces[] 是一组 u2 类型的数据的集合。这四项数据直接肯定了这个类的继承关系。Java 不容许多继承可是容许实现多个接口,这就为何super_class是一个而 interfaces 是一个集合。咱们经过分析[Class]文件能够看出 this_class 对应00 03 -> 3
从常量池中查询 #3 对应的常量
#3 = Class #23 // tech/techstack/blog/ByteCode #23 = Utf8 tech/techstack/blog/ByteCode
能够看出 #3 对应的就是当前类 tech/techstack/blog/ByteCode
。后面一样为占两个字节的 super_class 对应的``00 04 -> 4`从常量池中查询出来对应的常量为
#4 = Class #24 // java/lang/Object #24 = Utf8 java/lang/Object
因此 super_class 表示的为:java/lang/Object
。随后即是 interface_count 对应的 00 00 -> 0
说明 ByteCode
没有实现接口,所以就不存在后面的 interfaces[]。
字段表(field_info)用于描述接口或者类中声明的变量。字段(field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。fields_count 类中 field_info 的数量。fields[] 则是 field_info 的集合。field_info 的结构以下图所示:
字段修饰符 access_flag 和类中的 access_flag十分类似:
在实际状况中,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志最多只能选择其一,ACC_FINAL、ACC_VOLATILE不能同时选择。接口之中的字段必须有ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志。
继续分析Class文件,00 01 00 02 00 05 00 06 00 00
。其中 00 01 -> 1
表示 field_count,很显然 ByteCode
类中的字段只有一个 private String username;
。 参照上表继续取两个字节00 02 -> 2
表示access_flag,查询可知修饰符号为ACC_PRIVATE
,继续取两个字节00 05 -> 5
表示 name_index,从汇编代码中查询常量池#5为
#5 = Utf8 username
继续取两个字节00 006 -> 6
表示descriptor_index
,指向的是常量池 #6 的常量
#6 = Utf8 Ljava/lang/String;
后续的 00 00 -> 0
表示attribute_count
的个数,此处为 0。
名词释义:
全限定名和简单名称
把类名中的.
替换成/
,连续多个全限定名时,为了避免产生混淆,在使用时最后通常都会加入一个;
表示全限定名结束。方法、字段索引描述
方法的参数列表(包括数量、类型以及顺序)和返回值。根据描述符规则,基本数据类型(byte、char、double、float、int、long、short、boolean)以及表明无返回值的void类型都用一个大写字符来表示,而对象类型则用字符L加对象的全限定名来表示。
基本数据类型
B---->byte
C---->char
D---->double
F----->float
I------>int
J------>long
S------>short
Z------>boolean
V------->void对象类型
String------>Ljava/lang/String;
数组类型:每个惟独都是用一个前置 [ 来表示
int[] ------>[ I,
String [][]------>[[Ljava.lang.String;
用描述符来描述方法的,先参数列表,后返回值的格式,参数列表按照严格的顺序放在()中
好比源码 String getUserInfoByIdAndName(int id,String name) 的方法描述符(I,Ljava/lang/String;)Ljava/lang/String;
Class文件储存格式中对方法的描述与对字段的描述几乎采用了彻底一致的方式。方法表的结构以下图所示:
由于volatile关键字和transient关键字不能修饰方法,因此方法表的访问标志中没有了ACC_VOLATILE标志和ACC_TRANSIENT标志。与之相对的,synchronized、native、strictfp和abstract关键字能够修饰方法,因此方法表的访问标志中增长了ACC_SYNCHRONIZED、ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT标志:
一样根据Class文件进行分析。00 03
表示 method_count 说明ByteCode
类的方法有三个,根据Method_info继续取出第一个方法的 8 个字节00 01 00 07 00 08 00 01
,00 01 -> 0
表示的是方法的修饰符 表示的是access_flag 为 acc_public,00 07 -> 7
表示的是方法的名称(name_index) 指向常量池中#7常量
#7 = Utf8 <init>
表示方法为<init>
的构造方法。00 08 ->8
表明方法的描述符号(descriptor_index),指向常量池 #8 常量
#8 = Utf8 ()V
表示的是无参无返回值。00 01 -> 1
表示有一个方法属性的个数为 1。
根据 attribute_info 结构继续从Class文件中取出00 09 00 00 00 2F
。00 09 -> 9
表示方法属性名称(attribute_name_index)指向常量池 #9 常量
#9 = Utf8 Code
00 00 00 2F ->
表示Code
属性的长度为 47 个字节。(特别特别须要注意这47个字节从Code属性表中第三个开始也就是max_stack开始,由于此 attribute_info为 Code_attribute 自己,attribute_name_index 和 attribute_length 为 Code 的属性)。
Code_attribute属性表结构以下:
Code_attribute { u2 attribute_name_index; // 属性名索引,常量值固定为"Code" u4 attribute_length; //属性值长度,值为整个表的长度减去6个字节(attribute_name_index + attribute_length) u2 max_stack; //操做数栈深度最大值 u2 max_locals; //局部变量表所需的存储空间,单位为"Slot",Slot是虚拟机为局部变量分配内存所使用的最小的单位。 u4 code_length; // 存储Java源程序编译后生成的字节码指令,每一个指令为u1类型的单字节。虚拟机规范中明确限制了一个方法不容许超过65535条字节指令,实际上只用了u2长度。 u1 code[code_length]; // 方法指向的具体指令码 u2 exception_table_length; // 异常表的个数 { u2 start_pc; // start_pc 和 end_pc 表示在 Code 数组中的[start_pc, end_pc)处指令所抛出的异常由这个表处理。 u2 end_pc; u2 handler_pc; // 异常代码的开始处 u2 catch_type; // 表示被处理流程的异常类型,指向常量池中具体的某一个异常类,catchType为 0 处理全部的异常 } exception_table[exception_table_length]; // 异常表结构,用于存放异常信息 u2 attributes_count; // 属性的个数 attribute_info attributes[attributes_count]; // 属性的集合 }
第一个 Code 的汇编代码以下:
Code: stack=1, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return LineNumberTable: line 7: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Ltech/techstack/blog/ByteCode;
Tips: args_size=1是由于在任何实例方法里面,均可以经过"this"关键字访问到此方法所属的对象。这个访问机制对Java程序的编写很重要,而它的实现却很是简单,仅仅是经过Javac编译器编译的时候把对this关键字的访问转变为对一个普通方法参数的访问,而后在虚拟机调用实例方法时自动传入此参数而已。所以在实例方法的局部变量表中至少会存在一个指向当前对象实例的局部变量,局部变量表中也会预留出第一个Slot位来存放对象实例的引用,方法参数值从1开始计算。
回到示例代码,取出 47 位 Code 值:
// _ 是本文自行添加方便表示数据项之间的间隔,Class 文件中是不存在的 00 01 _00 01 _00 00 00 05 _2A B7 00 01 B1 _00 00 _00 02 _00 0A _00 00 00 06 _00 01 _00 00 _00 06 _00 0B _00 00 00 0C _00 01 00 00 00 05 00 0C 00 0D 00 00
00 01 -> 1
表示 操做数栈(max_stack)的最大深度为 1。后面的00 01 -> 1
表示局部变量表的长度(max_locals)为 1,正好与 Code 的汇编代码stack=1
locals=1
对应。紧接着后面 4 位00 00 00 05 -> 5
表示字节码指令长度(code_length)为 5。继续日后数 5 位2A B7 00 01 B1
表示 JVM具体的字节码指令。
0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: return
00 00
表示异常表个数(exception_table_length)为 0,方法没有抛出异常。
00 02 -> 2
表示 Code_attribute 结构中属性表的个数为 2 个。00 0A -> 10
表示 attribute_name_index 指向常量池 #10 LineNumberTable
常量。继续后面 4 位00 00 00 06 -> 10
表示 attribute_length 即 LineNumberTable 的长度。LineNumberTable 是用来描述Java源码行号与字节码行号(字节码偏移量)之间的对应关系,好比咱们平时 debug 某一行代码。其结构以下所示:
LineNumberTable_attribute { u2 attribute_name_index; u4 attribute_length; u2 line_number_table_length; { u2 start_pc; u2 line_number; } line_number_table[line_number_table_length]; }
00 01 -> 1
表示行号表的个数为 1,即只存在一个行号表。00 00
表示start_pc为字节码行号,00 06 -> 6
表示源码行号为第 7(6+1) 行。
00 0B -> 11
表示第二个属性表对应常量池 #11 LocalVariableTable
常量。00 00 00 0C -> 12
表示 LocalVariableTable
常量的长度为 12。LocalVariableTable 属性用于描述栈帧中局部变量表中的变量与Java源码中定义的变量之间的关系。其结构以下:
LocalVariableTable_attribute { u2 attribute_name_index; u4 attribute_length; u2 local_variable_table_length; { u2 start_pc; u2 length; u2 name_index; u2 descriptor_index; u2 index; } local_variable_table[local_variable_table_length]; }
LocalVariableTable也不是运行时必需的属性,但默认会生成到Class文件之中,能够在Javac中分别使用-g:none
或-g:vars
选项来取消或要求生成这项信息。若是没有生成这项属性,最大的影响就是当其余人引用这个方法时,全部的参数名称都将会丢失,IDE将会使用诸如arg0、arg1之类的占位符代替原有的参数名,这对程序运行没有影响,可是会对代码编写带来较大不便,并且在调试期间没法根据参数名称从上下文中得到参数值。
00 01 -> 1
表示本地变量表的个数 local_variable_table_length 为 1。00 00
表示local_variable_table 的 start_pc 为 0,其含义为这个局部变量的生命周期开始的字节码偏移量。00 05 -> 5
表示 local_variable_table 的 length 为 5,其含义为这个局部变量做用范围覆盖的长度。二者结合起来就是这个局部变量在字节码之中的做用域范围。00 0C
00 0D
分别表示 name_index 和 descriptor_index,分别指向常量池中 #12 this
和 #13 Ltech/techstack/blog/ByteCode;
常量。分别表明了局部变量的名称以及这个局部变量的描述符。00 00
表示了这个变量在本地变量表中的index 即这个局部变量在栈帧局部变量表中Slot的位置。当这个变量数据类型是64位类型时(double和long),它占用的Slot为index和index+1两个。
属性表(attribute_info)用于描述某些场景专有的信息。在Class文件、字段表、方法表均可以携带本身的属性表集合。全部的属性都具备一下常规格式:
attribute_info { u2 attribute_name_index; u4 attribute_length; u1 info [attribute_length]; }
根据The Java® Virtual Machine Specification已经增长到了 23 项。根据其用途能够分为三组:
五个属性对于class
Java虚拟机正确解释文件相当重要 :
十二个属性对于Java SE平台的类库正确解释class
文件相当重要 :
六个属性对于classJava虚拟机或Java SE平台的类库对文件的正确解释不是相当重要的 ,但对于工具来讲很是有用:
参考:
[1] 周志明.深刻理解Java虚拟机:JVM高级特性与最佳实践.北京:机械工业出版社,2013.
[2] Chapter 4. Th class File Format
[3] Chapter 6. The Java Virtual Machine Instruction Set
文章首发于陈建源的博客,欢迎访问。
文章做者:陈建源
文章连接:https://www.techstack.tech/post/zi-jie-ma-wen-jian-jie-gou-xiang-jie/