JVM之Class文件

单纯的看 JVM 规范有点无聊了,看的云里雾里的,所谓“不闻不若闻之,闻之不若见之,见之不若知之,知之不若行之,学至于行而止矣,行之,明也”。本文从一个简单的Java程序一步一步探讨Class文件的结构。先从简单的 Hello World 开始,使用 javac 和 javap命令进行编译和反编译,由于反编译输出的信息太多,将分成片段来进行描述,如下:

java 生成class 忽略缺少文件_字段

其中 javac –g 表示生成所有的调试信息,javap –p –v 表示显示全部的详细信息。每个Class文件都是以 8-bit 为单位的字节流,多字节数据以 big-endian 大端排列(比如1314这个数,以 4字节存储,那么,大端表示为 0x00000522,小端表示为0x22050000)。为了标识class文件的正确性,每个class都有一个magic魔数(0xCAFEBABE),和两个主从版本号字段,如上图。JVM在加载链接时,会进行校验。使用高版本编译的文件,不能再低版本使用,反之可以。下面查看下class文件的十六进制表示:

java 生成class 忽略缺少文件_字段

从图中可以看出,前4个字节为magic=0xcafebabe,2字节的minor=0x0000,2字节的major=0x0034=3*16+4=52,在往后的 2 个字节 0x0022 表示常量池的大小为34,根据JVM规范里的Class文件格式,根据相应的偏移量,就可以解析你需要的信息。这里的字节序是网络序,比较符合我们的直觉。ClassFile 结构如下:

java 生成class 忽略缺少文件_字段

常量池

JVM 执行时,不依赖类,实例或接口在内存是怎么布局的,而是依赖运行时常量池,基于栈来执行指令。常量池中包含字面量和符号引用,字面量就是字符串常量和数值常量;符号引用就是用来描述类或接口、字段名和其描述符、方法名和其描述符。根据 ClassFile 的结构,可以看到 2 字节表示常量池大小,后面 cp_info 是具体内容,cp_info不同类型的大小不同,常量池中的类型如下: 类型 标志 大小 结构 描述

CONSTANT_Utf8_info 0x01 3+len

java 生成class 忽略缺少文件_字段

Utf8 常量字符串,比如方法名,字段名等都需要引用此类型,以下简称 utf8 CONSTANT_String_info 0x08 3

java 生成class 忽略缺少文件_字段

字符串类型字面量,string_index引用 utf8 表示内容 CONSTANT_Class 0x07 3

java 生成class 忽略缺少文件_字段

类或接口的符号引用 CONSTANT_NameAndType 0x0c 5

java 生成class 忽略缺少文件_字段

字段或方法的符号引用,但没有指明是哪个类或接口的方法 CONSTANT_Fieldref 0x09 5

java 生成class 忽略缺少文件_字段

字段的符号引用,表明所属类,和字段名 CONSTANT_Methodref 0x0a 5

java 生成class 忽略缺少文件_字段

方法的符号引用,表明所属类,和方法名 CONSTANT_InterfaceMethodref 0x0b 5

java 生成class 忽略缺少文件_字段

接口中方法的符号引用,表明所属接口,和方法名 CONSTANT_Integer 0x03 5

java 生成class 忽略缺少文件_字段

整型字面量 CONSTANT_Float 0x04 5

java 生成class 忽略缺少文件_字段

浮点型字面量 CONSTANT_Long 0x05 6

java 生成class 忽略缺少文件_字段

长整型字面量 CONSTANT_Double 0x06 6

java 生成class 忽略缺少文件_字段

双精度浮点型字面量 CONSTANT_MethodHandle 0x0f 4

java 生成class 忽略缺少文件_字段

表示方法句柄 CONSTANT_MethodType 0x10 3

java 生成class 忽略缺少文件_字段

表示方法类型 CONSTANT_InvokeDynamic 0x12 5

java 生成class 忽略缺少文件_字段

动态调用

知道了常量池中类型的大小,就可以进行解析了,如下图,是部分常量池的十六进制表示:

java 生成class 忽略缺少文件_字段

其中,连续的,颜色相同的框表示一个类型的信息,比如【0a 00 06 00 14】标识为10,查询上表,可知此字段大小为5,JVM 会对常量池中解析的类型从 1 进行编号,这个字段内容就为,#1 = Methodref  #6.#20 后面引用所属类和方法描述。其他字段也是如此,如果需要引用到常量池中的其他内容解析出编号即可。再看一个常量字符串的序列【01 00 06 3c 69 6e 69 74 3e】01表明是utf8字符串,00 06 表示长度为6,后面6位转为字符串就是,这是类实例化时调用的方法。感兴趣可以写个代码解析一下,下面看一下,javap反编译的结果:

java 生成class 忽略缺少文件_字段

这个图结合下表看懂应该问题不大,这里就不详细解释了。类,字段和方法描述符: 类和接口名,使用全限定名称 把 点. 换成 /,比如 java.lang.Object 全限定名:java/lang/Object

方法,字段,局部变量名,使用非全限定名称 当前类的相对名称

描述符

字段描述符,描述什么类型

B,C,D,F,I,[ 分别对应 byte,char,double,float,int,数组类型

J,S,Z,V,L class_name 分别对应 long,short,boolean,void,class_name对象类型

方法描述符,描述参数和返回值类型 ( {ParameterDescriptor} ) ReturnDescriptor

()V Object m(int i, double d, Thread t) {...}

无参返回值为空 (IDLjava/lang/Thread;)Ljava/lang/Object;

当前类,父类和接口

根据ClassFIle的结构,接下来是 2 字节的 access_flags,【access_flags】表示类或接口的访问权限,具体的标志位如下表: 标志名称 标志值 含义

ACC_PUBLIC 0x0001 声明为 public

ACC_FINAL 0x0010 声明为 final,不能被继承

ACC_SUPER 0x0020 表明使用 invokespecial 调用父类方法

ACC_INTERFACE 0x0200 声明为 接口

ACC_ABSTRACT 0x0400 声明为抽象类,不能实例化

ACC_SYNTHETIC 0x1000 动态生成的代码,不是用户由编写的

ACC_ANNOTATION 0x2000 声明 注解

ACC_ENUM 0x4000 声明 枚举

java 生成class 忽略缺少文件_字段

access_flags使用 2 个字节表示,由上图可知为 0x0021,转换成二进制 【0000 0000 0020 0001】,查上表可得此类为 ACC_PUBLIC,ACC_SUPER。

接下来就是当前类索引,父类索引和接口索引描述,其中当前类和父类描述都是通过 2 个字节,2 字节的 this_class,2 字节的 super_class。这里this_calss是 #5 表示引用常量池中第5个常量,从常量池中可以看到就是当前的Demo类;super_class是 #6 ,查常量池可知,#6代表java.lang.Object对象,这说明了,Object是Java中所有类的直接或间接父类。如果类实现了接口,接口的信息会在接下来的字节表示出来,这里没有实现接口,字节全为 0 ,如果实现接口的话,首先 2 个字节表示接口的个数,接下来就是具体内容,会按照源代码中实现的顺序排列。

方法,字段以及属性描述

接下来就是描述此类的字段和方法信息,主要有字段的访问类型,数据类型等,方法的参数,返回值,字节码等。首先看一下每个字段表示的格式:

java 生成class 忽略缺少文件_字段

之前介绍了类的访问控制符,现在来看看字段的访问控制符: 标志名称 标志值 含义

ACC_PUBLIC 0x0001 声明为 public

ACC_PRIVATE 0x0002 声明为 private

ACC_PROTECTED 0x0004 表明为 protected

ACC_STATIC 0x0008 声明为 static

ACC_FINAL 0x0010 声明为 final

ACC_VOLATILE 0x0040 声明为 volatitle

ACC_TRANSIENT 0x0080 声明为 transient 不序列化

ACC_SYNTHETIC 0x1000 表示有编译器生成

ACC_ENUM 0x4000 声明为 enum

其中 2 字节name_inedx和 2 字节descriptor_index都是对常量池中的字符串的引用。字段的附加属性。

java 生成class 忽略缺少文件_字段