深入理解JVM(一)字节码详解

文章目录

    • 1 什么是字节码文件
    • 2 class文件结构
      • 2.1 魔数(magic)
      • 2.2 次版本号(minor_version)和主版本号(major_version)
      • 2.3 常量池
        • 2.3.1 常量池容量计数器(constant_pool_count)
        • 2.3.2 常量表集合constant_pool[constant_pool_count-1]
      • 2.4 访问标志(access_flags)
      • 2.5 类索引(this_class)和父类索引(super_class)
      • 2.6 接口索引集合
        • 2.6.1 接口计数器(interfaces_count)
        • 2.6.2 接口索引集合interfaces[interfaces_count]
      • 2.8 字段表集合
        • 2.8.1 字段容量计数器fields_count
        • 2.8.2 字段表集合fields[fields_count]
      • 2.9 方法表集合
        • 2.9.1 方法容量计数器methods_count
        • 2.9.2 方法表集合methods[methods_count]
      • 2.10 属性表集合
        • 2.10.1 属性容量计数器attributes_count
        • 2.10.2 属性表集合attributes[attributes_count]
    • 3 解析示例

1 什么是字节码文件

在cpu层面看来计算机中所有的操作都是一个个指令的运行汇集而成的,java是高级语言,只有人类才能理解其逻辑,计算机是无法识别的,所以java代码必须要先编译成字节码文件,jvm才能正确识别代码转换后的指令并将其运行,这个编译后的java代码文件,就是java字节码文件(.class)

2 class文件结构

u1,u2,u4,u8来分别代表1个字节,2个字节,4个字节和8个字节的无符号数。

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];//属性表集合
}

Class文件结构中只有常量池的容量计数是从1开始,对于其他集合类型,包括接口索引集合、字段表集合、方法表集合等的容量计数都是从0开始。

2.1 魔数(magic)

文件开头的4个字节(“cafe babe”)称之为魔数(magic),唯有以"cafe babe"开头的class文件方可被JVM所接受,这4个字节就是字节码文件的身份识别。

2.2 次版本号(minor_version)和主版本号(major_version)

第5和第6个字节是编译器jdk的次版本号(minor_version),第7和第8个字节是编译器jdk的主版本号(major_version)
深入理解JVM(一)字节码详解_第1张图片

2.3 常量池

常量池中主要存放两大类常量:字面量和符号引用。

字面量比较接近于Java语言层面的常量概念,例如

  • 文本字符串
  • 被声明为final的常量值等

符号引用则属于编译原理方面的概念,主要包括下面几类常量:

  • 类和接口的全限定名
  • 字段的名称和描述符
  • 方法的名称和描述符
  • 方法句柄和方法类型
  • 动态调用点和动态常量

Java代码在进行javac编译的时候,并不像C和C++那样有“连接”这一步骤,而是在JVM加载Class文件的时候进行动态连接。也就是说这些字段和方法符号引用只有在运行期转换后才能获得真正的内存入口地址。当JVM做类加载时,将会从常量池获得对应的符号引用,再在类创建时或运行时解析、翻译到具体的内存地址之中。

2.3.1 常量池容量计数器(constant_pool_count)

紧接着主、次版本号之后的是常量池入口,由于常量池中常量的数量是不固定的,所以在常量池的入口需要放置一项u2类型的数据(第9和第10个字节),代表常量池容量计数值(constant_pool_count)。这个容量计数是从1而不是0开始的,因此常量池容量=常量池容量计数值-1

2.3.2 常量表集合constant_pool[constant_pool_count-1]

常量池容量计数值之后的是常量表集合constant_pool[constant_pool_count-1],常量池中每一项常量都是一个表,截至JDK13,常量表中分别有17种不同类型的常量。这17类表都有一个共同的特点,表结构起始的第一位是个u1类型的标志位,代表着当前常量属于哪种常量类型

深入理解JVM(一)字节码详解_第2张图片
这17种常量类型各自有着完全独立的数据结构,两两之间并没有什么共性和联系。
深入理解JVM(一)字节码详解_第3张图片
深入理解JVM(一)字节码详解_第4张图片
深入理解JVM(一)字节码详解_第5张图片

2.4 访问标志(access_flags)

在常量池结束之后,紧接着的2个字节代表访问标志(access_flags),这个标志用于识别一些类或者接口层次的访问信息,包括:这个Class是类还是接口;是否定义为public类型;是否定义为abstract类型;如果是类的话,是否被声明为final等等,access_flags中一共有16个标志位可以使用,当前只定义了其中9个,没有使用到的标志位要求一律为零。
深入理解JVM(一)字节码详解_第6张图片
一个普通Java类,不是接口、枚举、注解或者模块,被public关键字修饰但没有被声明为final和abstract,并且它使用了JDK1.0.2之后的的编译器进行编译,因此它的ACC_PUBLIC、ACC_SUPER标志应当为真,而另外七个标志应当为假,因此它的access_flags的值应为:0x0001|0x0020=0x0021。

2.5 类索引(this_class)和父类索引(super_class)

访问标识后面是类索引(this_class)用于确定这个类的全限定名,之后是父类索引(super_class)用于确定这个类的父类的全限定名。由于Java语言不允许多重继承,所以父类索引只有一个,除了java.lang.Object之外,所有的Java类都有父类,因此除了java.lang.Object外,所有Java类的父类索引都不为0。

2.6 接口索引集合

随后是接口索引集合用来描述这个类实现了哪些接口,这些被实现的接口将按implements关键字(如果这个Class文件表示的是一个接口,则应当是extends关键字)后的接口顺序从左到右排列在接口索引集合中。

2.6.1 接口计数器(interfaces_count)

对于接口索引集合,入口的第一项u2类型的数据为接口计数器(interfaces_count),表示索引表的容量。如果该类没有实现任何接口,则该计数器值为0。

2.6.2 接口索引集合interfaces[interfaces_count]

由于Java支持多接口,因此这里设计成了接口计数器和接口索引集合来实现。如果该类没有实现任何接口,则接口索引表interfaces[interfaces_count]不再占用任何字节。

2.8 字段表集合

字段表(field_info)用于描述接口或者类中声明的变量。Java语言中的“字段”(Field)包括类级变量以及实例级变量,但不包括在方法内部声明的局部变量。

2.8.1 字段容量计数器fields_count

对于字段表集合,入口的第一项u2类型的数据为字段容量计数器fields_count

2.8.2 字段表集合fields[fields_count]

深入理解JVM(一)字节码详解_第7张图片
字段修饰符放在access_flags项目中,它与类中的access_flags项目是非常类似的,都是一个u2的数据类型。
深入理解JVM(一)字节码详解_第8张图片

由于语法规则的约束,ACC_PUBLIC、ACC_PRIVATE、ACC_PROTECTED三个标志最多只能选择其一,ACC_FINAL、ACC_VOLATILE不能同时选择。接口之中的字段必须有ACC_PUBLIC、ACC_STATIC、ACC_FINAL标志,这些都是由Java本身的语言规则所导致的。

跟随access_flags标志的是两项索引值:name_index和descriptor_index。它们都是对常量池项的引用,分别代表着字段的简单名称以及字段和方法的描述符。在descriptor_index之后跟随着一个属性表集合,用于存储一些额外的信息,字段表可以在属性表中附加描述零至多项的额外信息。

简单名称就是指没有类型和参数修饰的方法或者字段名称,描述符的作用是用来描述字段的数据类型、方法的参数列表(包括数量、类型以及顺序)和返回值。

深入理解JVM(一)字节码详解_第9张图片
注:void类型在《Java虚拟机规范》之中单独列出为“VoidDescriptor”,笔者为了结构统一,将其列在基本数据类型中一起描述。

字段表集合中不会列出从父类或者父接口中继承而来的字段,但有可能出现原本Java代码之中不存在的字段,譬如在内部类中为了保持对外部类的访问性,编译器就会自动添加指向外部类实例的字段。另外,Java语言中字段是无法重载的,两个字段的数据类型、修饰符不管是否相同,都必须使用不一样的名称,但是对于Class文件格式来讲,只要两个字段的描述符不是完全相同,那字段重名就是合法的。

2.9 方法表集合

Class文件存储格式中对方法的描述与对字段的描述采用了几乎完全一致的方式,方法表的结构如同字段表一样,依次包括访问标志(access_flags)、名称索引(name_index)、描述符索引(descriptor_index)、属性表集合几项。

2.9.1 方法容量计数器methods_count

对于方法表集合,入口的第一项u2类型的数据为方法容量计数器methods_count

2.9.2 方法表集合methods[methods_count]

深入理解JVM(一)字节码详解_第10张图片

因为volatile关键字和transient关键字不能修饰方法,所以方法表的访问标志中没有了ACC_VOLATILE标志和ACC_TRANSIENT标志。与之相对,synchronized、native、strictfp和abstract关键字可以修饰方法,方法表的访问标志中也相应地增加了ACC_SYNCHRONIZED、
ACC_NATIVE、ACC_STRICTFP和ACC_ABSTRACT标志

深入理解JVM(一)字节码详解_第11张图片

与字段表集合相对应地,如果父类方法在子类中没有被重写,方法表集合中就不会出现来自父类的方法信息。但同样地,有可能会出现由编译器自动添加的方法,最常见的便是类构造器“< clinit >()”方法和实例构造器“< init >()”方法。

在Java语言中,要重载(Overload)一个方法,除了要与原方法具有相同的简单名称之外,还要求必须拥有一个与原方法不同的特征签名。特征签名是指一个方法中各个参数在常量池中的字段符号引用的集合,也正是因为返回值不会包含在特征签名之中,所以Java语言里面是无法仅仅依靠返回值的不同来对一个已有方法进行重载的。但是在Class文件格式之中,特征签名的范围明显要更大一些,只要描述符不是完全一致的两个方法就可以共存。也就是说,如果两个方法有相同的名称和特征签名,但返回值不同,那么也是可以合法共存于同一个Class文件中的。

行文至此,也许有的读者会产生疑问,方法的定义可以通过访问标志、名称索引、描述符索引来表达清楚,但方法里面的代码去哪里了?方法里的Java代码,经过Javac编译器编译成字节码指令之后,存放在方法属性表集合中一个名为“Code”的属性里面,属性表作为Class文件格式中最具扩展性的一种数据项目,将在下一节中详细讲解。

2.10 属性表集合

属性表(attribute_info)在前面的讲解之中已经出现过数次,Class文件、字段表、方法表都可以携带自己的属性表集合,以描述某些场景专有的信息。

2.10.1 属性容量计数器attributes_count

对于属性表集合,入口的第一项u2类型的数据为属性容量计数器attributes_count

2.10.2 属性表集合attributes[attributes_count]

与Class文件中其他的数据项目要求严格的顺序、长度和内容不同,属性表集合的限制稍微宽松一些,不再要求各个属性表具有严格顺序,并且《Java虚拟机规范》允许只要不与已有属性名重复,任何人实现的编译器都可以向属性表中写入自己定义的属性信息,Java虚拟机运行时会忽略掉它不认识的属性。

对于每一个属性,它的名称都要从常量池中引用一个CONSTANT_Utf8_info类型的常量来表示,而属性值的结构则是完全自定义的,只需要通过一个u4的长度属性去说明属性值所占用的位数即可。
深入理解JVM(一)字节码详解_第12张图片
虚拟机规范预定义的属性如下:
深入理解JVM(一)字节码详解_第13张图片
深入理解JVM(一)字节码详解_第14张图片
由于属性结构过多,本文就不详细介绍了,想了解的可自行查阅其它资料

3 解析示例

  • 新建Test.java文件,代码如下
public class Test {
    public static void main(String[] args) {
        System.out.println("hello world");
    }
}
  • 在cmd中进入Test.java所在目录,使用javac命令在当前所在路径下生成一个Test.class文件
javac Test.java
  • 使用WinHex打开生成的Test.class文件(16进制)

深入理解JVM(一)字节码详解_第15张图片
CA FE BA BE为魔数
00 00为次版本号
00 34为主版本号,0x0034=52,编译器jdk版本为jdk8
00 1D为常量池容量计数器,0x001D=29,常量池中常量个数为28
0A 为第一个常量的标志位,0x0A=10,表示当前常量属于CONSTANT_Methodref_info类型
深入理解JVM(一)字节码详解_第16张图片
00 0600 0F为两个索引,0x0006=6,0x000F=15,表示该常量指向了第6和第15个常量

我们可以通过对Class文件反编译从而更方便的进行分析

在cmd中使用javap命令将Test.class进行反编译

javap -verbose Test.class

常量池容量计数值为00 1D,转换为10进制为29,所以常量池中共有28个常量
深入理解JVM(一)字节码详解_第17张图片
深入理解JVM(一)字节码详解_第18张图片

开头的7行信息包括:Class文件当前所在位置,最后修改时间,文件大小,MD5值,编译自哪个文件,类的全限定名,jdk次版本号,主版本号。

第一个常量是一个方法定义,指向了第6和第15个常量,查看第6和第15个常量,可以拼接成第一个常量右侧的注释内容,其它常量同理。

java/lang/Object."":()V

常量池至下图前的56结束
深入理解JVM(一)字节码详解_第19张图片
00 21代表访问标志,因为Test.java一个普通Java类,不是接口、枚举、注解或者模块,被public关键字修饰但没有被声明为final和abstract,并且它使用了JDK1.0.2之后的的编译器进行编译,因此它的ACC_PUBLIC、ACC_SUPER标志应当为真,而另外七个标志应当为假,因此它的access_flags的值应为:0x0001|0x0020=0x0021。

00 05为类索引,查看反编译后的文件得知,它指向了第21个常量,值为Test,表示该类的全限定名为"Test"
00 06为父类索引,查看反编译后的文件得知,它指向了第22个常量,值为"java/lang/Object",表示该类的父类的全限定名为"java/lang/Object"
00 00为接口计数器,表示该类没有实现任何接口,后面也就没有接口的索引表
再后面的00 00为字段容量计数器,表示该类中声明的变量的个数为0,后面也就没有字段表
00 02为方法计数器,表示该类中声明的方法的个数为2
00 01为第一个方法的访问标志,表示该方法的修饰符只有public
00 07为第一个方法的方法名称索引,查看反编译后的文件得知,它的值为"< init >",表示第一个方法的方法名称为"< init >"
00 08为第一个方法的描述符索引,表示该方法的参数列表(包括数量、类型以及顺序)和返回值,查看反编译后的文件得知,它的值为"()V",表示该方法的返回值为特殊类型void

之后为该方法的属性表,由于属性表过于复杂,这里就不进行分析了,感兴趣的可尝试进行分析

over

你可能感兴趣的:(Java,JVM,java,jvm)