Java字节码结构
一 java字节码的加载
1 java类加载机制
jdk对于字节码的加载是使用了双亲委派的模型;
即某个特定的类加载器在接收到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回,只有父类加载器无法完成加载任务时,才自己去加载
图片引至:JDK虚拟机模型
jdk中常见的类加载器:
类加载器名称 | 类加载范围 |
---|---|
启动类加载器Bootstrap ClassLoader | 存放在<JAVA_HOME>\lib目录中 |
扩展类加载器Extension ClassLoader | 存放在<JAVA_HOME>\lib\ext目录中的所有类库,开发者可以直接使用 |
应用程序加载器Application ClassLoader | 加载用户类路径上的指定的类库,开发者可以直接使用,一般情况下这个就是程序中的默认的类加载器 |
2 java类加载步骤
加载:
虚拟机需要完成以下三步
- 1,根据加载类的全限定名来获取此定义类的二进制字节码(并没有指明要从一个Class文件中获取,可以从其他渠道,譬如:网络、动态生成、数据库等)
- 2,将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构(这个JDK1.8应该是元空间Metaspace)
- 3,在内存中生成一个代表这个类的Class对象,作为方法区这个类的各种数据的访问入口;
验证:
验证是连接阶段的第一步,这一阶段的目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全
准备:
准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,
这个时候进行内存分配的仅包括类变量(被static修饰的变量),而不包括实例变量
例如:
public static int value = 123
那变量value在准备阶段过后的初始化值为0不是123,因为这时候尚未开始执行任何java方法,而把value赋值为123的putstatic指令是程序被编译后,存放于类构造器()方法之中,所以把value赋值为123的动作将在初始化阶段才会执行。
public static final int value = 123
至于“特殊情况”是指:public static final int value=123,即当类字段的字段标注为final之后,value的值在准备阶段初始化为123而非0.
总结:final是在准备阶段时就赋值了,static准备阶段时数据是零值,在初始化阶段才会赋值。
解析:
解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。
初始化:
类初始化阶段是类加载过程的最后一步,到了初始化阶段,才真正开始执行类中定义的java程序代码。
什么时候触发初始化的操作?
- 1,创建类的示例,也就是new一个对象
- 2,访问某个类或者接口的静态变量,或者对该类的静态变量进行赋值
- 3,调用类的静态方法,
- 4,反射 Class.forName("")
- 5,初始化一个子类(会首先初始化对应的父类数据)
- 6,JVM启动时标明的启动类,即文件名和类名相同的那个类
二 字节码结构
内容引至:美团技术团队,字节码增强技术探索
.java文件通过javac编译后将得到一个.class文件,比如编写一个简单的ByteCodeDemo类,如下图的左侧部分:
编译后生成ByteCodeDemo.class文件,打开后是一堆十六进制数,按字节为单位进行分割后展示如上图右侧部分所示。JVM对于字节码是有规范要求的,那么看似杂乱的十六进制符合什么结构呢?JVM规范要求每一个字节码文件都要由十部分按照固定的顺序组成,整体结构如下所示。接下来我们将一一介绍这十部分:
(1) 魔数(Magic Number)
所有的.class文件的前四个字节都是魔数,魔数的固定值为:0xCAFEBABE。魔数放在文件开头,JVM可以根据文件的开头来判断这个文件是否可能是一个.class文件,如果是,才会继续进行之后的操作。
(2) 版本号
版本号为魔数之后的4个字节,前两个字节表示次版本号(Minor Version),后两个字节表示主版本号(Major Version)。上图2中版本号为“00 00 00 34”,次版本号转化为十进制为0,主版本号转化为十进制为52,在Oracle官网中查询序号52对应的主版本号为1.8,所以编译该文件的Java版本号为1.8.0。
(3) 常量池(Constant Pool)
紧接着主版本号之后的字节为常量池入口。常量池中存储两类常量:字面量与符号引用。字面量为代码中声明为Final的常量值,符号引用如类和接口的全局限定名、字段的名称和描述符、方法的名称和描述符。常量池整体上分为两部分:常量池计数器以及常量池数据区,如下图所示,常量池数据区域的字节码组成部分。
- 常量池计数器(constant_pool_count):由于常量的数量不固定,所以需要先放置两个字节来表示常量池容量计数值。图2中示例代码的字节码前10个字节如下图5所示,将十六进制的24转化为十进制值为36,排除掉下标“0”,也就是说,这个类文件*有35个常量。
- 常量池数据区:数据区是由(constant_pool_count-1)个cp_info结构组成,一个cp_info结构对应一个常量。在字节码*有14种类型的cp_info(如下图),每种类型的结构都是固定的。
常量池常量对应字节码定义格式:
具体以CONSTANT_utf8_info为例,它的结构如下图7左侧所示。首先一个字节“tag”,它的值取自上图6中对应项的Tag,由于它的类型是utf8_info,所以值为“01”。接下来两个字节标识该字符串的长度Length,然后Length个字节为这个字符串具体的值。从图2中的字节码摘取一个cp_info结构,如下图右侧所示。将它翻译过来后,其含义为:该常量类型为utf8字符串,长度为一字节,数据为“a”。
ByteCodeDemo前15个字节的分析
同样,你也可以通过javap -verbose ByteCodeDemo命令,查看JVM反编译后的完整字节码常量池图
第一个 #1 代表对应得当前常量池在当前表对应得索引
第二个标识对应的常量池的常量定义
第三个和第四个参数标识对应的常量的索引
第五个参数表示对应常量的内容描述
(4) 访问标志
常量池结束之后的两个字节,描述该Class是类还是接口,以及是否被Public、Abstract、Final等修饰符修饰。JVM规范规定了如下图9的访问标志(Access_Flag)。需要注意的是,JVM并没有穷举所有的访问标志,而是使用按位或操作来进行描述的,比如某个类的修饰符为Public Final,则对应的访问修饰符的值为ACC_PUBLIC | ACC_FINAL,即0x0001 | 0x0010=0x0011。
下图为访问标志对应标志值的对应:
(5) 当前类名
访问标志后的两个字节,描述的是当前类的全限定名。这两个字节保存的值为常量池中的索引值,根据索引值就能在常量池中找到这个类的全限定名。
(6) 父类名称
当前类名后的两个字节,描述父类的全限定名,同上,保存的也是常量池中的索引值。
(7) 接口信息
父类名称后为两字节的接口计数器,描述了该类或父类实现的接口数量。紧接着的n个字节是所有接口名称的字符串常量的索引值。
(8) 字段表
字段表用于描述类和接口中声明的变量,包含类级别的变量以及实例变量,但是不包含方法内部声明的局部变量。字段表也分为两部分,第一部分为两个字节,描述字段个数;第二部分是每个字段的详细信息fields_info。字段表结构如下图所示:
以上图中字节码的字段表为例,如下图所示。其中字段的访问标志查(4)中的访问标识图例可知,0002对应为Private。通过常量池索引下标在中常量池分别得到字段名为“a”,描述符为“I”(代表int)。综上,就可以唯一确定出一个类中声明的变量private int a。
(9)方法表
字段表结束后为方法表,方法表也是由两部分组成,第一部分为两个字节描述方法的个数;第二部分为每个方法的详细信息。方法的详细信息较为复杂,包括方法的访问标志、方法名、方法的描述符以及方法的属性,如下图所示:
方法的权限修饰符依然可以通过上图的访问标志对应标志值得对应表查询得到,方法名和方法的描述符都是常量池中的索引值,可以通过索引值在常量池中找到。而“方法的属性”这一部分较为复杂,直接借助javap -verbose将其反编译为人可以读懂的信息进行解读,如图13所示。可以看到属性中包括以下三个部分: - “Code区”:源代码对应的JVM指令操作码,在进行字节码增强时重点操作的就是“Code区”这一部分。
- “LineNumberTable”:行号表,将Code区的操作码和源代码中的行号对应,Debug时会起到作用(源代码走一行,需要走多少个JVM指令操作码)。
- “LocalVariableTable”:本地变量表,包含This和局部变量,之所以可以在每一个方法内部都可以调用This,是因为JVM将This作为每一个方法的第一个参数隐式进行传入。当然,这是针对非Static方法而言。
(10)附加属性表
字节码的最后一部分,该项存放了在该文件中类或接口所定义属性的基本信息。