1.从字节码的角度分析i++
/**
* 从字节码角度分析 a++ 相关题目
*/
public class Demo3_2 {
public static void main(String[] args) {
int a = 10;
int b = a++ + ++a + a--;
System.out.println(a);
System.out.println(b);
}
}
a++
和 ++a
实际上代表了两个不同的操作,它们分别对应自增和取数的过程。首先需要明确的是,自增操作是在局部变量上完成的。接着,了解这两者的区别至关重要:哪一个是先取数据,哪一个是先进行自增(或自减)。
具体来说:
a++
和a--
先取数,再进行局部自增(或自减)。++a
和--a
先进行局部自增(或自减),然后再取数。
因此,上述的代码最终结果是 b = 34
。
2.条件判断指令
2.1 几点说明:
- byte,short,char 都会按 int 比较,因为操作数栈都是 4 字节
- goto 用来进行跳转到指定行号的字节码
2.2 案例
public class Demo3_3 {
public static void main(String[] args) {
int a = 0;
if(a == 0) {
a = 10;
} else {
a = 20;
}
}
//字节码
0: iconst_0
1: istore_1
2: iload_1
3: ifne 12 //判断是否为0,若是跳到12行
6: bipush 10
8: istore_1
9: goto 15 //跳转
12: bipush 20
14: istore_1
15: return
3. 循环控制指令
好的,下面是经过整理和润色后的内容:
在 Java 中,循环控制结构其实是通过之前介绍的字节码指令来实现的。我们通过分析 while
、do while
和 for
循环的字节码,可以更好地理解它们背后的实现原理。
3.1. while
循环字节码示例
对于以下的 while
循环代码:
public class Demo3_4 {
public static void main(String[] args) {
int a = 0;
while (a < 10) {
a++;
}
}
}
对应的字节码是:
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 14
8: iinc 1, 1
11: goto 2
14: return
字节码的执行流程如下:
- 初始化变量
a
(即istore_1
)。 - 每次循环时,检查
a
是否小于 10(通过if_icmpge
判断)。 - 如果条件为
true
,则执行iinc
(自增a
),然后跳回循环判断(goto 2
)。 - 如果条件为
false
,则退出循环并结束程序。
3.2 do while
循环字节码示例
对于以下的 do while
循环代码:
public class Demo3_5 {
public static void main(String[] args) {
int a = 0;
do {
a++;
} while (a < 10);
}
}
对应的字节码是:
0: iconst_0
1: istore_1
2: iinc 1, 1
5: iload_1
6: bipush 10
8: if_icmplt 2
11: return
字节码的执行流程如下:
- 初始化变量
a
(istore_1
)。 - 在循环内部先执行
iinc
(自增a
),然后检查a
是否小于 10。 - 如果
a
小于 10,跳回到步骤 2,继续自增并检查条件。 - 如果条件不满足,则退出循环并结束程序。
3.3 for
循环字节码示例
最后是 for
循环。其实,如果我们仔细观察 while
和 for
循环的字节码,我们会发现它们是一模一样的。尽管语法上 for
循环包括了初始化、条件判断和自增部分,但在字节码层面,它们表现得几乎是相同的。
因此,以下是 for
循环的字节码:
public class Demo3_6 {
public static void main(String[] args) {
for (int a = 0; a < 10; a++) {
// do something
}
}
}
字节码也是与 while
循环的字节码完全相同:
0: iconst_0
1: istore_1
2: iload_1
3: bipush 10
5: if_icmpge 14
8: iinc 1, 1
11: goto 2
14: return
4.判断的结果
请从字节码角度分析,下列代码运行的结果:
public class Demo3_6_1 {
public static void main(String[] args) {
int i = 0;
int x = 0;
while (i < 10) {
x = x++;
i++;
}
System.out.println(x); // 结果是 0
}
}
分析结果为什么是0,是因为x++,先取数后自增,取数到操作栈依旧是0,局部变量位置是1,但是x=,这个表示复制,又把局部变量的位置改成0,这样就陷入死循环,无论循环多少次一直为0.
5.构造方法
5.1 <cint> ()v
public class StaticTest {
static int i = 10;
static {
i = 30;
}
static {
i = 20;
}
public static void main(String[] args) {
}
}
编译器会按从上至下的顺序,收集所有静态代码块和静态成员赋值的代码,合并为一个特殊的方法
Code:
0: bipush 10
2: putstatic #7 // Field i:I
5: bipush 30
7: putstatic #7 // Field i:I
10: bipush 20
12: putstatic #7 // Field i:I
15: return
5.2 <int>()V
public class Demo3_8_2 {
private String a = "s1";
{
b = 20;
}
private int b = 10;
{
a = "s2";
}
public Demo3_8_2(String a, int b) {
this.a = a;
this.b = b;
}
public static void main(String[] args) {
Demo3_8_2 d = new Demo3_8_2("s3", 30);
System.out.println(d.a);
System.out.println(d.b);
}
}
编译器会按从至下的顺序,收集所有{}代码和成员变量,形成新的构造方法,但是原始的构造放内的代码执行总是在最后面
因此这个代码的结果是:s3和30
6.方法的调用
public class Demo3_9 {
public Demo3_9() { }
private void test1() { }
private final void test2() { }
public void test3() { }
public static void test4() { }
public static void main(String[] args) {
Demo3_9 d = new Demo3_9();
d.test1();
d.test2();
d.test3();
d.test4();
Demo3_9.test4();
}
}
字节码
0: new #2 // class cn/itcast/jvm/t3/bytecode/Demo3_9
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
16: aload_1
17: invokevirtual #6 // Method test3:()V
20: aload_1
21: pop
22: invokestatic #7 // Method test4:()V
25: invokestatic #7 // Method test4:()V
28: return
几种类型的方法的调用说明一下:
- invokespecial,普通私有方法的调用
- invokevirtual,调用实例方法,通过对象的类型决定调用哪个方法(普通方法调用)。
- invokestatic,静态的方法调用
整个流程为:
- new 新建对象,给对象分配堆内存,然后把对象引用压入操作数栈
- dup,是把对象复制一份,为什么要复制一份呢,一个是调用对象构造方法,一个是把这个对象赋给局部变量
- 然后调用方法就行
- 备注:d.test3();这种形式调用静态方法,会有一个pop出栈的操作,这样就会把【对象引用】从操作数弹掉
7.多态是在字节码中是如何调用的
当执行 invokevirtual 指令时,
- 获取对象引用:从栈帧中的操作数栈中获取方法调用的对象引用。
- 分析对象头:通过对象引用找到对象的
Class
对象,进一步分析该对象的实际类型。 - 查找 vtable:通过对象的实际类型,获取其
vtable
(虚方法表)。 - 查表得到方法地址:根据方法名和签名,在
vtable
中查找到对应的方法地址,确保方法是动态绑定的。 - 执行方法字节码:跳转到方法的字节码地址并执行该方法。
8.异常处理
8.1 catch
public class Demo3_11_1 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
}
}
}
//main方法的字节码
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=1
0: iconst_0
1: istore_1
2: bipush 10
4: istore_1
5: goto 12
8: astore_2
9: bipush 20
11: istore_1
12: return
Exception table:
from to target type
2 5 8 Class java/lang/Exception
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
9 3 2 e Ljava/lang/Exception;
0 13 0 args [Ljava/lang/String;
2 11 1 i I
StackMapTable: ...
MethodParameters: ...
}
- 可以看到多出来一个 Exception table 的结构,[from, to) 是前闭后开的检测范围,一旦这个范围内的字节码执行出现异常,则通过 type 匹配异常类型,如果一致,进入 target 所指示行号
- 8 行的字节码指令 astore_2 是将异常对象引用存入局部变量表的 slot 2 位置
8.2 finally
public class Demo3_11_4 {
public static void main(String[] args) {
int i = 0;
try {
i = 10;
} catch (Exception e) {
i = 20;
} finally {
i = 30;
}
}
}
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=4, args_size=1
0: iconst_0
1: istore_1 // 0 -> i
2: bipush 10 // try --------------------------------------
4: istore_1 // 10 -> i |
5: bipush 30 // finally |
7: istore_1 // 30 -> i |
8: goto 27 // return -----------------------------------
11: astore_2 // catch Exceptin -> e ----------------------
12: bipush 20 // |
14: istore_1 // 20 -> i |
15: bipush 30 // finally |
17: istore_1 // 30 -> i |
18: goto 27 // return -----------------------------------
21: astore_3 // catch any -> slot 3 ----------------------
22: bipush 30 // finally |
24: istore_1 // 30 -> i |
25: aload_3 // <- slot 3 |
26: athrow // throw ------------------------------------
27: return
Exception table:
from to target type
2 5 11 Class java/lang/Exception
2 5 21 any // 剩余的异常类型,比如 Error
11 15 21 any // 剩余的异常类型,比如 Error
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
12 3 2 e Ljava/lang/Exception;
0 28 0 args [Ljava/lang/String;
2 26 1 i I
StackMapTable: ...
MethodParameters: ...
由于finally必须要执行,可以看到 finally 中的代码被复制了 3 份,分别放入 try 流程,catch 流程以及 catch 剩余的异常类型流程,最后一份是,怕catch捕获不到的异常,执行不了所有在其他异常也复制了一份
8.2.1finally面试题1
public class Demo3_12_2 {
public static void main(String[] args) {
int result = test();
System.out.println(result);
public static int test() {
try {
return 10;
} finally {
return 20;
}
}
}
//方法字节码
public static int test();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=2, args_size=0
0: bipush 10 // <- 10 放入栈顶
2: istore_0 // 10 -> slot 0 (从栈顶移除了)
3: bipush 20 // <- 20 放入栈顶
5: ireturn // 返回栈顶 int(20)
6: astore_1 // catch any -> slot 1
7: bipush 20 // <- 20 放入栈顶
9: ireturn // 返回栈顶 int(20)
Exception table:
from to target type
0 3 6 any
LineNumberTable: ...
StackMapTable: ...
由这个字节码我们可以看到,finally里面的逻辑一定会执行,但是如果 finally
中有 return
,它会覆盖任何异常或者其他的返回值。
8.2.2 finally面试题2
public class Demo3_12_2 {
public static void main(String[] args) {
int result = test();
System.out.println(result);
}
public static int test() {
int i = 10;
try {
return i;
} finally {
i = 20;
}
}
}
//字节码
public static int test();
descriptor: ()I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=3, args_size=0
0: bipush 10 // <- 10 放入栈顶
2: istore_0 // 10 -> i
3: iload_0 // <- i(10)
4: istore_1 // 10 -> slot 1,暂存至 slot 1,目的是为了固定返回值
5: bipush 20 // <- 20 放入栈顶
7: istore_0 // 20 -> i
8: iload_1 // <- slot 1(10) 载入 slot 1 暂存的值
9: ireturn // 返回栈顶的 int(10)
10: astore_2
11: bipush 20
13: istore_0
14: aload_2
15: athrow
Exception table:
from to target type
3 5 10 any
LineNumberTable: ...
LocalVariableTable:
Start Length Slot Name Signature
3 13 0 i I
通过使用 istore_1 暂存 i 的值,字节码确保了即使在 finally 块中修改了 i 的值(将其设置为 20局部变量
),最终返回的仍然是 try 块中的原始 i 值(即 10)。