本文图片以及部分内容来自Java字节码增强探秘。
Java字节码的介绍
字节码基础
.java文件通过javac编译后将得到一个.class文件,如下图所示,class文件中都是16进制数。
java字节码主要包括以下几部分。
(1)魔数,魔数固定的值是CAFEBABY,占用四个字节,用此来判断该文件是否可以被虚拟机所接收。我理解在类加载过程中,校验的第一步格式校验就是去校验魔数。
补充:为什么CA占一个字节呢,因为一个字节是8位01,而这个是16进制的,最大可以到15,15就是F,也就是1111,所以一个16进制数需要4位01来存储,因此两个16进制数就是一字节。
(2)版本数:紧接着魔数的后4位数就是版本数,虚拟机要求不能执行超过其版本号的class文件。
(3)常量池:常量池中存储的是字面量与符号引用。由于存储的数量不定,因此需要两字节来存储数量。常量池中存储与常规不同,它的存储是以1开始的,因此比如值为0x0016,对应22。常量池中有21个常量,其下标为1-21。
(4)访问标志:之后的两个字节表示访问标志,包括: 这个Class是类还是接口; 是否定义为public类型; 是否定义为abstract类型; 如果是类的话, 是否被声明为final等。
(5) 当前类名:访问标志后的两个字节,描述的是当前类的全限定名。这两个字节保存的值为常量池中的索引值,根据索引值就能在常量池中找到这个类的全限定名。
(6) 父类名称:当前类名后的两个字节,描述父类的全限定名,同上,保存的也是常量池中的索引值。
(7) 接口信息:父类名称后为两字节的接口计数器,描述了该类或父类实现的接口数量。紧接着的n个字节是所有接口名称的字符串常量的索引值。
(8) 字段表:字段表( field_info) 用于描述接口或者类中声明的变量。
字段可以包含字段的作用域(public、private、protected修饰符)、是实例变量还是类变量(static修饰符)、可变性(final)、并发可见性(volatile修饰符,是否强制从主内存读写)、可否被序列化(transient修饰符)、字段数据类型(基本类型、对象、数组)、字段名称。
描述符的作用是用来描述字段的数据类型、 方法的参数列表( 包括数量、 类型以及顺序) 和返回值。
对应数组的话,int[]
会被描述为[I
,而java.lang.String[][]
被描述为[[java.lang.String
。
(9)方法表集合:方法表的结构如同字段表一样, 依次包括了访问标志( access_flags) 、 名称索引( name_index) 、 描述符索引( descriptor_index) 、 属性表集合( attributes) 几项。
字节码指令
我们以这个例子为例。
1 package com.company.niuke;
2
3 public class jvmtest {
4 public static void main(String[]args){
5 int a = 1 ;
6 int b = 2 ;
7 int c = a + b ;
8 }
9}
10
对应的class文件如下,
public static main([Ljava/lang/String;)V
L0
LINENUMBER 5 L0
ICONST_1
ISTORE 1
L1
LINENUMBER 6 L1
ICONST_2
ISTORE 2
L2
LINENUMBER 7 L2
ILOAD 1
ILOAD 2
IADD
ISTORE 3
L3
LINENUMBER 8 L3
RETURN
L4
LOCALVARIABLE args [Ljava/lang/String; L0 L4 0
LOCALVARIABLE a I L1 L4 1
LOCALVARIABLE b I L2 L4 2
LOCALVARIABLE c I L3 L4 3
MAXSTACK = 2
MAXLOCALS = 4
可以看到LINENUMBER代表当前对应的行。
第五行的int a = 1
其实对应两个字节码:(1)iconst 1:将整形常量1放入操作数栈。(2)istore 1:在索引为1的位置将第一个操作数出栈(一个int值)并且将其存进本地变量,相当于变量a。
第六行与第五行相似。
而第七行则涉及把a和b从本地变量中取出,放入操作数栈中ILOAD 1
。
iadd
:把操作数栈中的前两个int值出栈并相加,将相加的结果放入操作数栈。
ISTORE 3
:将操作数栈中的3从栈中取出并存入本地变量。