写在前面
java字节码由单字节的指令(也叫做操作码)
组成,但一个 byte 最多能够存储 256 个指令,够用吗?截止到目前是够的,因为指令的个数是200多一点,指令分为如下四类:
1:栈操作指令(load store)
2:流程控制指令(goto )
3:对象操作指令(new invokestatic)
4:算数运算,类型转换指令(iadd i2d)
接下来我们来通过一些字节码分析的例子,来实际的了解下各种指令,以及其作用。在正式开始之前我们先来看下方法栈,栈帧,局部变量表等相关概念。
JVM会为每个线程分配一个独属于自己的线程栈,如下图:
接着当线程执行一个方法的时候,则会为该方法生成一个栈帧,并将栈帧压入线程栈,假定有如下代码:
void A() {B();
}
void B() {C();
}
则会分别生成A的栈帧,B的栈帧,C的栈帧,并压入线程栈,如下图:
栈帧中包含如下信息:
局部变量表:存储方法内部的局部变量
操作数栈:存储程序当前运行状态下需要用到的变量
Class引用:方法所属Class在常量池的引用
即如下:
这里局部变量表中的信息,其实就是我们在方法中定义的局部变量,比如如下代码:
package javadeveloper.mkmoney;public class TestByte {public static void main(String[] args) {int a1 = 20;String a2 = "uuuu"; }
}
则局部变量表信息可通过如下方式(注意编译时需要增加-g)
查看:
bogon:temp xb$ javac -g -d . TestByte.java
bogon:temp xb$ javap -c -verbose javadeveloper/mkmoney/TestByte.class
...public static void main(java.lang.String[]);...LocalVariableTable:Start Length Slot Name Signature0 7 0 args [Ljava/lang/String;3 4 1 a1 I6 1 2 a2 Ljava/lang/String;
}
可以看到主函数main局部变量表中有3个变量,第一个是主函数的入参String数组,第二个和第三个使我们在代码中定义的局部变量a1和a2。
最后因为后续分析还需要用到常量池,我们也来看下,常量池保存的是我们在程序中定义的常量,以及在程序运行过程中需要用到的类权限定名称信息等,比如定义如下代码:
package javadeveloper.mkmoney;public class TestByte {final static int c1 = 900;final static String sportName = "羽毛球";
}
查看常量池:
bogon:temp xb$ javap -c -verbose javadeveloper/mkmoney/TestByte.class | grep 'Constant pool' -A 30
Constant pool:#1 = Methodref #3.#17 // java/lang/Object."<init>":()V#2 = Class #18 // javadeveloper/mkmoney/TestByte#3 = Class #19 // java/lang/Object#4 = Utf8 c1#5 = Utf8 I#6 = Utf8 ConstantValue#7 = Integer 900#8 = Utf8 sportName#9 = Utf8 Ljava/lang/String;#10 = String #20 // 羽毛球#11 = Utf8 <init>#12 = Utf8 ()V#13 = Utf8 Code#14 = Utf8 LineNumberTable#15 = Utf8 SourceFile#16 = Utf8 TestByte.java#17 = NameAndType #11:#12 // "<init>":()V#18 = Utf8 javadeveloper/mkmoney/TestByte#19 = Utf8 java/lang/Object#20 = Utf8 羽毛球
我们只来分析和我们定义的两个常量c1
和sportName
相关的部分:
常量c1分析:#4 = Utf8 c1 --》常量名称c1#7 = Integer 900 --》常量值是900
常量sportName分析:#8 = Utf8 sportName --》常量名称sportName#10 = String #20 // 羽毛球 -》String类型的值,在常量池20的位置#20 = Utf8 羽毛球 --》常量值是羽毛球
1:简单的java类字节码分析
假定我们有如下的java源码:
package javadeveloper.mkmoney;public class TestByte {public static void main(String[] args) {TestByte testByte = new TestByte();}
}
首先我们来将源码通过javac编译为class字节码文件,如下:
bogon:temp xb$ javac -d . TestByte.java
bogon:temp xb$ tree
.
|____javadeveloper
| |____mkmoney
| | |____TestByte.class
|____.DS_Store
|____TestByte.java
接下来我们通过javap查看字节码信息:
bogon:temp xb$ javap -c javadeveloper/mkmoney/TestByte.class
Compiled from "TestByte.java"
public class javadeveloper.mkmoney.TestByte {public javadeveloper.mkmoney.TestByte();Code:0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: returnpublic static void main(java.lang.String[]);Code:0: new #2 // class javadeveloper/mkmoney/TestByte3: dup4: invokespecial #3 // Method "<init>":()V7: astore_18: return
}
我们可以看到默认的构造函数TestByte()
以及我们定义的主函数main
,方法下面的第一列的整数是字节的偏移量,数字的右侧就是操作码+操作数(可能没有)
,接下来我们分别来看下。
1.1:构造函数TestByte()
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
首先第一行0: aload_0
,0
代表偏移量是0,即从0字节开始是该指令的内容,aload_0
中的a是代表一个对象,load是代表加载本地变量表中的变量到栈中,_0代表的是从本地变量表中加载变量的位置,因此,整个意思就是,从本地变量表的0位置加载一个对象类型的值压到栈中,如下图:
第二行1: invokespecial #1
代表偏移量是1,即从第一个字节开始是该指令的内容,invokespecial
代表是调用一个类实例方法,#1
代表是要调用的类实例方法信息在常量池的1位置。
第三行4: return
中4代表偏移量是4,即从第四个字节是该指令的信息(说明前一个指令占用了3个字节,这是因为invokespecial占用了2个字节的操作数,只不过这里只使用了字节存储了#1)
,return指令,代表方法结束,弹出栈帧。
1.2:主函数main
0: new #2 // class javadeveloper/mkmoney/TestByte
3: dup
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: return
第一行0: new #2
,指令的偏移量是0,new
指令代表是创建一个对象,#2
代表要创建的对象的信息在常量池2的位置。
第二行3: dup
,指令的偏移量是3,dup
指令是压栈,即将第一行指令创建的对象压到栈顶。
第三行4: invokespecial #3
,指令偏移量是4,调用常量池中位置为3的方法,即构造函数。
第四行7: astore_1
,指令偏移量是7(说明上个指令占用了3个字节,因为有2字节的的操作数)
,astore_1中的a
代表对象,store,代表存储到本地变量表中,_1
代表存储的局部变量表的位置是1,即将当前栈顶的引用类型元素出栈并存储到局部变量表的1位置,如下图:
第五行8: return
,指令偏移量是8,return指令,返回,栈帧弹出,方法结束。
2:局部变量表分析
首先我们准备两个类,如下:
package monkey.study;public class MovieAverage {private int count = 0;private double sum = 0.0D;public void submit(double value) {this.count++;this.sum += value;}public double getAvg() {if (0 == this.count) { return sum; }return this.sum / this.count;}
}
package monkey.study;public class LocalVariableTest {public static void main(String[] args) {MovieAverage ma = new MovieAverage();int num1 = 1;int num2 = 2;ma.submit(num1);ma.submit(num2);double avg = ma.getAvg();}
}
其中LocalVariableTest类重点分析,MovieAverage类作为辅助。
接着我们来编译(增加-g参数保留变量名称信息,方便我们阅读字节码)
,如下:
D:\test>javac -g -d . MovieAverage.java
D:\test>javac -g -d . LocalVariableTest.java
编译后结构如下:
D:\test>tree
Folder PATH listing for volume 新加卷
Volume serial number is 0023-BF0B
D:.
└───monkey└───study└───MovieAverage.class└───LocalVariableTest.class
接下来你通过javap命令查看LocalVariableTest字节码信息:
D:\test>javap -verbose -c monkey.study.LocalVariableTest
...public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=3, locals=6, args_size=10: new #2 // class monkey/study/MovieAverage3: dup4: invokespecial #3 // Method monkey/study/MovieAverage."<init>":()V7: astore_18: iconst_19: istore_210: iconst_211: istore_312: aload_113: iload_214: i2d15: invokevirtual #4 // Method monkey/study/MovieAverage.submit:(D)V18: aload_119: iload_320: i2d21: invokevirtual #4 // Method monkey/study/MovieAverage.submit:(D)V24: aload_125: invokevirtual #5 // Method monkey/study/MovieAverage.getAvg:()D28: dstore 430: returnLineNumberTable:line 5: 0line 6: 8line 7: 10line 8: 12line 9: 18line 10: 24line 11: 30LocalVariableTable:Start Length Slot Name Signature0 31 0 args [Ljava/lang/String;8 23 1 ma Lmonkey/study/MovieAverage;10 21 2 num1 I12 19 3 num2 I30 1 4 avg D
}
LocalVariableTable
就是局部局部变量表信息,其他源码对应关系如下图:
接着我们看下指令是如何同本地变量表发生关系的:
0: new #2 // class monkey/study/MovieAverage创建对象MovieAverage,并将其引用值压入栈顶
3: dup复制栈顶元素并压入栈顶,即创建MovieAverage对象应用被复制一份
4: invokespecial #3 // Method monkey/study/MovieAverage."<init>":()V操作数栈出栈,获取MovieAverage对象应用,并调用其构造函数
7: astore_1操作数栈出栈,获取MovieAverage对象应用(因为dup复制了一份,所以这里还有一份),并存储到本地变量表slot 1的位置,即变量ma,对应代码MovieAverage ma = new MovieAverage();
8: iconst_1将常量1压入栈帧操作数栈栈顶
9: istore_2将操作数栈栈顶元素出栈,并存储到本地变量表slot 2的位置,即变量num1,对应代码int num1 = 1;
10: iconst_2将常量1压入栈帧操作数栈栈顶
11: istore_3将操作数栈栈顶元素出栈,并存储到本地变量表slot 3的位置,即变量num2,对应代码int num2 = 2;
12: aload_1从局部变量表slot 1位置加载变量,即加载变量ma,即获取代码ma.submit(num1);中的ma
13: iload_2从局部变量表slot 2位置加载变量,即加载变量num1,即获取代码ma.submit(num1);中的num1
14: i2d弹出操作数栈栈顶元素,并装换为double,因为MovieAverage.submit方法的入参是double的,所以这里执行强转
15: invokevirtual #4 // Method monkey/study/MovieAverage.submit:(D)V弹出操作数栈栈顶元素,并执行其实例方法submit,使用上一个指令的结果作为参数
18: aload_1从局部变量表slot 1位置加载变量,即加载变量ma,即获取代码ma.submit(num2);中的ma
19: iload_3从局部变量表slot 2位置加载变量,即加载变量num2,即获取代码ma.submit(num2);中的num2
20: i2d弹出操作数栈栈顶元素,并装换为double,因为MovieAverage.submit方法的入参是double的,所以这里执行强转
21: invokevirtual #4 // Method monkey/study/MovieAverage.submit:(D)V弹出操作数栈栈顶元素,并执行其实例方法submit,使用上一个指令的结果作为参数
24: aload_1从局部变量表slot 1位置加载变量,即加载变量ma,即获取代码double avg = ma.getAvg();中的ma
25: invokevirtual #5 // Method monkey/study/MovieAverage.getAvg:()D弹出操作数栈栈顶元素,即ma,并执行其实例方法getAvg,并将操作结果压入操作数栈栈顶,该方法不需要参数
28: dstore 4将操作数栈栈顶的double类型数据出栈,并存入本地变量表slot 4的位置,即赋值给avg变量
30: return栈帧弹出线程栈,方法执行完毕
3:行号表分析
行号表用来存储字节码指令和源代码位置的对应关系,如下测试代码:
package monkey.study;public class LineNumTableTest {public static void main(String[] args) {int a = 1;int b = 2;}
}
接着编译:
D:\test>javac -g -d . LineNumTableTest.java
javap 查看字节码只保留LineNumberTable
:
D:\test>javap -c -verbose monkey.study.LineNumTableTest
Classfile /D:/test/monkey/study/LineNumTableTest.class...Code:stack=1, locals=3, args_size=10: iconst_11: istore_12: iconst_23: istore_24: returnLineNumberTable:line 5: 0line 6: 2line 7: 4LocalVariableTable:...
}
行号表line 5: 0
意思是指令码偏移量0位置对应的源码的行号是5,如下图:
其他类似,完整的如下图:
4:循环控制分析
测试代码如下:
package monkey.study;public class MovieAverage {private int count = 0;private double sum = 0.0D;public void submit(double value) {this.count++;this.sum += value;}public double getAvg() {if (0 == this.count) { return sum; }return this.sum / this.count;}
}
package monkey.study;public class ForLoopTest {private static int[] numbers = {1, 6, 8};public static void main(String[] args) {MovieAverage ma = new MovieAverage();for (int number : numbers) {ma.submit(number);}}
}
接下来编译:
D:\test>javac -d . MovieAverage.javaD:\test>javac -d . ForLoopTest.java
查看字节码如下:
D:\test>javap -c -verbose monkey/study/ForLoopTest
... public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=3, locals=6, args_size=10: new #2 // class monkey/study/MovieAverage3: dup4: invokespecial #3 // Method monkey/study/MovieAverage."<init>":()V7: astore_18: getstatic #4 // Field numbers:[I11: astore_212: aload_213: arraylength14: istore_315: iconst_016: istore 418: iload 420: iload_321: if_icmpge 4324: aload_225: iload 427: iaload28: istore 530: aload_131: iload 533: i2d34: invokevirtual #5 // Method monkey/study/MovieAverage.submit:(D)V37: iinc 4, 140: goto 1843: return
...
指令分析如下:
0: new #2 // class monkey/study/MovieAverage创建对象,类信息在常量池2位置,并将创建的对象压入栈帧的操作数栈栈顶
3: dup复制栈顶元素,并压入操作数栈栈顶
4: invokespecial #3 // Method monkey/study/MovieAverage."<init>":()V栈顶元素出栈,并调用其<init>方法
7: astore_1栈顶对象类型(a)元素出栈,并赋值到局部变量表slot 1位置的变量ma
8: getstatic #4 // Field numbers:[I获取常量并压栈都操作数栈栈顶,常量信息通过常量池4位置获取,即private static int[] numbers = {1, 6, 8};
11: astore_2操作数栈出栈,并存储在本地变量表2位置
12: aload_2从本地变量表2位置获取变量,并压栈到操作数栈栈顶
13: arraylength获取操作数栈栈顶元素,并获取其长度,并将长度值压栈到操作数栈栈顶
14: istore_3获取操作数栈栈顶元素并存储到本地变量表3位置
15: iconst_0将整形常量0,压入操作数栈栈顶
16: istore 4获取栈顶整形值并存储到本地变量表4位置
18: iload 4从本地变量表4位置加载对应变量,并压栈到操作数栈栈顶(即for循环的初始值0)
20: iload_3从本地变量表3位置加载对应变量,并压栈到操作数栈栈顶(即数组numbers = {1, 6, 8}的长度3)
21: if_icmpge 43从操作数栈中出栈两个整数,并比较,如果第一个大于第二个,则跳转到指令43的位置,这里43就是43: return,即方法结束
24: aload_2加载本地变量表2位置对象类型变量到操作数栈栈顶,即数组(即数组numbers = {1, 6, 8})
25: iload 4加载本地变量表4位置整数类型变量到操作数栈栈顶,第一次获取的就是0
27: iaload出栈2次,使用第一个出栈结果作为索引,从第二出栈结果中获取整数类型的数据,即执行numbers[0]
28: istore 5将获取的整数结果存储到本地变量表5位置,即赋值到number变量,此时for循环里的number值就是0
30: aload_1从局部变量表1位置获取对象类型变量,即ma,并压栈到操作数栈的栈顶
31: iload 5从局部变量表5位置获取整数类型变量,即number 0,并压栈到操作数栈栈顶
33: i2d栈顶元素出栈,并强转为double,之后重新入栈
34: invokevirtual #5 // Method monkey/study/MovieAverage.submit:(D)V执行MovieAverage.submit,并出栈操作数栈栈顶元素作为参数,首次就是ma.submit(0d);
37: iinc 4, 1将本地变量表4位置的值+1,即for循环中的number变为1
40: goto 18回到指令18的位置,继续整个循环的过程
43: return
5:一个动态例子
在开始之前先补充一个知识点,程序计数器,用来存储程序当前执行的位置,即字节码指令的偏移量。
如下代码:
package monkey.study;public class DynamicExample {public static void main(String[] args) {int a = 1;int b = 2;int c = (a + b) + 3;}
}
编译并查看字节码:
D:\test>javac -d . -g DynamicExample.javaD:\test>javap -c -verbose monkey.study.DynamicExample
...public static void main(java.lang.String[]);descriptor: ([Ljava/lang/String;)Vflags: ACC_PUBLIC, ACC_STATICCode:stack=2, locals=4, args_size=10: iconst_11: istore_12: iconst_23: istore_24: iload_15: iload_26: iadd7: iconst_38: iadd9: istore_310: returnLineNumberTable:line 6: 0line 7: 2line 8: 4line 9: 10LocalVariableTable:Start Length Slot Name Signature0 11 0 args [Ljava/lang/String;2 9 1 a I4 7 2 b I10 1 3 c I
}
接下来我们通过程序计数器,局部变量表,操作数栈3个元素的变化情况来分析下整个执行过程,初始化时如下:
执行指令0: iconst_1
,将整数常量1压到操作数栈:
执行指令1: istore_1
,将栈顶元素出栈,并赋值到局部变量表1位置:
这样a就被赋值1,接着执行指令2: iconst_2
,将整数常量2压到操作数栈:
接着执行指令3: istore_2
,操作数栈出栈,并赋值给局部变量表2位置:
这样b就赋值2,执行指令4: iload_1
,加载局部变量表1位置值到操作数栈,即a变量:
执行指令5: iload_2
,将局部变量表2位置值压到操作数栈栈顶,即变量b:
执行指令6: iadd
,将操作数栈顶2个元素出栈,并执行加法运算,最后将结果重新压到操作数栈栈顶:
执行指令7: iconst_3
,加载整形常量3到操作数栈栈顶:
执行指令8: iadd
,将操作数栈栈顶元素出栈2次,并执行加法运算,将结果重新压到操作数栈栈顶:
执行指令9: istore_3
,操作数栈出栈,并赋值到局部变量表的3位置的整型变量中,即c:
执行指令10: return
,方法结束,方法对应栈帧弹出线程栈。