一个简单的 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种方式:
javap -verbose XXX.class
.class
文件,需要装载插件HexEditor,参考文章:notepad++查看二进制.class文件——HexEditor插件(64/32位)安装教程,安装文章中的方式二即可完成,并可以打开.class
文件查看二进制字节码:根据JVM规范类文件的结构如下:
其中u跟数字n,表示占用n个字节
魔数(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位属于编号)
版本:对应字节码文件的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。
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) 和 0015(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 和 #260000020 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 650000060 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
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
表示成员变量数量,本类为 0
0 0000660 29 56 00 21 00 05 00 06 00 00 00 00
00 02 00 01
Java 中提供了 javap 工具来反编译 class 文件
javap -v D:Demo.class
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 个槽位。
栈帧:局部变量表、操作数栈、动态链接、方法出口
执行引擎开始执行字节码
bipush 10:将一个 byte 压入操作数栈(其长度会补齐 4 个字节),类似的指令还有
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
(一个方法一个栈帧)
return
完成 main 方法调用,弹出 main 栈帧,程序结束
/*** * 从字节码的角度分析 a++ 相关题目 */
public class ByteCodeTest {
public static void main(String[] args) {
int a = 10 ;
int b = a++ + ++a + a-- ;
System.out.println(a); //11
System.out.println(b); //34 : 10 + 12 + 12
}
}
public class ByteCodeTest {
public static void main(String[] args) {
int i = 0 ;
int x = 0 ; //变量表中的两个变量分别为 0 ,0
while(i < 10){
x = x++ ; iload 先将x=0取出到操作数栈中,然后对变量表中x的数据++,然后将操作数栈中的数据itore写回给x
i++ ; 因此导致无效add
}
System.out.println(x); // 输出0
}
}
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); // 30
}
}
编译器会按从上至下的顺序,收集所有 static 静态代码块和静态成员赋值的代码,合并为一个特殊的方法 cinit()V :
stack=1, locals=0, args_size=0
0: bipush 10
2: putstatic #3 // Field i:I
5: bipush 20
7: putstatic #3 // Field i:I
10: bipush 30
12: putstatic #3 // Field i:I
15: return
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); //在init()V之后执行
System.out.println(d.a);
System.out.println(d.b);
}
}
编译器会按=从上至下的顺序,收集所有 {} 代码块和成员变量赋值的代码,形成新的构造方法,但原始构造方法内的代码总是在后.
注意:如果是静态代码块和静态成员变量,优先级高于{}代码块和成员变量
public class Code_14_MethodTest {
public Code_14_MethodTest() {
}
private void test1() { //私有方法 invokespecial
}
private final void test2() { //final方法 invokespecial
}
public void test3() { //普通成员方法 invokevirtual
}
public static void test4() { //静态方法 invokestatic
}
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();
}
}
不同方法在调用时,对应的虚拟机指令有所区别
Code:
stack=2, locals=2, args_size=1
0: new #2 //
3: dup // 复制一份对象地址压入操作数栈中
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: aload_1
9: invokespecial #4 // Method test1:()V
12: aload_1
13: invokespecial #5 // Method test2:()V
15: invokevirtual #6 // Method test3:()V
16: aload_1
17: pop
18: invokestatic #7 //通过对象调用静态方法test4,会浪费2个指令aload1和pop,因此最好不要使用
20: invokestatic #8 // Method test4:()V
23: return
因为普通成员方法需要在运行时才能确定具体的内容,所以虚拟机需要调用 invokevirtual 指令
在执行 invokevirtual 指令时,经历了以下几个步骤
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行执行
多个 single-catch
public class Code_16_MultipleCatchTest {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
}catch (ArithmeticException e) { //同理muitlcatch(ArithmeticException | Exception 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
finally
public class Code_17_FinallyTest {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
} finally { //为什么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
可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程
注意:虽然从字节码指令看来,每个块中都有 finally 块,但是 finally 块中的代码只会被执行一次
//我们的结果会是多少呢? 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
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 // class java/lang/Object
3: dup // 复制一份栈顶,然后压入栈中。用于函数消耗
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1 // 将栈顶的对象地址方法 局部变量表中 1 中
8: aload_1 // 加载到操作数栈
9: dup // 复制一份,放到操作数栈,用于加锁时消耗
10: astore_2 // 将操作数栈顶元素弹出,暂存到局部变量表的 2 号槽位。这时操作数栈中有一份对象的引用
11: monitorenter // 加锁
12: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #4 // String ok
17: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
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
可以看到 finally 中的代码被==复制了 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
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 // class java/lang/Object
3: dup // 复制一份栈顶,然后压入栈中。用于函数消耗
4: invokespecial #1 // Method java/lang/Object."<init>":()V
7: astore_1 // 将栈顶的对象地址方法 局部变量表中 1 中
8: aload_1 // 加载到操作数栈
9: dup // 复制一份,放到操作数栈,用于加锁时消耗
10: astore_2 // 将操作数栈顶元素弹出,暂存到局部变量表的 2 号槽位。这时操作数栈中有一份对象的引用
11: monitorenter // 加锁
12: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
15: ldc #4 // String ok
17: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
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加锁的对象最后一定会释放锁!
版权说明 : 本文为转载文章, 版权归原作者所有 版权申明
原文链接 : https://blog.csdn.net/m0_46571920/article/details/121595410
内容来源于网络,如有侵权,请联系作者删除!