读《深入理解JVM》笔记之class文件结构

Class文件是一组以8位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在Class文件之中,中间没有添加任何分隔符,这使得整个Class文件中存储的内容几乎全部是程序运行的必要数据,没有空隙存在。

数据结构

class文件格式采用一种类似于C语言结构体的伪结构来存储数据,这种伪结构中只有两种数据类型,

  • 无符号数。基本数据类型,以u1、u2、u4、u8分别代表1个字节、2个字节、4个字节、8个字节的无符号数。无符号数可以用来描述数字、索引引用、数值、按照UTF-8编码构成字符串值
  • 表。复合结构类型。表可以由多个无符号数或者其他表构成。整个class文件本质上就是一张表。

NOTE:无论是无符号数还是表,当需要描述同一类型但数量不定的多个数据时,经常会使用一个前置的容量计数器加若干个连续的数据项的形式。

class文件组成
读《深入理解JVM》笔记之class文件结构_第1张图片
Figure1 class文件格式
  • 魔数
  • 次版本号
  • 主版本号
  • 常量池
    常量池中存放的内容主要包括两种类型,字面量和符号引用。
    其中字面量包括字符串、final常量等;符号引用包括类和接口的全限定名、字段名称和描述符、方法名和描述符。

常量池中的每一项常量都是一个表。共有14种类型的表。


读《深入理解JVM》笔记之class文件结构_第2张图片
Figure2 常量池的项目类型

可以看到常量池中涉及的项目类型分成了三大类型,字面量用的类型、符号引用用的类型、后三种CONSTANT_MethodHandle_info、CONSTANT_MethodType_info、CONSTANT_InvokeDynamic_info是为了更好的支持动态语言特性在JDK1.7添加的,其余11种是在JDK1.7之前就有。

查看Figure 3,可以得到一个共同点:这些表的结构的第一个字节存放类型标记tag,作用显而易见,用来区分这些常量类型。
读《深入理解JVM》笔记之class文件结构_第3张图片
Figure3 14种常量池使用的表结构
  • access_flags(访问标志)

    注意该访问标志是针对类或接口而言,与字段、方法无关。从图中可以看出这些是经常用来修饰类或者接口的修饰符。access_flags数据类型为u2,所以access_flags一共有16个标记位可供使用(标记则记为1,否则为0)。当前只定义了8个,没用到的置为0。
    读《深入理解JVM》笔记之class文件结构_第4张图片
    Figure4 类或接口访问标志
  • 类索引、父类索引、接口索引集合
    从class文件格式图可以看出,类索引、父类索引、接口索引类型都是u2。类索引确定该类或者接口的全限定名,父类索引确定该类的父类的全限定名,因为Java语言不允许多重继承,所有父类索引值为1,且Java中除了Object类没有父类外,其他类都有父类,所以除Object类外,所有的Java类的父类索引不为0。

  • 字段集合

    字段包括类级变量(即static修饰的静态变量)以及实例级变量,不包括方法中声明的局部变量。
    Figure5 字段表结构

    看一下字段表的构成。access_flags是一个u2类型的项目,它用来存放字段的修饰符,这些修饰符包括以下,
    读《深入理解JVM》笔记之class文件结构_第5张图片
    Figure6 字段方法标记

之后是name_index、descriptor_index两个索引值,name_index代表着字段的简单名称,descriptor_index代表着字段的描述符。简单名称很好理解,在使用Log的时候经常需要一个TAG,我们经常见如下代码

private static final String TAG = Xxx.class.getSimpleName();

这样,常量TAG就被赋值了该类的简单名称,即该类的类名。描述符是用来描述字段的数据类型,在方法中,描述符用来记录方法的参数列表(包括数量、类型和顺序)返回值。根据描述符规则,基本数据类型、Void无返回值类型都要一个大写字母表示,对象类型则是一个大写L接类的全限定名表示。如图,
读《深入理解JVM》笔记之class文件结构_第6张图片
Figure7 描述符标识字符

数组类型中每一个维度用" [ "表示。方法中描述符,按照先参数列表,后返回值的顺序描述。举例,

void test(){ ... }     相应的描述符为  ()V
int demo(int i, float j, boolean k){ ... }  相应的描述符为 (IFZ)I   
String toString(){ ... } 相应的描述符为 ()Ljava/lang/String   
void test(String[] args, long args1){ ... } 相应的描述符为 ([Ljava/lang/StringJ)V

NOTE: 字段表集合中不会列出从超类或者付接口中继承而来的字段,但有可能列出原本java代码中不存在的字段。譬如在内部类中为了保持对外部类的访问性,会自动添加指向外部类实例的字段。

  • 方法集合

    同字段表结构类似,方法表的结构包含了access_flags、name_index、descriptor_index、attributes_count、attributes五项属性,相应的数据类型也相同。
    Figure8 方法表结构
    读《深入理解JVM》笔记之class文件结构_第7张图片
    Figure9 方法访问标记

    这里你可能会问了,access_flags涉及方法的修饰符,name_index涉及了方法名称,descriptor_index中涉及了方法的参数、返回类型,那方法体哪里去了?方法体代码经过编译成为字节码指令,存放在方法属性表集合中的一个名称为Code的属性中,Code属性会在后面提到。

  • 属性表集合

    class文件、字段表、方法表都有属性表集合(零个或多个),先来看一下标准的属性表结构Figure10,共同项包括attribute_name_index(不同的属性表,该值不同)、attribute_length,最后一项为属性具体内容。
    Figure10 标准属性表结构
  1. Code属性表。前面提到过,Java源代码经过编译器编译后,字节码指令存在Code属性中。它是class文件中最重要的一个属性。Figure11是Code属性表的结构。其中,max_stack表示操作数栈的最大深度值。在执行方法时,操作数栈都不会超过这个深度。max_locals表示局部变量表所需的存储空间。单位为Slot,Slot是虚拟机为局部变量分配内存所使用的最小单位。对byte、char、short、int、float、boolean、returnAddress等长度不超过32位的数据类型,每个局部变量占用1个Slot,而double和long这两种64位的数据类型则需要两个Slot来存储。code_length和code用来存储Java源程序编译后生成的字节码指令。
    读《深入理解JVM》笔记之class文件结构_第8张图片
    Figure11 Code属性表结构
  2. LineNumberTable属性。LineNumberTable属性用于描述Java源码行号与字节码行号(字节码的偏移量)之间的对应关系。
    读《深入理解JVM》笔记之class文件结构_第9张图片
    Figure12 LineNumberTable属性表结构

    注意到line_numer_table的类型位line_number_info表,line_number_info表包括了start_pc和line_number两个u2类型的数据项,前者是字节码行号,后者是Java源码行号。

  3. SourceFile属性。SourceFile属性用于记录生成这个class文件的源码文件名称
    读《深入理解JVM》笔记之class文件结构_第10张图片
    Figure13 SourceFile属性表结构
  4. BootstrapMethod属性。BootstrapMethod属性与invokedynamic指令有关,虽然在TestClass源码中没有涉及到,但我这里想着记录一下。这个属性用于保存invokedynamic指令引用的引导方法限定符。invokedynamic指令的内容有待进一步深究,这里就不详细说了。
    读《深入理解JVM》笔记之class文件结构_第11张图片
    Figure14 BootstrapMethod属性表结构

当然,属性表中不止这几种属性,还有Exceptions属性、LocalVariableTable属性、ConstantValue属性、InnerClasses属性等,这些在后面有机会碰到再进行补充。

介绍了class文件的组成部分,下面我们来结合具体的一段测试代码的字节码,巩固一下上面所讲到内容。

/** 测试代码选自《深入理解JVM》一书第六章,因为在反推字节码过程中预防出现解
决不了的疑问,所以直接按照书中的测试代码进行练习,这样如果哪部分字节码有疑问,
也能有所参照 */

//TestClass.java
package org.fenixsoft.clazz; 
public class TestClass{ 
    private int m;
    public int inc(){ 
        return m + 1;
    } 
}

编译得到TestClass.class文件。在分析具体的字节码内容之前,先获取反编译文件。方便在分析字节码时核对校验。(命令 javap -verbose TestClass,可以看到,输出包括版本号、access_flags、常量池、方法)


读《深入理解JVM》笔记之class文件结构_第12张图片
Figure15 反编译class文件

使用WinHex打开TestClass.class文件,查看字节码文件具体内容

读《深入理解JVM》笔记之class文件结构_第13张图片
Figure16 class文件的具体内容

为了方便分析,将16进制的数字按照原来样式复制到Excel文件中,具体分析后的结果如下,
读《深入理解JVM》笔记之class文件结构_第14张图片
Figure17 最后的分析结果

(Excel文件我会在文末提供云盘下载)
下面对分析结果进行详细讲解。

  1. class文件最开头的是,u4类型的魔数。
  2. 接着,u2类型的次版本号。<00 00>
  3. 接着,u2类型的主版本号。<00 34>(说明一下,Java的版本号是从45开始,之后的每个JDK大版本发布,主版本号向上加1。这里我使用的是JDK1.8,十六进制的34转为十进制就是52,即对应的JDK1.8版本。)
  4. 接着,u2类型的constant_pool_count(常量池容量计数)。<00 13>(十进制为19,即0-18,共19个。但是真正的计数是从1开始,也就是说,实际的常量池数量是18个,这一点可以从反编译class文件中得到验证,常量池中确实有18个常量。至于第0项常量,我引用一下《深入理解JVM》书中原话,书中没有对这种特定情况进行进一步阐述,我这里也存疑)

设计者将第0项常量空出来是有特殊考虑的,这样做的目的在于满足后面某些指向常量池的索引值的数据在特定情况下需要表达“不引用任何一个常量池项目” 的含义,这种情况就可以把索引值置为0来表示。

  1. 接着就是常量池中的具体元素了。前面提到,常量池中的元素表的一个共同点就是第一个字节存放的是常量类型标记tag,tag取值有1、3-12、15、16、18共14个。这个tag就是用来识别常量池中元素的重要标记。tag对应的表需要去Figure 3中查找。

第1个常量<0A 00 04 00 0F>。0A标识这是一个CONSTANT_Methodref_info常量,查看Figure3中CONSTANT_Methodref_info表结构可知,<00 04>是声明方法的类描述符,<00 0F>是方法的名称及类型描述符。查看Figure 15可知,<00 04>表示java/lang/Object,<00 0F>表示"":()V。综合起来看就是java/lang/Object."":()V,与Figure15中常量池第一项正确对应。

第2个常量<09 00 03 00 10>。09标识这是一个CONSTANT_Fieldref_info常量,查看Figure3中CONSTANT_Fieldref_info表结构可知,<00 03>是声明字段的类描述符,<00 10>是字段描述符。查阅Figure15可知,<00 03>表示org/fenixsoft/clazz/TestClass,<00 10>表示 m:I。综合看,为org/fenixsoft/clazz/TestClass.m:I,同Figure15常量池中第二项对应正确。

第3个常量<07 00 11>。07标识这是一个CONSTANT_Class_info常量,查看CONSTANT_Class_info表结构及Figure15,可知<00 11>是类的全限定名org/fenixsoft/clazz/TestClass。

第4个常量<07 00 12>。表示类全限定名java/lang/Object。

第5个常量<01 00 01 6D>。01标识这是一个CONSTANT_Utf8_info常量,查看CONSTANT_Utf8_info表结构,可知<00 01>表示该UTF-8编码的字符串占用字节数为1。所有,往后一个字节<6D>就是UTF-8编码的字符串,通过ascii码表可知,<6D>为m。

第6个常量<01 00 01 49>。表示字符1。从第7个常量开始到第18个,使用WinHex帮助查看。
第7个常量<01 00 06 3C 69 6E 69 74 3E>。01标识这是一个CONSTANT_Utf8_info常量,这个字符串使用UTF-8缩略编码。这里直接引用《深入理解JVM》中一段来说UTF-8缩略编码与普通UTF-8编码的区别。

从'\u0001'到'\u007f'之间的字符的缩略编码使用一个字节表示;从'\u0080'到'\u07ff'之间的所有字符的缩略编码用两个字节表示;从'\u0800'到'\uffff'之间的所有字符的缩略编码就按照普通UTF-8编码规则使用三个字节表示。

所以,这个常量再逐个字节去分析就比较困难了,我们结合WinHex查看,结果一目了然,这个字符串为""
image.png

第8个常量<01 00 03 28 29 56>。字符串为"()V"
image.png

第9个常量<01 00 04 43 6F 64 65>。字符串为"Code"
image.png

第10个常量<01 00 0F 4C 69 6E 65 4E 75 6D 62 65 72 54 61 62 6C 65>。字符串为"LineNumberTable"
image.png

第11个常量<01 00 03 69 6E 63>。字符串为"inc"
image.png

第12个常量<01 00 03 28 29 49>。字符串为" ()I "。
image.png

第13个常量<01 00 0A 53 6F 75 72 63 65 46 69 6C 65>。字符串为"SourceFile"。
image.png

第14个常量<01 00 0E 54 65 73 74 43 6C 61 73 73 2E 6A 61 76 61>。字符串为"TestClass.class"。
image.png

第15个常量<0C 00 07 00 08>。tag为0C,查询Figure3可知,这是一个CONSTANT_NameAndType_info常量。查询Figure15,可知<00 07>表示,<00 08>表示()V,所以该常量为 “:()V”

第16个常量<0C 00 05 00 06>。同上一个常量是同一个类型。为“m:I”。

第17个常量<01 00 1D 6F 72 67 2F 66 65 6E 69 78 73 6F 66 74 2F 63 6C 61 7A 7A 2F 54 65 73 74 43 6C 61 73 73>。字符串为"org/fenixsoft/clazz/TestClass"。
image.png

第18个常量<01 00 10 6A 61 76 61 2F 6C 61 6E 67 2F 4F 62 6A 65 63 74>。字符串为"java/lang/Object"。
image.png
  1. 接着,u2类型的access_flags<00 21>。TestClass是一个普通类,被public修饰,使用JDK1.8编译器编译,所以ACC_PUBLIC、ACC_SUPER标志应为真,所以access_flags = 0x0001 | 0x0020 = 0x0021。

  2. 接着,u2类型的类索引、u2类型的父类索引、u2类型的接口数量、u2类型的接口索引。其中,
    <00 03>为 org/fenixsoft/clazz/TestClass
    <00 04>为 java/lang/Object
    <00 00> 则表示当前类实现的接口数量为0。接口数量为0时,后面的接口索引不再占用任何字节。

  3. 接着,u2类型的字段数量fields_count及fields_count大小的字段索引fields。
    <00 01>表示当前类中有一个字段。
    <00 02 00 05 00 06 00 00>。查看字段表结构,
    <00 02>表示字段的access_flags被设置为ACC_PRIVATE;
    <00 05>查询Figure15,为m
    <00 06>查询Figure15, 为I
    <00 00>表示attrubutes_count为0。所以,后面的attributes也不再占用任何字节。
    这样综合一下,可以推断为 private int m;

  4. 接着,u2类型的方法数量methods_count及methods_count大小的方法集合methods。<00 02 00 01 00 07 00 08 00 01 00 09 00 00 00 1D 00 01 00 01 00 00 00 05 2A B7 00 01 B1 00 00 00 01 00 0A 00 00 00 06 00 01 00 00 00 03 >。我们划分着看,
    <00 02>表示methods_count值为2,即有两个方法。接着看具体是哪两个方法。
    <00 01 00 07 00 08> methods_count之后就是methods,<00 01>为access_flags,查询Figure 9可知,该方法是public公共方法。<00 07 00 08>则是方法名称及描述符,查询Figure 15可知,该方法为:()V。即,默认构造方法 public void TestClass(){ ... }
    <00 01>描述符后面是占用2个字节的attributes_count,即属性表数量。这里表示TestClass类默认构造方法有一个属性表。
    <00 09 00 00 00 1D 00 01 00 01 00 00 00 05 2A B7 00 01 B1 00 00>查看属性表结构,首先是u2类型的属性名称索引attribute_name_index,查阅Figure 15, <00 09>常量为“Code”,也就是说这一段数据是Code属性表。查看Code属性表结构,之后4个字节的attribute_length为29(<00 00 00 1D>)。也就是后面的29个字节都是该属性的内容。<00 01>表示操作数栈的最大深度值为1。<00 01>表示局部变量表的最大容量为1。接下来的code_length和code用来存储TestClass类编译后生成的字节码指令。<00 00 00 05>表示code_length为5,所有接下的5个字节<2A B7 00 01 B1>就是存放的字节码指令。每一个字节码指令占用一个字节,所以这里有5个字节码指令。2A表示aload_0,B7表示invokespecial,<00 01>表示invokespecial的参数,查询Figure15可知,为Method java/lang/Object."":()V。B1表示return。接下来的<00 00>是exception_table_length,默认构造方法没有抛出异常,所以后面的exception_table不占用任何字节。
    <00 01> 表示attrubutes_count。(注意,这是Code属性表中的一部分)
    <00 0A 00 00 00 06 00 01 00 00 00 03 > 接着看attrubutes(注意,这是Code属性表中的一部分)。<00 0A>表示这是一个LineNumberTable属性表。查看LineNumberTable属性表结构,<00 00 00 06>为attribute_length,<00 01>为line_number_table_length,<00 00 00 03>则是line_number_info类型的line_number_table。

以上就是TestClass默认构造方法的所有字节码信息。再贴上反编译后的文件内容,对照着看一下该方法都有什么信息。

 public org.fenixsoft.clazz.TestClass();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."":()V
         4: return
      LineNumberTable:
        line 3: 0

接下来是第二个方法,就不再继续分析,直接贴上对应的信息。
<00 01 00 0B 00 0C 00 01 00 09 00 00 00 1F 00 02 00 01 00 00 00 07 2A B4 00 02 04 60 AC 00 00 00 01 00 0A 00 00 00 06 00 01 00 00 00 06>

<00 01 00 0B 00 0C> //access_flags name_index descriptor_inex (public int inc)
<00 01> //attributes_count
<00 09 00 00 00 1F 00 02 00 01 00 00 00 07 2A B4 00 02 04 60 AC 00 00 00 01 00 0A 00 00 00 06 00 01 00 00 00 06>
再细分<00 09>Code属性表; <00 00 00 1F>attribute_length; <00 02>max_stack; <00 01>max_locals; <00 00 00 07>code_length; <2A B4 00 02 04 60 AC>code; <00 00>exception_table_length; <00 01>attrubutes_count; <00 0A>LineNumberTable属性; <00 00 00 06 00 01 00 00 00 06>line_number_table_length及line_number_info

  1. 接着,属性表集合,attributes_count与attributes,这也是class文件中的最后一个组成部分。<00 01 00 0D 00 00 00 02 00 0E>, 其中,<00 01>是attributes_count,为1。查阅Figure 15,可知<00 0D>表示这是一个SourceFile属性。查看SourceFile属性表结构。<00 00 00 02>为attribute_length,肉眼可见确实只剩下两个字节的数据了。<00 0E>为sourcefile索引sourcefile_index。查阅Figure 15可知,为TestClass.class。
后记

终于到后记部分了,呼。再回头看一下java源码,

package org.fenixsoft.clazz; 

public class TestClass{ 
    private int m;
    public int inc(){ 
        return m + 1;
    } 
}

你问我什么感觉?EMMM,,,,,,幸亏没手贱多写一个变量,多写一个方法。最后,解析字节码这种事情还是交给虚拟机做吧。最后奉上excel文件链接,不嫌麻烦的小伙伴可以搭配excel文件对class文件结构学习一波吧。
链接:https://pan.baidu.com/s/10JRSAhAXeU17n4f9Wom4wg
提取码:sg90

参考资料:

  1. 《深入理解Java虚拟机》
  2. 《Java虚拟机规范(JavaSE 7版)》
  3. https://www.cnblogs.com/longjee/p/8675771.html

你可能感兴趣的:(读《深入理解JVM》笔记之class文件结构)