类文件结构与字节码指令
1、类文件结构
一个简单的 HelloWorld.java 程序:
public class HelloWorld {
public static void main(String[] args) {
System.out.println("hello world!");
}
}
接下来执行:javac -parameters -d . HelloWorld.java 命令编译.java 文件为.class 文件:
其中-parameters 表示将源文件转换为字节码文件的时候保留参数信息
获得二进制字节码文件后怎么读呢?有2种方式:
- 方式一:JDK自带的反编译工具:
javap -verbose XXX.class
根据JVM规范类文件的结构如下:
其中u跟数字n,表示占用n个字节
1.1 魔数(u4 magic)
魔数(u4 magic):对应字节码文件的0~3个字节,表示文件的特定类型,不同文件有自己不同的魔数信息,例如java的二进制.class文件的
魔数类型就是如下:
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 091
对于这个cafebabe的由来就不再说了!(每一行的前8位属于编号)
1.2 版本(u2 minor_version、u2 major_version)
版本:对应字节码文件的4~7个字节,表示类的版本 :我们的小版本号在这里不显示,所以前2个字节为00 00
0000000 ca fe ba be00 00 00 3400 23 0a 00 06 00 15 09
这里的十六进制 34H(00 34) 表示十进制的 52,代表的就是JDK8,依次类推:51 就是 JDK 7,53 就是JDK 9。
扩展:2AF5换算成10进制 : 5 * 16^0 + F * 16^1 + A * 16^2 + 2 * 16^3 = 10997
1.3 常量池
实在太麻烦:参考文章
Constant Type 常量类型 | Value 常量对应的序号(十进制) | Value 常量对应的序号(十六进制) |
---|
CONSTANT_Class | 7 | 7 | CONSTANT_Fieldref | 9 | 9 | CONSTANT_Methodref | 10 | a | CONSTANT_InterfaceMethodref | 11 | b | CONSTANT_String | 8 | 8 | CONSTANT_Integer | 3 | 3 | CONSTANT_Float | 4 | 4 | CONSTANT_Long | 5 | 5 | CONSTANT_Double | 6 | 6 | CONSTANT_NameAndType | 12 | c | CONSTANT_Utf8 | 1 | 1 | CONSTANT_MethodHandle | 15 | f | CONSTANT_MethodType | 16 | 10 | CONSTANT_InvokeDynamic | 18 | 12 |
案例分析
8~9 字节,表示常量池长度,00 23 (35) 表示常量池有 #1~#34 项,注意#0 项不计入,也没有值,注意00表示引用
0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09
-
第#1项 0a (十六进制) 对应的十进制是10,查找常量池表得知为CONSTANT_Methodref,即方法引用(方法信息),00 06 (6) 和 00 15(21) 表示它引用了常量池中 #6 和#21 项来获得这个方法的【所属类】和【方法名】。 0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09 -
第#2项 09 查找常量池表得知,表示一个 Field 信息,00 16(22)和 00 17(23) 表示它引用了常量池中 #22 和# 23 项来获得这个成员变量的【所属类】和【成员变量名】 0000000 ca fe ba be 00 00 00 34 00 23 0a 00 06 00 15 09 0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07 -
第#3项 08 表示一个字符串常量名称,00 18(24)表示它引用了常量池中#24 项 0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07 -
第#4项 0a 表示一个 Method 信息,00 19(25) 和 00 1a(26) 表示它引用了常量池中 #25 和 #26 项来获得这个方法的【所属类】和【方法名】 0000020 00 16 00 17 08 00 18 0a 00 19 00 1a07 00 1b 07 -
第#5项 07 表示一个 Class 信息,00 1b(27) 表示它引用了常量池中 #27 项 0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07 -
第#6项 07 表示一个 Class 信息,00 1c(28) 表示它引用了常量池中 #28 项 0000020 00 16 00 17 08 00 18 0a 00 19 00 1a 07 00 1b 07 0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29 -
第#7项 01 表示一个 utf8 串,00 06 表示长度,3c 69 6e 69 74 3e 是【< init > 】 0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29 -
第#8项 01 表示一个 utf8 串,00 03 表示长度,28 29 56 是【() V】其实就是表示无参、无返回值 0000040 00 1c 01 00 06 3c 69 6e 69 74 3e 01 00 03 28 29 0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e -
第#9项 01 表示一个 utf8 串,00 04 表示长度,43 6f 64 65 是【Code】 0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e -
第#10项 01 表示一个 utf8 串,00 0f(15) 表示长度,4c 69 6e 65 4e 75 6d 62 65 72 54 61 62 6c 65 是【LineNumberTable】 0000060 56 01 00 04 43 6f 64 65 01 00 0f 4c 69 6e 65 4e 0000100 75 6d 62 65 72 54 61 62 6c 65 01 00 12 4c 6f 63
实在写不下去了,需要知道的是常量池一共34项,每项表示各不相同!
1.4 访问标识与继承信息
21 表示该 class 是一个类,公共的类,查下面表中第得到 (0x0001 + 0x0020)ACC_PUBLIC + ACC_SUPER :
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 05
表示根据常量池中 #5 找到本类全限定名:
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01 06
表示根据常量池中 #6 找到父类全限定名:
0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
表示接口的数量,本类为 0:
0 0000660 29 56 00 21 00 05 00 0600 00 00 00 00 02 00 01
1.5 Field 信息
表示成员变量数量,本类为 0
0 0000660 29 56 00 21 00 05 00 06 00 00 00 00 00 02 00 01
这么分析也太麻烦了,所以Oracle提供了Javap工具,反编译.class文件
2、字节码指令
2.1、javap 工具
Java 中提供了 javap 工具来反编译 class 文件
javap -v D:Demo.class
2.2、图解运行流程
执行的代码
public class Demo3_1 {
public static void main(String[] args) {
int a = 10;
int b = Short.MAX_VALUE + 1;
int c = a + b;
System.out.println(c);
}
}
常量池也属于方法区,只不过这里单独提出来了
方法字节码载入方法区
由于:(stack=2,locals=4) 对应操作数栈有 2 个空间(每个空间 4 个字节),局部变量表中有 4 个槽位。
- stack:最大栈深度,是我们的同时最多能操作多少变量
- locals:我们一共有多少个变量
栈帧:局部变量表、操作数栈、动态链接、方法出口
执行引擎开始执行字节码
bipush 10:将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有
- sipush 将一个 short 压入操作数栈(其长度会补齐 4 个字节)
- ldc 将一个 int 压入操作数栈
- ldc2_w 将一个 long 压入操作数栈(分两次压入,因为 long 是 8 个字节)
- 这里小的数字都是和字节码指令存在一起,超过 short 范围(-127~128)的数字存入了常量池
istore 1
将操作数栈栈顶元素弹出,放入局部变量表的 slot 1 中
对应代码中的 a = 10,为a变量赋值完成
执行Idc #3 将运行时 常量池中的#3压入操作数栈帧
istore2 将操作数栈中的元素弹出,放到局部变量表的 2 号位置
iload1 iload2 将局部变量表中 1 号位置和 2 号位置的元素放入操作数栈中。因为只能在操作数栈中执行运算操作
iadd 将操作数栈中的两个元素弹出栈并相加,结果在压入操作数栈中。
istore_3
将我们操作数栈中的计算好的数据,放到局部变量表3的位置
getstatic #4
在运行时常量池中找到 #4 ,发现是一个对象,在堆内存中找到该对象,并将其引用放入操作数栈中
iload #3
将我们局部变量中的3号位置的数据放入我们的操作数栈中
invokevirtual #5
- 找到常量池 #5 项,定位到方法区 java/io/PrintStream.println:(I)V 方法
- 生成新的栈帧(分配 locals、stack等)
- 传递参数,执行新栈帧中的字节码
(一个方法一个栈帧)
return
完成 main 方法调用,弹出 main 栈帧,程序结束
2.3、相关练习
练习 a++ 相关问题
public class ByteCodeTest {
public static void main(String[] args) {
int a = 10 ;
int b = a++ + ++a + a-- ;
System.out.println(a);
System.out.println(b);
}
}
参考:
练习
public class ByteCodeTest {
public static void main(String[] args) {
int i = 0 ;
int x = 0 ;
while(i < 10){
x = x++ ; iload 先将x=0取出到操作数栈中,然后对变量表中x的数据++,然后将操作数栈中的数据itore写回给x
i++ ; 因此导致无效add
}
System.out.println(x);
}
}
2.4、构造方法
cinit() V ,构造方法,无返回值
public class Code_12_CinitTest {
static int i = 10;
static {
i = 20;
}
static {
i = 30;
}
public static void main(String[] args) {
System.out.println(i);
}
}
编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 cinit()V :
stack=1, locals=0, args_size=0
0: bipush 10
2: putstatic
5: bipush 20
7: putstatic
10: bipush 30
12: putstatic
15: return
init()V
public class Code_13_InitTest {
private String a = "s1";
{
b = 20;
}
private int b = 10;
{
a = "s2";
}
public Code_13_InitTest(String a, int b) {
this.a = a;
this.b = b;
}
public static void main(String[] args) {
Code_13_InitTest d = new Code_13_InitTest("s3", 30);
System.out.println(d.a);
System.out.println(d.b);
}
}
编译器会按=从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在后.
注意:如果是静态代码块和静态成员变量,优先级高于{}代码块和成员变量
2.5、方法的调用
public class Code_14_MethodTest {
public Code_14_MethodTest() {
}
private void test1() {
}
private final void test2() {
}
public void test3() {
}
public static void test4() {
}
public static void main(String[] args) {
Code_14_MethodTest obj = new Code_14_MethodTest();
obj.test1();
obj.test2();
obj.test3();
obj.test4();
Code_14_MethodTest.test4();
}
}
不同方法在调用时,对应的虚拟机指令有所区别
- 私有、构造、被 final 修饰的方法,在调用时都使用 invokespecial 指令
- 普通成员方法在调用时,使用 invokevirtual 指令。因为编译期间无法确定该方法的内容,只有在运行期间才能确定
- 静态方法在调用时使用 invokestatic 指令
Code:
stack=2, locals=2, args_size=1
0: new #2
3: dup
4: invokespecial #3
7: astore_1
8: aload_1
9: invokespecial #4
12: aload_1
13: invokespecial #5
15: invokevirtual #6
16: aload_1
17: pop
18: invokestatic #7
20: invokestatic #8
23: return
2.6、多态的原理
因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用 invokevirtual 指令
在执行 invokevirtual 指令时,经历了以下几个步骤
- 先通过栈帧中对象的引用找到对象
- 分析对象头,找到对象实际的 Class
- Class 结构中有 vtable
- 查询 vtable 找到方法的具体地址
- 执行方法的字节码
2.7、异常处理
try - catch
public class Code_15_TryCatchTest {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
}catch (Exception e) {
i = 20;
}
}
}
对应字节码指令 :
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1 //为i赋值
2: bipush 10
4: istore_1 //将10写入变量表中的i
5: goto 12 //结束(不抛出异常到此结束,goto直接到12行)
8: astore_2 //将我们的异常存入异常表
9: bipush 20 //20放入操作数栈
11: istore_1 //20写入变量表中的i
12: return
//多出来一个异常表
Exception table:
from to target type
2 5 8 Class java/lang/Exception 当我们的代码在[2,5)中出现异常,直接跳转到8行执行
- 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开(也就是检测 2~4 行)的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
- 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 2 号位置(为 e )
多个 single-catch
public class Code_16_MultipleCatchTest {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
}catch (ArithmeticException e) {
i = 20;
}catch (Exception e) {
i = 30;
}
}
}
对应的字节码
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 19
8: astore_2
9: bipush 20
11: istore_1
12: goto 19
15: astore_2
16: bipush 30
18: istore_1
19: return
Exception table:
from to target type
2 5 8 Class java/lang/ArithmeticException
2 5 15 Class java/lang/Exception
- 因为异常出现时,只能进入 Exception table 中一个分支,所以局部变量表 slot 2 位置被共用
- multicatch同理只是Exception table中是一样的,同样是一个槽保存异常信息
finally
public class Code_17_FinallyTest {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
} finally {
i = 30;
}
}
}
字节码文件
Code:
stack=1, locals=4, args_size=1
0: iconst_0
1: istore_1
// try块
2: bipush 10
4: istore_1
// try块执行完后,会执行finally
5: bipush 30
7: istore_1
8: goto 27
// catch块
11: astore_2 // 异常信息放入局部变量表的2号槽位
12: bipush 20
14: istore_1
// catch块执行完后,会执行finally
15: bipush 30
17: istore_1
18: goto 27
// 出现异常,但未被 Exception 捕获,会抛出其他异常,这时也需要执行 finally 块中的代码
21: astore_3
22: bipush 30
24: istore_1
25: aload_3
26: athrow // 抛出异常
27: return
Exception table:
from to target type
2 5 11 Class java/lang/Exception
2 5 21 any
11 15 21 any
可以看到 ?nally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程
注意:虽然从字节码指令看来,每个块中都有 finally 块,但是 finally 块中的代码只会被执行一次
finally相关面试题
public class finallyTest {
public static void main(String[] args) {
int result = test() ;
System.out.println(result);
}
public static int test(){
try {
return 10 ;
}finally {
return 20 ;
}
}
}
结论:当我们的try语句块中遇到return的时候,会先将return的值保存一下(不会被更改),然后执行finally语句块!
注意:finally块中return导致覆盖了我们的原有的return,所以finally尽量不要用,会吞了我们自己的return
2.8、Synchronized
public class Code_19_SyncTest {
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println("ok");
}
}
}
对应字节码
Code:
stack=2, locals=4, args_size=1
0: new #2
3: dup
4: invokespecial #1
7: astore_1
8: aload_1
9: dup
10: astore_2
11: monitorenter
12: getstatic #3
15: ldc #4
17: invokevirtual #5
20: aload_2
21: monitorexit
22: goto 30
25: astore_3
26: aload_2
27: monitorexit
28: aload_3
29: athrow
30: return
Exception table:
from to target type
12 22 25 any
25 28 25 any
2 5 21 any
11 15 21 any
可以看到 ?nally 中的代码被==复制了 3 份==,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程
**注意:虽然从字节码指令看来,每个块中都有 finally 块,但是 finally 块中的代码只会被执行一次**
> finally相关面试题
```java
//我们的结果会是多少呢? 20
public class finallyTest {
public static void main(String[] args) {
int result = test() ;
System.out.println(result);
}
public static int test(){
try {
return 10 ;
}finally {
return 20 ;
}
}
}
结论:当我们的try语句块中遇到return的时候,会先将return的值保存一下(不会被更改),然后执行finally语句块!
注意:finally块中return导致覆盖了我们的原有的return,所以finally尽量不要用,会吞了我们自己的return
2.8、Synchronized
public class Code_19_SyncTest {
public static void main(String[] args) {
Object lock = new Object();
synchronized (lock) {
System.out.println("ok");
}
}
}
对应字节码
Code:
stack=2, locals=4, args_size=1
0: new #2
3: dup
4: invokespecial #1
7: astore_1
8: aload_1
9: dup
10: astore_2
11: monitorenter
12: getstatic #3
15: ldc #4
17: invokevirtual #5
20: aload_2
21: monitorexit
22: goto 30
25: astore_3
26: aload_2
27: monitorexit
28: aload_3
29: athrow
30: return
Exception table:
from to target type
12 22 25 any
25 28 25 any
结论 : 无论如何,我们通过synchronized加锁的对象最后一定会释放锁!
|