JVM中 Class 文件分析
每个 Class 文件都是由 8 字节为单位的字节流组成,所有的 16 位、32 位和 64 位长度的数 据将被构造成 2 个、4 个和 8 个 8 字节单位来表示。多字节数据项总是按照 Big-Endian1的顺 序进行存储。在Java SDK中,访问这种格式的数据可以使用java.io.DataInput、 java.io.DataOutput 等接口和 java.io.DataInputStream 和 java.io.DataOutputStream 等类来实现。
Class 文件的内容可用一组私有数据类型来表示,它们包括 u1,u2 和 u4,分别代 表了1、2和4个字节的无符号数。在 Java SDK 中这些类型的数据可以通过实现接口 java.io.DataInput 中的 readUnsignedByte、readUnsignedShort 和 readInt 方法进 行读取。
ClassFile 结构 每一个 Class 文件对应于一个如下所示的 ClassFile 结构体,其包含的属性如下表:
看到上表,很多人就会懵逼,这些都是啥啊,后面我会根据一个示例来讲解这些特性。
下面我们来看一个简单的 java 类,就是一个输出了一个 hello world。
package com.hello.test; public class Log { public static void main(String[] args) { System.out.println("hello world!"); } }
输入命令 javac Log.java 将其编译成 class 文件后,打开:
cafe babe 0000 0034 001d 0a00 0600 0f09 0010 0011 0800 120a 0013 0014 0700 1507 0016 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 0a53 6f75 7263 6546 696c 6501 0008 4c6f 672e 6a61 7661 0c00 0700 0807 0017 0c00 1800 1901 000c 6865 6c6c 6f20 776f 726c 6421 0700 1a0c 001b 001c 0100 1263 6f6d 2f68 656c 6c6f 2f74 6573 742f 4c6f 6701 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 0600 0000 0000 0200 0100 0700 0800 0100 0900 0000 1d00 0100 0100 0000 052a b700 01b1 0000 0001 000a 0000 0006 0001 0000 0003 0009 000b 000c 0001 0009 0000 0025 0002 0001 0000 0009 b200 0212 03b6 0004 b100 0000 0100 0a00 0000 0a00 0200 0000 0500 0800 0600 0100 0d00 0000 0200 0e
下面将根据 class 文件来分析每一个字节所代表的含义。
1. magic 魔数
Class 文件的第 1 - 4 个字节代表了该文件的魔数。
魔数的唯一作用是确定这个文件是否为一个能被虚拟机所接受的 Class 文件。魔数值固定为 0xCAFEBABE,不会改变。
2. minor_version、major_version
Class 文件的第 5 - 6 个字节代表了 Class 文件的副版本号。
Class 文件的第 7 - 8 个字节代表了 Class 文件的主版本号。
副版本号和主版本号,minor_version 和 major_version 的值分别表示 Class 文件 的副、主版本。它们共同构成了 Class 文件的格式版本号。譬如某个 Class 文件的主版本号为 M,副版本号为 m,那么这个 Class 文件的格式版本号就确定为 M.m。Class 文件格式版本号大小的顺序为:1.5 < 2.0 < 2.1。
一个 Java 虚拟机实例只能支持特定范围内的主版本号(Mi 至 Mj)和 0 至特定范围 内(0 至 m)的副版本号。假设一个 Class 文件的格式版本号为 V,仅当 Mi.0 ≤ v ≤ Mj.m 成立时,这个 Class 文件才可以被此 Java 虚拟机支持。不同版本的 Java 虚拟机实现 支持的版本号也不同,高版本号的 Java 虚拟机实现可以支持低版本号的 Class 文件。
下表列出了各个版本 JDK 的十六进制版本号信息:
上述 class 文件 0000 0034 对应的就是表格中的 JDK1.8。
3. 常量池集合
3.1 constant_pool_count 常量池计数器
紧跟版本信息之后的是常量池信息,其中前 2 个字节表示常量池计数器,其后的不定长数据则表示常量池的具体信息。
constant_pool_count 的值等于 constant_pool 表中的成员数加 1。 constant_pool 表的索引值只有在大于 0 且小于 constant_pool_count 时才会被认为是有效的,对于 long 和 double 类型有例外情况。在 Class 文件的常量池中,所有的 8 字节的常量都占两个表成员(项)的空间。如果一个 CONSTANT_Long_info 或 CONSTANT_Double_info 结构的项在常量池中的索引为 n,则常量池中下一个有效的项的索引为 n+2,此时常量池中索引为 n+1 的项有效但必须被认为不可用。
class 文件字节码对应的内容是:001d
,其值为 29,表示一共有 29 - 1 = 28 个常量。
3.2 constant_pool[]
紧跟着常量池计数器后面就是 28 个常量池了,因为每个常量都对应不同的类型,需要一个个具体分析。
常量池,constant_pool 是一种表结构,它包含 Class 文件结构及其子结构 中引用的所有字符串常量、类或接口名、字段名和其它常量。常量池中的每一项都具备相 同的格式特征——第一个字节作为类型标记用于识别该项是哪种类型的常量,称为 "tag byte"。常量池的索引范围是 1 至 constant_pool_count − 1。
所有的常量池项都具有如下通用格式:
cp_info {
u1 tag;
u1 info[];
}
常量池中,每个 cp_info 项的格式必须相同,它们都以一个表示 cp_info 类型的单字节 “tag”项开头。后面 info[]项的内容 tag 由的类型所决定。tag 有效的类型和对应的取值在表 4.3 列出。每个 tag 项必须跟随 2 个或更多的字节,这些字节用于给定这个常量的信息,附加字节的信息格式由 tag 的值决定。
在 Java 虚拟机规范中一共有 14 种 cp_info
类型的表结构。
而上面这些 cp_info
表结构又有不同的数据结构,其对应的数据结构如下图所示。
接下来我们开始分析上述 Log.class 文件每个字节的含义,前面第一句话已经说了,紧跟着常量池计数器后面的就是常量池了。下面开始分析:
第 1 个常量
紧接着 001d 的后一个字节为 0A,为十进制数字 10,查表可知其为方法引用类型(CONSTANT_Methodref_info)的常量。在 cp_info 中结构如下所示:
查找的方式是先确定 tag 值,根据 tag 值判断当前属于哪一个常量。这里 tag 为 10,查表即可知是上图。
然后看其结构显示还有两个 U2 的index,说明后面 4 个字节都是属于第一个常量,其中第 2 - 3 个字节表示类信息,第 4 - 5 个字节表示名称及类描述符。
接下来我们取出这部分的数据:0a00 0600 0f :
该常量项第 2 - 3 个字节,其值为 00 06,表示指向常量池第 6 个常量所表示的信息。根据后面我们分析的结果知道第 6 个常量是 java/lang/Object
。第 4 - 5 个字节,其值为 000f,表示指向常量池第 15 个常量所表示的信息,根据 javap 反编译出来的信息可知第 15 个常量是 <init>:()V
。将这两者组合起来就是:java/lang/Object.<init>:V
,即 Object 的 init 初始化方法。
下面是输入
javap -v Log.class
反编译出来的结果:
javap -v Log.class Classfile /Users/xxx/Desktop/Log.class Last modified 2020-1-8; size 427 bytes MD5 checksum 745be5a6df4d9554e783dbbcecaf9b6d Compiled from "Log.java" public class com.hello.test.Log minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER Constant pool: #1 = Methodref #6.#15 // java/lang/Object."<init>":()V #2 = Fieldref #16.#17 // java/lang/System.out:Ljava/io/PrintStream; #3 = String #18 // hello world! #4 = Methodref #19.#20 // java/io/PrintStream.println:(Ljava/lang/String;)V #5 = Class #21 // com/hello/test/Log #6 = Class #22 // java/lang/Object #7 = Utf8 <init> #8 = Utf8 ()V #9 = Utf8 Code #10 = Utf8 LineNumberTable #11 = Utf8 main #12 = Utf8 ([Ljava/lang/String;)V #13 = Utf8 SourceFile #14 = Utf8 Log.java #15 = NameAndType #7:#8 // "<init>":()V #16 = Class #23 // java/lang/System #17 = NameAndType #24:#25 // out:Ljava/io/PrintStream; #18 = Utf8 hello world! #19 = Class #26 // java/io/PrintStream #20 = NameAndType #27:#28 // println:(Ljava/lang/String;)V #21 = Utf8 com/hello/test/Log #22 = Utf8 java/lang/Object #23 = Utf8 java/lang/System #24 = Utf8 out #25 = Utf8 Ljava/io/PrintStream; #26 = Utf8 java/io/PrintStream #27 = Utf8 println #28 = Utf8 (Ljava/lang/String;)V { public com.hello.test.Log(); 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 hello world! 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 5: 0 line 6: 8 } SourceFile: "Log.java"
其实从上面的结果也可以看出来,第一个常量对应的是第6,15个常量,组合起来的含义后面注释也写着了。
其他很多常量都是类似的,接下来我们看看字符串是怎么来得。
第 21 个常量
第 21 个常量,数据为
0100 1263 6f6d 2f68 656c 6c6f 2f74 6573 742f 4c6f 67
这里 tag 值是 01,对应的结构如下:
length 是 u2,对应着 0012,说明后面跟着 18 个字节:63 6f6d 2f68 656c 6c6f 2f74 6573 742f 4c6f 67;查 ASCII 表可得 63-c, 6f-o, 6d-m, 2f-/ ··· 4c-L,6f-o, 67-g,
组合起来就是:com/hello/test/Log 。
相信通过上面两个例子,大家就知道如何去分析常量池里面的索引了。但很多时候我们可以借助 JDK 提供的 javap 命令直接查看 Class 文件的常量池信息,但是手动分析能够让你更加了解结果为啥是这样的。其实 javap 出来的就是人家分析总结好的。
4. access_flags 访问标志
在常量池结束之后,紧接着的两个字节代表类或接口的访问标记(access_flags)。这里的数据为 00 21。
access_flags 是一种掩码标志,用于表示某个类或者接口的访问权限及基础属性。access_flags 的取值范围和相应含义见下表:
第一列是标记名;
第二列是对应的值;
第三列是对应的说明。
-
带有 ACC_SYNTHETIC 标志的类,意味着它是由编译器自己产生的而不是由程序员 编写的源代码生成的。
-
带有 ACC_ENUM 标志的类,意味着它或它的父类被声明为枚举类型。
-
带有 ACC_INTERFACE 标志的类,意味着它是接口而不是类,反之是类而不是接口。
如果一个 Class 文件被设置了 ACC_INTERFACE 标志,那么同时也得设置 ACC_ABSTRACT 标志。同时它不能再设置ACC_FINAL、 ACC_SUPER 和 ACC_ENUM 标志。
-
注解类型必定带有 ACC_ANNOTATION 标记,如果设置了 ANNOTATION 标记, ACC_INTERFACE 也必须被同时设置。如果没有同时设置 ACC_INTERFACE 标记, 那么这个 Class 文件可以具有表 4.1 中的除 ACC_ANNOTATION 外的所有其它标记。 当然 ACC_FINAL 和 ACC_ABSTRACT 这类互斥的标记除外。
-
ACC_SUPER 标志用于确定该 Class 文件里面的 invokespecial 指令使用的是哪 一种执行语义。目前 Java 虚拟机的编译器都应当设置这个标志。ACC_SUPER 标记 是为了向后兼容旧编译器编译的 Class 文件而存在的,在 JDK1.0.2 版本以前的编 译器产生的 Class 文件中,access_flag 里面没有 ACC_SUPER 标志。同时, JDK1.0.2 前的 Java 虚拟机遇到 ACC_SUPER 标记会自动忽略它。
- 在表中没有使用的 access_flags 标志位是为未来扩充而预留的,这些预留的标志为在编译器中会被设置为 0, Java 虚拟机实现也会自动忽略它们。
5. 类索引、父类索引、接口索引
在访问标记后,则是类索引、父类索引、接口索引的数据,这里数据为:00 05 、00 06 、00 00。
类索引和父类索引都是一个 u2 类型的数据,而接口索引集合是一组 u2 类型的数据的集合,这个可以由前面 Class 文件的构成可以得到。Class 文件中由这三项数据来确定这个类的继承关系。
5.1 this_class 类索引
类索引,this_class 的值必须是对 constant_pool 表中项目的一个有效索引值。 constant_pool 表在这个索引处的项必须为 CONSTANT_Class_info 类型常量,表示这个 Class 文件所定义的类或接口。这里的类索引是 00 05 表示其指向了常量池中第 5 个常量,通过我们之前的分析,我们知道第 5 个常量其最终的信息是 Log 类。
5.2 super_class 父类索引
对于类来说,super_class 的值必须为 0 或者是对 constant_pool 表中 项目的一个有效索引值。如果它的值不为 0,那 constant_pool 表在这个索引处的项 必须为 CONSTANT_Class_info 类型常量,表示这个 Class 文件所定义的 类的直接父类。当前类的直接父类,以及它所有间接父类的 access_flag 中都不能有 ACC_FINAL 标记。对于接口来说,它的 Class 文件的 super_class 项的值必须是 对 constant_pool 表中项目的一个有效索引值。constant_pool 表在这个索引处的 项必须为代表 java.lang.Object 的 CONSTANT_Class_info 类型常量。 如果 Class 文件的 super_class 的值为 0,那这个 Class 文件只可能是定义的是 java.lang.Object 类,只有它是唯一没有父类的类。这里的父类索引是 00 06 表示其指向了常量池中第 6 个常量,通过我们之前的分析,我们知道第 6 个常量其最终的信息是 Object 类。因为其并没有继承任何类,所以 Demo 类的父类就是默认的 Object 类。interfaces_count 接口计数器,
interfaces_count 的值表示当前类或接口的直接父接口数量。
5.3 interfaces[] 接口表
interfaces[] 数组中的每个成员的值必须是一个对 constant_pool 表中项 目的一个有效索引值,它的长度为 interfaces_count。每个成员 interfaces[i] 必 须为CONSTANT_Class_info类型常量,其中0 ≤ i < interfaces_count。在 interfaces[]数组中,成员所表示的接口顺序和对应的源 代码中给定的接口顺序(从左至右)一样,即 interfaces[0]对应的是源代码中最左 边的接口。
这里 Log 类的字节码文件中,因为并没有实现任何接口,所以紧跟着父类索引后的两个字节是0x0000,这表示该类没有实现任何接口。因此后面的接口索引表为空。
6. 字段表集合
字段表集合用于描述接口或者类中声明的变量,这里的数据为:00 00。
6.1 fields_count 字段计数器
fields_count 的值表示当前 Class 文件 fields[]数组的成员个数。 fields[]数组中每一项都是一个 field_info 结构的数据项,它用于表示该类或接口声明的类字段或者实例字段。
6.2 fields[] 字段表
fields[]数组中的每个成员都必须是一个 fields_info 结构的数 据项,用于表示当前类或接口中某个字段的完整描述。fields[]数组描述当前类或接口 声明的所有字段,但不包括从父类或父接口继承的部分。
field_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
7. 属性集合
7.1 attributes_count 属性计数器
attributes_count 的值表示当前 Class 文件 attributes 表的成员个 数。attributes 表中每一项都是一个 attribute_info 结构的数据项。
7.2 attributes[]属性表
属性表,attributes 表的每个项的值必须是 attribute_info 结构(§4.7)。
属性(Attributes)在 Class 文件格式中的 ClassFile 结构、field_info 结构,method_info 结构和 Code_attribute 结构都有使用,所有属性的通用格式如下:
对于任意属性,attribute_name_index 必须是对当前 Class 文件的常量池的有效 16 位 无符号索引。常量池在该索引处的项必须是 CONSTANT_Utf8_info 结构,表示当前属性的名字。attribute_length 项的值给出了跟随其后的字节的长度,这个长度不包括 attribute_name_index 和 attribute_name_index 项的 6 个字节。
有些属性因 Class 文件格式规范所需,已被预先定义好。这些属性在表 4.6 中列出,同时,被列出的信息还包括它们首次出现的Class文件版本和 Java SE 版本号。在本规范定义的环境 中,也就是已包含这些预定义属性的 Class 文件中,它们的属性名称被保留,不能再被属性表中其他的自定义属性所使用。 下表是 class 文件的属性:
8. 方法表集合
在字段表后的 2 个字节是一个方法计数器,表示类中总有有几个方法,在字段计数器后,才是具体的方法数据。这里数据为:00 02 。
8.1 methods_count 方法计数器
8.2 methods[] 方法表
methods[] 数组中的每个成员都必须是一个 method_info 结构的 数据项,用于表示当前类或接口中某个方法的完整描述。如果某个 method_info 结构 的 access_flags 项既没有设置 ACC_NATIVE 标志也没有设置 ACC_ABSTRACT 标志, 那么它所对应的方法体就应当可以被 Java 虚拟机直接从当前类加载,而不需要引用其它 类。method_info 结构可以表示类和接口中定义的所有方法,包括实例方法、类方法、 实例初始化方法方法和类或接口初始化方法方法。methods[]数组 只描述当前类或接口中声明的方法,不包括从父类或父接口继承的方法。method_info {
u2 access_flags;
u2 name_index;
u2 descriptor_index;
u2 attributes_count;
attribute_info attributes[attributes_count];
}
Log 类的字节码文件中,方法计数器的值为 00 02,表示一共有 2 个方法。
第一个方法:
00 0100 0700 0800 0100 0900 0000 1d00 0100 0100 0000 052a b700 01b1 0000 0001 000a 0000 0006 0001 0000 0003
根据前面的结构格式可以知道,
方法计数器后 2 个字节表示方法访问标识,这里是 00 01,表示其实 ACC_PUBLIC 标识,对比上面的图表可知其表示 public 访问标识。
紧接着 2 个字节表示方法名称的索引,这里是 00 07 表示指向了常量池第 7 个常量,查阅可知其指向了<init>
。
紧接着的 2 个字节表示方法描述符索引项,这里是 00 08 表示指向了常量池第 8 个常量,查阅可知其指向了()V
。
紧接着 2 个字节表示属性表计数器,这里是 00 01 表示该方法的属性表一共有 1 个属性。属性表的表结构如下:
attribute_info {
u2 attribute_name_index;
u4 attribute_length;
u1 info[attribute_length];
}
前两个字节是名字索引、接着 4 个字节是属性长度、接着是属性的值。这里前两个字节为 0009,指向了常量池第 9 个常量,查询可知其值为 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 个字节为 0009,即常量池第 9 个常量,查询知道是字符串常量 Code
。
接着 4 个字节表示属性长度,这里值为 0000 001d,即 29 的长度。下面我们继续分析 Code 属性的数据内容。
紧接着 2 个字节为 max_stack 属性。这里数据为 00 01,表示操作数栈深度的最大值。
紧接着 2 个字节为 max_locals 属性。这里是数据为 00 01,表示局部变量表所需的存储空间为 1 个 Slot。在这里 max_locals 的单位是 Slot,Slot 是虚拟机为局部变量分配内存所使用的最小单位。
接着 4 个字节为 code_length,表示生成字节码这里给的长度。这里数据为 00 00 00 05,表示生成字节码长度为 5 个字节。那么紧接着 5 个自己就是对应的数据,这里数据为 2a b7 00 01 b1,这一串数据其实就是字节码指令。通过查询字节码指令表,可知其对应的字节码指令:
-
读入 2A,查表得 0x2A 对应的指令为 aload_0,这个指令的含义是将第 0 个 Slot 中为 reference 类型的本地变量推送到操作数栈顶。
-
读入 B7,查表得0xB7对应的指令为 invokespecial,这条指令的作用是以栈顶的 reference 类型的数据所指向的对象作为方法接收者,调用此对象的实例构造器方法、private 方法或者它的父类的方法。这个方法有一个 u2 类型的参数说明具体调用哪一个方法,它指向常量池中的一个 CONSTANT_Methodref_info 类型常量,即此方法的方法符号引用。
-
读入 00 01,这是 invokespecial 的参数,查常量池得 0x0001 对应的常量为实例构造器“”方法的符号引用。
-
读入 B1,查表得0xB1对应的指令为 return,含义是返回此方法,并且返回值为void。这条指令执行后,当前方法结束。
接着 2 个字节为异常表长度,这里数据为 00 00,表示没有异常表数据。那么接下来也就不会有异常表的值。
紧接着 2 个字节是属性表的长度,这里数据为 00 01,表示有一个属性。该属性长度为一个 attribute_info 那么长。
首先,前两个字节表示属性名称索引,这里数据为:00 0A。指向了第 10 个常量,查阅可知值为:LineNumberTable。LineNumberTable 属性是可选变长属性,位于 Code(§4.7.3)结构的属性表。它被调试 器用于确定源文件中行号表示的内容在 Java 虚拟机的 code[]数组中对应的部分。在 Code 属性 的属性表中,LineNumberTable 属性可以按照任意顺序出现,此外,多个 LineNumberTable 属性可以共同表示一个行号在源文件中表示的内容,即 LineNumberTable 属性不需要与源文件 的行一一对应。
LineNumberTable 属性格式如下:
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 0A。
接着 4 个字节是属性长度,这里数据为 00 00 00 06,表示有 6 个字节的数据。接着 2 个字节是 LineNumberTable 的长度,这里数据是 00 01,表示长度为 1。接着跟着 1 个 line_number_info 类型的数据,下面是 line_number_info 表的结构,其包含了 start_pc 和 line_number 两个 u2 类型的数据项。前者是字节码行号,后者是 Java 源码行号。
那么接下来 2 个字节为 00 00,即 start_pc 表示的字节码行号为第 0 行。接着 00 03,即 line_number 表示 Java 源码行号为第 3 行。
从上面的反编译来看,上面的分析也是对的:
{ public com.hello.test.Log(); 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 hello world! 5: invokevirtual #4 // Method java/io/PrintStream.println:(Ljava/lang/String;)V 8: return LineNumberTable: line 5: 0 line 6: 8 }
第二个方法这里就不详细分析了,大家可以自己对着上面的反编译结果进行分析。
第二个方法开头是 0009,表示的是 ACC_PUBLIC 与 ACC_STATIC 合在一起的结果。
总结
到这里我们通过对 Log 类的解析,从而对 Java 类文件结构有了一个全面的认识。进一步还简单了解了 Java 虚拟机以及 Java 虚拟机规范。希望读完这篇文章,大家能对 Java 类文件结构有一个深入的认识。 最后用一张图来总结一下: