你的位置:首页 > Java教程

[Java教程]JVM之Class文件


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

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

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

ClassFile

常量池

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

类型 标志 大小 结构 描述
CONSTANT_Utf8_info 0x01 3+len Utf8 常量字符串,比如方法名,字段名等都需要引用此类型,以下简称 utf8
CONSTANT_String_info 0x08 3 字符串类型字面量,string_index引用 utf8 表示内容
CONSTANT_Class 0x07 3 类或接口的符号引用
CONSTANT_NameAndType 0x0c 5 字段或方法的符号引用,但没有指明是哪个类或接口的方法
CONSTANT_Fieldref 0x09 5 字段的符号引用,表明所属类,和字段名
CONSTANT_Methodref 0x0a 5 方法的符号引用,表明所属类,和方法名
CONSTANT_InterfaceMethodref 0x0b 5 接口中方法的符号引用,表明所属接口,和方法名
CONSTANT_Integer 0x03 5 整型字面量
CONSTANT_Float 0x04 5 浮点型字面量
CONSTANT_Long 0x05 6 长整型字面量
CONSTANT_Double 0x06 6 双精度浮点型字面量
CONSTANT_MethodHandle 0x0f 4 表示方法句柄
CONSTANT_MethodType 0x10 3 表示方法类型
CONSTANT_InvokeDynamic 0x12 5 动态调用

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

  其中,连续的,颜色相同的框表示一个类型的信息,比如【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位转为字符串就是<init>,这是类实例化时调用的方法。感兴趣可以写个代码解析一下,下面看一下,javap反编译的结果:

  这个图结合下表看懂应该问题不大,这里就不详细解释了。类,字段和方法描述符:

类和接口名,使用全限定名称 把 点. 换成 /,比如 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 声明 枚举

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 个字节表示接口的个数,接下来就是具体内容,会按照源代码中实现的顺序排列。

方法,字段以及属性描述

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

之前介绍了类的访问控制符,现在来看看字段的访问控制符:

标志名称 标志值 含义
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都是对常量池中的字符串的引用。字段的附加属性。

 

本文使用的Demo源码和Class文件:http://files.cnblogs.com/files/cyhe/Demo.rar