这两天在看《深入理解Java虚拟机》,其中字节码那部分看得我极其烦躁,最后决定亲子拆解一遍它的例子,拆完后觉得印象深刻多了。
字节码拆解实验
对书上的例子进行了实现和分析,几个部分需要对照,相互印证,同时结合下面的理论部分才能明白。
Java代码
例子使用的Java代码和书上基本一样,只不过我写的时候包名不一样,代码如下:
package com.way.clazz;
public class TestClass {
private int m;
public int inc(){
return m+1;
}
}
拆解图示
不同的颜色的划线区域代表不同的字节码组成部分。下面的知识部分和图是一一对应的。
.class字节码
为了清洗,把原生的字节码放在这里
javap命令解析出来的结构:
Javap命令解析出的文件结构有助于对照验证。
Classfile /root/TestClass.class
Last modified Mar 8, 2017; size 289 bytes
MD5 checksum 796893667dc8e7d29bbece83875f670d
Compiled from "TestClass.java"
public class com.way.clazz.TestClass
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // com/way/clazz/TestClass.m:I
#3 = Class #17 // com/way/clazz/TestClass
#4 = Class #18 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 inc
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 TestClass.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // m:I
#17 = Utf8 com/way/clazz/TestClass
#18 = Utf8 java/lang/Object
{
public com.way.clazz.TestClass();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
public int inc();
descriptor: ()I
flags: ACC_PUBLIC
Code:
stack=2, locals=1, args_size=1
0: aload_0
1: getfield #2 // Field m:I
4: iconst_1
5: iadd
6: ireturn
LineNumberTable:
line 8: 0
}
SourceFile: "TestClass.java"
字节码理论
基本结构
Class文件的结构不像XML等描述语言那样松散自由。由于它没有任何分隔符号,所以,以上数据项无论是顺序还是数量都是被严格限定的。哪个字节代表什么含义,长度是多少,先后顺序如何,都不允许改变。
仔细观察上面的Class文件格式,可以看出Class文件格式采用一种类似于C语言结构体的伪结构来存储,这种伪结构中只有两种数据类型:无符号数和表。无符号数就是u1、u2、u4、u8来分别代表1个、2个、4个、8个字节。表是由多个无符号数或其他表构成的复合数据类型,以“_info”结尾。在表开始位置,通常会使用一个前置的容量计数器,因为表通常要描述数量不定的多个数据。
下图表示的就是Class文件格式的基本结构,所有_info就是表结构,它们的长度是不确定的。每个_info表的前两个字节会表示_info表的元素个数,根据元素个数,和每个元素的类型,可以确定最终长度。
Header头部
对应上面拆解图的灰色部分,长度固定,共8个字节:
1,MagicNum:4个字节,内容0xCAFEBABE,固定值
2,minor_version:2字节,JDK小版本号
3,major_version:2字节,JDK大版本号,决定着JVM能否处理该class文件,向下兼容。JDK1.8的主版本号是52
Constant Pool
常量池是一个表结构,表中元素可以是两种东西:字面量和符号引用
字面量:类似于Java语言层面所说的常量,如本文字符串,被声明为final的常量值。
符号引用:类和接口的全限定名,字段的名称和描述符,方法的名称和描述符。
表中元素即一个常量的结构如下图:
以一个变量为例解释该表格的意义:
上图是常量池区域的开头部分,作为一个表结构,前两的字节必然是表元素数目,即0x08,0x09,存储的是0x0013,十进制是19,这里是从1开始计数,即存储了18个元素。接下来就是元素的具体内容。每一个元素的第一个字节都是类型的标识数字,如上表,标识数字是1~12,没有2,11种。第一个元素的头(0x10)值是0x0A,即10,查表,是CONSTANT_Methodref_info, 存储的是两个index,index即指针,索引,指向的已一定是常量池中的其他元素位置,这也就是所谓符号索引。CONSTANT_Methodref_info存储的是:指向声明方法接口描述符CONSTANT_class_info的索引;指向名称及类型描述符CONSTANT_NameAndType_info的索引。
看字节码,两个位置分别存储的是0x0004,0x000F,让我们顺藤摸瓜,看看到底是什么意思?0x0004指向常量表中第四号元素,0x000F指向第十五号元素。看解析内容,四号元素是CONSTANT_class_info类型,存储了指向十八号元素的索引,十八号元素是常量,存储的是“java/lang/Object”。再看十五号元素,十五号元素是CONSTANT_NameAndType_info,存储了一个名称索引和一个描述符索引,分别指向7号和8号,7号是,这是一个特殊标记,表示这个方法是构造器,8号是V,标识void,即空。
至此整个脉络搞清楚了,1号位存储的是一个方法的说明,这个方法返回值的Class存在了4号位,四号位指向了真是存储的常量字符串“java.lang.Object”,这个方法的名称和传入参数存在了15号位,15号位又分别指向了7号,和8号位,标识这个方法是构造器,传入参数为空。到这里我们就明白,这是类的共有构造函数的编译结果,即public com.way.clazz.TestClass(){}
,这个方法我们没有写,但是隐式存在的。
遵从以上方法,可以查询所有元素的含义,javap命令也就是做了这样的事情,并将结果以可理解的形式展示给我们。
常量池字节码的解析内容:
Constant pool:
#1 = Methodref #4.#15 // java/lang/Object."<init>":()V
#2 = Fieldref #3.#16 // com/way/clazz/TestClass.m:I
#3 = Class #17 // com/way/clazz/TestClass
#4 = Class #18 // java/lang/Object
#5 = Utf8 m
#6 = Utf8 I
#7 = Utf8 <init>
#8 = Utf8 ()V
#9 = Utf8 Code
#10 = Utf8 LineNumberTable
#11 = Utf8 inc
#12 = Utf8 ()I
#13 = Utf8 SourceFile
#14 = Utf8 TestClass.java
#15 = NameAndType #7:#8 // "<init>":()V
#16 = NameAndType #5:#6 // m:I
#17 = Utf8 com/way/clazz/TestClass
#18 = Utf8 java/lang/Object
后面的访问标志,域,方法,类属性部分都可以根据以上方法一一拆解。将每个区域的数据结构列在下面
访问标签Access Right
字节码长度为u2,数值是上表中所有为真的标志取“或”运算。例如我们这里是ACC_PUBLIC,ACC_SUPER的,那么就是(0x0001|0x0020)得到0x0021。
索引 Inplement Interfaces
包含类索引,父索引和其它接口索引集合。类索引即对本类的索引,父索引即对继承父类的索引,如果没有继承类,就是java.lang.Object类,所有类都继承自它,它们都最终指向常量池中的值。接口索引是一个表,前两个字节是长度,后面的是对应个数的接口索引,本例没有其他接口。
字段区Field
字段区是表,前两字节是元素个数,之后的每一个元素遵从上表数据结构,attributes_info会连接在元素后面,其他是对常量池的索引。
其中Access_flag就是变量访问修饰符
Descriptor_index指的是数据的类型
方法区Method
方法区和字段区几乎一样
方法访问标识略有不同
属性表
上面的几个部分勾勒了一个类的框架,但一个字段和方法的具体内容则是在属性表里的。因此属性表是灵活和扩展性很强的。
虚拟机规定的属性类别如下:
本例中方法有code属性:
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 3: 0
对应的字节码是:
根据Code属性数据结构,可以一一对应解读。
Java字节码是JVM运行的的运行依据,JVM在字节码上采取了开放的策略,这也就是为什么有这么多的建立在JVM的语言,如Jython,Scala等。因为只要有相应的编译器将它们的语言文件编译成.class字节码,JVM都可以运行。而这些字节码的结构为什么如此与编译原理以及JVM类加载机制有关。而类加载机制是《深入理解JVM》的下一章节。