JVM中class文件探索与解析

时间:2022-11-08 17:20:26

一直想成为一名优秀的架构师的我,转眼已经工作快两年了,对于java内核了解甚少,闲来时间,看看JVM,吧自己的一些研究写下来供大家参考,有不对的地方请指正。

废话不多说,一起来看看JVM中类文件是如何加载和运行的。

(1)首先,编写简单代码,对其编译生成的class文件进行研究,其java代码如下:

JVM中class文件探索与解析JVM中class文件探索与解析
 1 public class test {
 2     private static int count = 0;
 3     public static void recursion(){
 4         count++;
 5         recursion();
 6     }
 7 
 8     public  static  void  main (String args[]){
 9         try {
10             recursion();
11         }catch (Exception ex){
12             System.out.print("deep of callings:"+count+"\n");
13             ex.printStackTrace();
14         }
15     }
16 }
View Code

编译之后,用WinHex软件打开其class文件,可以看到其编译的十六进制文件如下:

JVM中class文件探索与解析

  按照上图分析,开头的前4个字节JVM中class文件探索与解析,是魔数(类似于拼音“咖啡宝贝”),它的用处是标识该文件是否能被java虚拟机识别;

  紧接着魔数的4个字节JVM中class文件探索与解析,前两字节0x00代表次版本号(小数点之后的数字),后两字节0x0033代表是class文件的主版本号,换算成十进制是51,标识是JDK1.7可识别的版本(不同的版本可以查看class文件版本号表如下:)

版本号 对应十进制 jdk版本号
2E 46 jdk1.2
2F 47 jdk1.3
30 48 jdk1.4
31 49 jdk1.5
32 50 jdk1.6
33 51 jdk1.7
34 52 jdk1.8

  在主版本号字节之后的是常量池,可以理解为Class文件的资源仓库,存储着与class文件相关的数据项。由于不同class文件,常量池数量不同,常量池入口放置两个字节的数据JVM中class文件探索与解析(0x0028)为常量池计数器。十六进制的0x0028为十进制的40(地址偏移量),代表常量池中有39个常量,索引范围为1-40(注:java仅限于class文件结构中容量计数器是从1开始的,java的设计者将索引0拿出来是有特殊考虑的,用来表示不引用任何一个常量池中的项)。

  常量池中,存放两类数据:(1)字面量:可以理解为java中的常量,例如:字符串、final修饰常量等。

              (2)符号引用:主要包括①类、接口的全限定名②字段的名称和描述符③方法的名称和描述符

  在常量池里,存储常量结构如下:u1(常量标志位,用于指明常量的类型,可以查看如下常量池项目类型对应表)+常量信息

JVM中class文件探索与解析

  让我们以上述class文件为例,索引为1的常量标志位是0x0A(十进制为10),对应上表中的CONSTANT_Methoddef_info类型的常量,参考常量结构表如下图(在jdk1.7中新增了tag=15/16/18的常量类型,更好的支持动态语言的调用,此处就不列举了),

JVM中class文件探索与解析

  该class文件中,常量池里索引为1的常量(const#1),项目类型标识符为0x0A,二进制为10,查询上表,代表着类方法的符号引用。紧接着两个u2字符代表该常量的信息内容,其中方法描述符0x0006为#6常量,名称及类型描述为0x001A指向#26常量。

  紧跟其后的是索引为2的常量(const#2),其标志符为0x09(十进制为9),是字段的符号引用,紧接着的两个u2字符代表其引用索引ID,方法的类描述符指向#27常量,字段描述符指向#28常量;

  分析了以上两个字节之后,这里就不一一分析后面的常量了,有兴趣的可以自己分析下。其他的常量池用jdk自带的javap进行生成,在windows中打开cmd(安装jdk并配置环境变量),输入:javap -verbose class文件路径,可以看到编译之后的常量池如下:

JVM中class文件探索与解析

   将上述class文件中常量池部分标记图如下,红色框代表一个常量池中的项,依次编号为1-39,

JVM中class文件探索与解析

  我们将上图和javap生成的常量内容对比一下,以const#9为例,#9项为:0x01 (utf8类型) 0x0006(占用字节) 0x3C 0x69 0x6E 0x69 0x74 0x3E(项内容),我们对项内容进行在线转换,将十六进制转换ASCII码值,得到该常量表示:<init>,如下图:

JVM中class文件探索与解析

  与javap生成的常量文件对比,发现两者完全一致。对字节码有兴趣的朋友可以逐个试一试。

JVM中class文件探索与解析

  在常量池区域结束之后,紧接着的一个u2(两个字节)类型的字符代表访问标志,它用于识别类或者接口的访问信息,例如:class是类还是接口,访问是private还是public等。访问标志表如下图:

 JVM中class文件探索与解析

  在上述文件中,访问标志为:JVM中class文件探索与解析,即:0x0021,对照上表,只有ACC_PUBLIC和ACC_SUPER为真,其他几项为假。该类为public 能够使用invoke指令。

  跟在访问标志之后的分别是类索引、父类索引JVM中class文件探索与解析。由于java不允许多继承,所以类索引和父类索引是一个u2类型的数据。在上述文件中,类索引为#5常量(TestClass),父类索引为#6常量(java/lang/Object);

  紧接着类索引和父类索引的是接口索引信息。在java中一个类可以实现多个接口,所以用u2类型的数据集合来表示接口索引。在接口索引的入口,有一项u2类型的接口计数器JVM中class文件探索与解析,计数器为0表示接口的索引表不占用任何字节。

  在接口相关描述信息之后的,是字段表集合,用于描述类或者接口中申明的变量(注:此处的变量是指类或者接口级变量,即类变量或者实例级变量,而不包括方法中的局部变量)。这些字段通常包含哪些信息呢?通常有:字段访问域(private、public、protected等)+是实例变量还是类变量(Static)+是否可修改(final)+并发可见性(volatitle,用volatile修饰的变量,线程在每次使用变量的时候,都会读取变量修改后的最终的值)+可否序列化(transient)+字段类型(基本类型、对象、数组)+字段名称。在JVM中,字段表结构如下:

类型     名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

  我们来看,字段表集合的入口u2类型数据项是字段表的容量计数器为JVM中class文件探索与解析(0x0001),表示只有一个字段项。紧接着字段表容量计数器的u2类型的数据为0x0002,参考下表,表示该字段为private。字段名称JVM中class文件探索与解析为0x0007,其值为“m”,描述信息0x0008,其值为“I”,可以推断,原代码的定义字段为:“private int m”;

标志名称 标志值 含义
ACC_PUBLIC 0x00 01 字段是否为public
ACC_PRIVATE 0x00 02 字段是否为private
ACC_PROTECTED 0x00 04 字段是否为protected
ACC_STATIC 0x00 08 字段是否为static
ACC_FINAL 0x00 10 字段是否为final
ACC_VOLATILE 0x00 40 字段是否为volatile
ACC_TRANSTENT 0x00 80 字段是否为transient
ACC_SYNCHETIC 0x10 00 字段是否为由编译器自动产生
ACC_ENUM 0x40 00 字段是否为enum

  通常而言,在字段描述之后还有一些属性表信息存储额外的信息,在以上class文件中,属性计数器位0x0000,表示没有额外的属性信息。

  在字段表之后的是方法表集合,其表示方法与字段信息表几乎一致。其结构如下表:

类型     名称 数量
u2 access_flags 1
u2 name_index 1
u2 descriptor_index 1
u2 attributes_count 1
attribute_info attributes attributes_count

  但是,方法表的修饰属性比字段表要多,例如:方法有abstract、synchronize等。在方法表的入口,同样也有一个方法容量计数器,占用u2字节。在上述class中,方法的计数器为JVM中class文件探索与解析,即0x0003表示有三个方法。由上图可以看出,方法的前4个u2位分别为:访问标志、方法名索引、描述索引和属性数量。接在属性数量之后的,即为属性表集合

  我们来分析一下,第一个方法,function#1的第一、二、三、四u2数据项分别为:0x0001、0x0009、0x000A、0x0001,代表方法为public、方法名指向const#9("<init>")、方法描述为const#10(“()V”)、含有一个属性。然后我们看一下属性表,

  属性表的头两位为属性名称索引,即0x000B(二进制11)该属性指向const#11(“Code”)属性,java呈现方法体重的代码经过javac编译之后,就存储在Code属性里。


JVM中class文件探索与解析

  (根据上表结构)接着的4个u2类型为属性长度,即JVM中class文件探索与解析,(换算成二进制47),表示属性长度为47。下图标红部分,即为该方法中属性的长度

JVM中class文件探索与解析

 

  表示属性即为之后的47个u1字节,

在属性长度之后的,两个u2类型的项分别是:操作数栈的最大深度局部变量表存储的空间(最小单位为Slot)。对应class文件中的均为:0x0001,表示操作数栈的最大深度和局部变量表存储的空间均为1。之后,有一个u4类型的项0x00000005(二进制为5),表示字节码长度为5。在字节码长度之后紧跟的便是字节码,该项的长度与字节码的长度(code_length)完全一致,上述class中字节码长度为5个u2类型的项,对应0x2A 0xB7 0x00 0x01 0xB1,这里解释下以上字节码的含义。(关于字节码指令可以查看字节码指令表,由于项太多就不一一列举了)。

  0x2A——对应指令aload_0,意思是将第0个Slot中为reference类型的本地变量推送到操作数栈顶。

  0xB7——对应指令inbokespecial,意思是将以栈顶的reference类型的数据所指向的对象作为接收者,调用该类型的实例构造方法。

  0x00 01——对应指令invokespecial的参数,差的常量池0x0001对应的const#1对应的<init>方法的符号引用。

  0xB1——指令为return,意识是返回此方法,并且返回值为void;当次行命令执行之后,方法执行结束。

  在方法字节码之后,便是异常表exceptions_table。在异常表的入口,有一项u2类型的数据,代表异常表的长度,对应上述class文件中的0x0000 ,表示该Code中没有异常表,

  接在Code的异常表之后的u2项表示属性表,上述class中为0x0002(换算成十进制=2),表示该方法的Code属性中有两项Code属性,它是class文件中最重要的一个属性。如果把java代码分为方法体中的代码和元数据两部分,Code属性用于秒速代码,所有其他属性用于描述元数据。我们来看第一个Code属性,入口u2项为0x000C(换算成十进制的12),我们查看常量池中const#12代表LineNumberTable。这个属性是用于描述java源码和字节码行号之间的对应关系。若没有改项,程序编译不会出错,但是在抛出异常时,无法知道异常所在的行号。接着分析,我们来看一下LineNumberTable属性的结构:

JVM中class文件探索与解析根据上表,我们看一下接下来的u4项为:0x00000006,表示该属性的长度为6,即0x00 0x01 0x00 0x00 0x00 0x04,接下来的u2项0x0001表示有一个类型为line_number_info的集合。line_number_info为startpx和line_number两个u2类型的项,前者表示字节码起始行号,后者表示java行号,一一对应。分别对应class文件中的0x0000 和0x0004,表示字节码中0行和java代码中4行对应。(PS前三行为注释,字节码不翻译,所以对应关系是正确的),所里这里便是Code的第一个属性——LineNumberTable属性。

在之前的分析中,Code应该包含两个属性。我们接着来看看Code的第二个属性,我们看下第二个属性的入口u2项,0x000D(对应十进制的13),还是跟之前一样,查询常量池中const#13对应的项,我们可以看到便是LocalVariableTable属性。这个属性作用是什么呢?它是用于描述栈帧中局部变量表中的变量与java源码中定义变量之间的关系的。我们来看一下它的属性结构,如下图:

JVM中class文件探索与解析

  前面的u2和u4项分别代表其属性对应的索引和属性字节长度。索引为13,长度为0x0000000C,表示字节码有12个u1项。即为图中标出的12项,

JVM中class文件探索与解析

  在这12项中,头u2项为0x0001,表示local_variable_info_table的长度为1,我们看一下local_variable_info_table的结构,如下图

JVM中class文件探索与解析

  其中,start_pc和length分别代表了这个局部变量的生命周期字节码起始位置和其覆盖范围长度,对应的起始位置字节偏移量和覆盖长度分别为0x0000 和0x0005;name_index和descriptor_index都是指向常量池中的索引,代表了这个局部变量名称和其描述符,对应的是0x000E(14,对应常量池中的this)和0x000F(15,对应常量池中的LTestClass)。index表示这个局部变量在栈帧中的Slot位置,0x0000表示从占用index=0的Slot位置,这与之前this占用index=0的理论一致。

  好了,到目前位置,我们已经分析过class文件中的一个方法表了,由于方法表结构复杂,剩下的两个就不一一分析了,下面我把另外两个方法所占用的字节给大家标注一下,大家有兴趣的可以自己研究、自行分析下。

JVM中class文件探索与解析

上图中,以垂直分割线分开的即为一个方法描述,可以自行分析下。通过javap命令生成的以上方法截图如下:

JVM中class文件探索与解析

 

接着方法表后面的,即为class文件SourceFile属性。其结构如下图:

JVM中class文件探索与解析

 

入口0x0001,表示有一个SourceFile属性,接下来的u2、u4和u2项(0x0018、0x00000002、0x0019)转换成十进制为(24,2,25),对应着属性名索引、属性长度和sourcefile的索引,即为SourceFile TestClass.java,完全与java代码一致。

自此,关于class的文件解析工作就结束了,文中有不对的地方,欢迎各位大神批评指正。(文中结构表均为粘贴,如若转发,请标明出处!)