分析.class字节码文件(上):常量池结构
目录
前面日志说到,Java程序中的类编译后会生成对应的.class字节码文件,里面的数据都是二进制流存储,一个字节码文件由很多部分组成,例如魔数,版本号,常量池,访问权限和接口列表等等,这些数据项对应的字节流都是按照顺序存储的,例如.class文件开头的四个字节一定是魔数,它用来标识该文件是字节码文件,JVM在加载.class文件时都会先检查前面这四个字节,如果发现不是固定的魔数,那么会拒绝加载。平时想要查看字节码内容,可以用javap -verbose命令,它可以分析出字节码文件中例如版本号,常量池和类的构造函数等信息:
可以看到,版本号53,对应的即使用JDK 9编译,常量池,里面保存了很多索引。下面用十六进制打开一个Java程序的编译后得到的字节码文件,来详细分析一下里面的每一部分。
十六进制字节码
字节码用二进制数据流的方式存储,想用十六进制方式打开,我使用的是WinHex,打开编译后的.class文件:
魔数
首先来看前面四个字节的数据,CAFEBABE,正是我们前面所说的,标识.class字节码文件的魔数:
魔数除了在JVM中有体现外,在其他地方,例如可执行文件ELF文件中也会用到魔数,它会检查这个magic number和预期的是否一样,如果不同,则表明该文件已损坏。还有在缓冲数组中放一个魔数来检测缓冲区溢出等。在JVM中只是作为一个身份标识符,表明该文件是一个字节码文件,如果魔数不是预期的固定值,JVM就会拒绝加载。
版本号
接着是两个字节的版本号,分为次版本号Minor Version和主版本号Major Version,JDK更新会为Java带来一些新的特性,不同版本的JDK对应不同的class版本号:
JDK版本 |
1.6 |
1.7 |
1.8 |
9 |
Class版本 |
50 |
51 |
52 |
53 |
对于高版本的JVM可以兼容低版本的Class,如果用低版本的JDK来编译Java程序,得到的.class字节码文件可以被叫高版本的JRE执行,反之,如果JRE版本较低,JDK版本较高,编译得到的字节码文件则无法向上兼容,被低版本的JRE执行。
从字节码文件的第5到第8字节的值,换算成十进制就可以得到版本号,如上图所示,前面两个字节00 00是次版本号,后面两个字节00 35是主版本号,十六进制0x35,即0011 0101,换算成十进制是53,.class字节码文件53对应的JDK版本号是JDK 9,表明该程序是由JDK 9编译的。
常量池结构
常量池是.class字节码文件中极其重要的一部分,从上面javap –verbose命令分析出的字节码文件中可以看到,Java类中定义的各种变量和方法都存放在常量池里面。常量池的结构分为常量池长度和常量池数组,首先来看常量池长度,它标识的是常量池中元素的总数量:
字节码文件中的第9到10字节数据描述的就是常量池长度,十六进制0x5B换算成十进制就是91,表明有91个元素,但是注意,JVM中不使用第0个元素,所以其实常量池里一共有90个元素。
到常量池数组,用来存放Java类方法或变量元素,每一个元素在数组中都有一个标志位tag,类型为u1类型,也就是占一个字节,JVM根据标志位的值来判断该元素是什么类型,例如整形元素的u1值为3,浮点型元素u1值为4,字符串为8,Java类中的方法符号引用元素,u1的值为10等。
常量池元素
接着往下走,常量池数量在字节码文件的第9-10个字节处,那么第11个字节就是常量池数组里的元素了,前面说到常量池元素前都会有一个字节的标志位tag,如下图:
第11个字节0x07对应的十进制是7,标志位7代表该元素是Java类或接口的引用,类或接口的引用元素结构除了一个字节的tag外还有两个字节的index,也就是第12-13字节0x00,0x02,所以从第14个字节开始,0x01表示的是第二个常量池元素的类型,tag等于1,表示这个元素是UTF-8编码的字符串元素,这种类型的元素结构除了一个字节的tag外,还有两个字节的length,和大小为length字节的bytes(假设length为2,则bytes所占字节为2):
从上图我们可以看到,tag位0x01后两个字节为0x00和0x0B,转换对应的十进制等于11,也就是后面11个字节的数据,从0x68到0x6F,都是字符串元素。
父类常量元素
接着往下走
下一个元素的标志位是07,上面说过该标志标识的是Java类或接口的信息,在这里表示的是父类常量元素,0x07后面两个字节0x00和0x04表示的是index,之后的0x01才是下一个元素的标志位tag,0x01,又是一个字符串元素,跟在标志位后面的两个字节标识长度,0x10转换十进制等于16,后面16个字节从0x6A到0x74,都是bytes数据,对应的字符串正好是java/lang/Object,为什么说它是父类常量元素呢?因为这个Java类没有去显示继承其他类,这样的类是初始化默认继承Java.lang.Object类的,它的父类就是Java.lang.Object类。
变量型常量元素
接着往下走,我们就可以看到Java类中的变量元素,这些变量包括成员变量和类变量:
第一个元素,01 00 01 61,标识其是一个UTF-8编码的字符串,长度为1,数据61对应的是UTF-8编码的字符a,说明Java类中有一个变量a,它是什么类型的变量呢?往下看,01 00 13,标识的是长度为19的字符串,0x13后面19个字节数据都表示的是该元素,从0x4C到0x3B,每两个十六进制数据对应一个ASCII字符,表示的字符串就是“Ljava/util/Scanner;”,原来变量a是一个Scanner对象,同样的我们可以找到其他如Ljava/util/String类型的变量信息。