本篇注意探讨的问题是 finally与return代码哪个先执行?

先说结论 : 先执行return中的代码

执行顺序的问题 , 非常容易解决 , 请查看下面的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public static void main(String[] args) {
System.out.printf("Int::main : %d\n",intVal);
}

static int add(){
int val = 0;
try {
val++;
return returnInt(val);
} finally {
val++;
System.out.printf("Int::finally : %d\n",val);
}
}

static int returnInt(int val){
System.out.printf("Int::return : %d\n",val);
return val;
}

运行的结果是

1
2
3
Int::return : 1
Int::finally : 2
Int::main : 1

执行顺序的问题解决了, 那么这里你可能会问 , 为什么 main中返回的结果是1呢?

还是先说结论,

由于Java中的参数传递只有值传递 , return的时候会先拷贝一个 val 的引用 , main函数中获取到的就是这个val的引用(对于基本数据类型是直接copy的)

这里我们可以查看反编译的代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static int add() {
int val = 0;

int var1;
try {
++val;
var1 = returnInt(val);
} finally {
++val;
System.out.printf("Int::finally : %d\n", val);
}
return var1;
}

static int returnInt(int val) {
System.out.printf("Int::return : %d\n", val);
return val;
}

可以看到最后返回的并不是我们预先定义的val , 而是一个新的变量 => var1。

JVM内存区域

如果已经了解请直接跳到下一部分

为了方便理解字节码部分的内容, 这里简单叙述

变量类型

image-20231005145936539

Java虚拟机栈

与程序计数器一样,Java 虚拟机栈(后文简称栈)也是线程私有的,它的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。

除了一些 Native 方法调用是通过本地方法栈实现的,其他所有的 Java 方法调用都是通过栈来实现的(也需要和其他运行时数据区域比如程序计数器配合)。

Java 运行时数据区域(JDK1.7)

方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。

栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。


Java 方法有两种返回方式,一种是 return 语句正常返回,一种是抛出异常。

不管哪种返回方式,都会导致栈帧被弹出。也就是说, 栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。

除了 StackOverFlowError 错误之外,栈还可能会出现OutOfMemoryError错误,这是因为如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常。

  • 例如下面的deep()就会导致 StackOverFlowError
1
2
3
4
5
6
7
public static void main(String[] args) {
deep();
}

static void deep(){
deep();
}
  • 下面的代码就会出现 OutOfMemoryError ( 8byte , 1MB大概是 )

    通过Runtime获取到当前最大的内存容量, 接着定义 超过内存大小的数组 , 出现了内存溢出错误。 8Byte * 1<<30 => 1<<33 Byte

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
long l = Runtime.getRuntime().totalMemory();
int idx = 0;
while(l!=0){
l>>=1;
idx ++;
}
System.out.println("当前虚拟机的内容量级为 2^"+ idx);
long[] longs = new long[1<<30];
}
//当前虚拟机的内容量级为 2^33
//Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
// at com.dhx.Main.main(Main.java:22)

局部变量表 主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,

不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。

和数据结构上的栈类似,两者都是先进后出的数据结构,只支持出栈和入栈两种操作。

局部变量表

操作数栈 主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中

动态链接 主要服务一个方法需要调用其他方法的场景。Class 文件的常量池里保存有大量的符号引用比如方法引用的符号引用。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用,这个过程也被称为 动态连接

线程私有的:

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈

线程共享的:

  • 堆 :
  • 方法区 : 首先类编译后产生.class的字节码文件,执行类加载器把.class的字节码文件加载到方法区内存(方法的代码片段以及整个类的代码片段都是存储到方法区内存当中)
  • 直接内存 (非运行时数据区的一部分)

Java 虚拟机所管理的内存中最大的一块,堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。

字节码解读

有了预备的JVM知识之后, 我们来解读字节码文件

这里通过IDEA jclasslib 插件来查看字节码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 0 iconst_0
1 istore_0
2 iinc 0 by 1
5 iload_0
6 invokestatic #15 <com/dhx/db/Main.returnInt : (I)I>
9 istore_1
10 iinc 0 by 1
13 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>
16 iload_0
17 invokevirtual #16 <java/io/PrintStream.println : (I)V>
20 iload_1
21 ireturn
22 astore_2
23 iinc 0 by 1
26 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>
29 iload_0
30 invokevirtual #16 <java/io/PrintStream.println : (I)V>
33 aload_2
34 athrow

首先需要知道其中几个关键的 JVM指令

具体请参考 https://docs.oracle.com/javase/specs/jvms/se19/html/jvms-6.html

接下来提到的栈默认为操作数栈

操作数栈局部变量表 都存在于线程的方法栈的栈帧中

简单的来理解 , 操作数栈用于进行变量的操作 , 局部变量表用来进行存储

  • a开头的指令操作的对象为 引用
  • i开头的指令操作的对象为 整数

iconst_<i>

把常量 i 压入到操作数栈

Operation

Push int constant

Description

Push the int constant <i> (-1, 0, 1, 2, 3, 4 or 5) onto the operand stack.

istore_<n>

弹出栈顶元素, 存储到局部变量表 table[n] 中

Operation

Store int into local variable

Description

The <n> must be an index into the local variable array of the current frame (§2.6). The value on the top of the operand stack must be of type int. It is popped from the operand stack, and the value of the local variable at <n> is set to value.

iinc

Operation

iinc 0 by 1 局部变量表 talbe[0] 变量自增1

Increment local variable by constant

The index is an unsigned byte that must be an index into the local variable array of the current frame (§2.6). The const is an immediate signed byte. The local variable at index must contain an int. The value const is first sign-extended to an int, and then the local variable at index is incremented by that amount.

iload_<n>

将局部变量表 table[n]的值加载到栈顶中

Operation

Load int from local variable

Description

The <n> must be an index into the local variable array of the current frame (§2.6). The local variable at <n> must contain an int. The value of the local variable at <n> is pushed onto the operand stack.

istore_<n>

将 栈顶的 值 存储到table[n]中

Operation

Store int into local variable

Description

The <n> must be an index into the local variable array of the current frame (§2.6). The value on the top of the operand stack must be of type int. It is popped from the operand stack, and the value of the local variable at <n> is set to value.

astore_<n>

将栈顶的值存储到局部变量表 table[n] 中

Operation

Store reference into local variable

Description

The <n> must be an index into the local variable array of the current frame (§2.6). The objectref on the top of the operand stack must be of type returnAddress or of type reference. It is popped from the operand stack, and the value of the local variable at <n> is set to objectref.

astore_<n>istore<n> 的区别在于存储的数据类型。

  • astore_<n> 指令用于将引用类型(Reference type,如对象引用)的值存储到局部变量中。其中,<n> 表示局部变量表的索引,范围为0到3。例如,astore_2 将栈顶的引用类型值存储到局部变量表中索引为2的位置。
  • istore<n> 指令用于将整数类型(Integer type,如int)的值存储到局部变量中。其中,<n> 表示局部变量表的索引,范围为0到3。例如,istore_1 将栈顶的整数类型值存储到局部变量表中索引为1的位置。

其实就是

  • astore_<n> 用于存储引用类型的值。
  • istore<n> 用于存储整数类型的值。

dup

复制栈顶元素并push到栈顶中

Operation

Duplicate the top operand stack value

Description

Duplicate the top value on the operand stack and push the duplicated value onto the operand stack.

The dup instruction must not be used unless value is a value of a category 1 computational type (§2.11.1).

pop

栈顶元素出栈

Operation

Pop the top operand stack value

Description

Pop the top value from the operand stack.

The pop instruction must not be used unless value is a value of a category 1 computational type (§2.11.1).

ireturn

Operation

Return int from method

方法返回 , 类型为整数

athrow

Operation

Throw exception or error

抛出异常错误

.class分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
 0 iconst_0
1 istore_0
2 iinc 0 by 1
5 iload_0
6 invokestatic #15 <com/dhx/db/Main.returnInt : (I)I>
9 istore_1
10 iinc 0 by 1
13 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>
16 iload_0
17 invokevirtual #16 <java/io/PrintStream.println : (I)V>
20 iload_1
21 ireturn
22 astore_2
23 iinc 0 by 1
26 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>
29 iload_0
30 invokevirtual #16 <java/io/PrintStream.println : (I)V>
33 aload_2
34 athrow
  1. iconst_0:该指令将常量值 0 压入操作数栈顶。也就是初始化一个变量为 0。
  2. istore_0:将栈顶的值存储到局部变量 0 (table[0])中。将变量 0 的初始值存储到局部变量表中。
  3. iinc 0 by 1:将局部变量 0 (table[0])的值增加 1。这里是将变量 0 的值递增。
  4. iload_0:将局部变量 0 (table[0])的值加载到栈顶,准备执行后续操作。
  5. istore_1:将栈顶的值存储到局部变量 1 (table[1])中。
  6. getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>:获取 System.out 的静态字段,即标准输出流。这里用于获取标准输出流。
  7. iload_0:将局部变量 0 (table[0])的值加载到栈顶,准备执行后续操作。
  8. invokedynamic #6 <makeConcatWithConstants, BootstrapMethods #1>:动态调用 makeConcatWithConstants 方法,并将栈顶的参数传递给该方法。这里调用了一个动态方法,并将栈顶的参数传递给这个方法。
  9. invokevirtual #5 <java/io/PrintStream.println : (Ljava/lang/String;)V>:调用 PrintStream.println 方法,将栈顶的字符串参数打印到标准输出。这里执行的是调用 println 方法,将栈顶的字符串参数打印到标准输出。
  10. iload_1:将局部变量 1 (table[1])的值加载到栈顶,准备作为方法的返回值。
  11. ireturn:将局部变量 1 (table[1])的值作为方法的返回值。
  12. astore_2:将栈顶的值存储到局部变量 2 中。
  13. athrow:在发生异常时抛出栈顶的异常。

那么每一步的内容以及对应的操作数栈和局部变量表如下(前面的序号对应字节码的行号)

0: iconst_0
将常量0压入操作数栈中:[0]
局部变量表:0: 0

1: istore_0
将操作数栈顶的值(0)弹出,并存储到局部变量表的索引为0的位置。
操作数栈:[]
局部变量表:0: 0

2: iinc 0 by 1
将局部变量表索引为0的位置的值增加1。
操作数栈:[]
局部变量表:0: 1

5: iload_0
将局部变量表索引为0的位置的值压入操作数栈中:[1]
局部变量表:0: 1

6: invokestatic #15 <com/dhx/db/Main.returnInt : (I)I>
调用com.dhx.db.Main.returnInt方法,该方法接受一个整数参数,并返回一个整数值。
操作数栈:[1]
局部变量表:0: 1

9: istore_1
将操作数栈顶的值(调用方法返回的整数值)弹出,并存储到局部变量表的索引为1的位置。
操作数栈:[]
局部变量表:0: 1, 1: 返回值

10: iinc 0 by 1
将局部变量表索引为0的位置的值增加1。
操作数栈:[]
局部变量表:0: 2, 1: 返回值

13: getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>
从静态字段java.lang.System.out中获取PrintStream对象,并将其压入操作数栈中:[PrintStream]
局部变量表:0: 2, 1: 返回值

16: iload_0
将局部变量表索引为0的位置的值压入操作数栈中:[PrintStream, 2]
局部变量表:0: 2, 1: 返回值

17: invokevirtual #16 <java/io/PrintStream.println : (I)V>
调用PrintStream对象的println方法,该方法接受一个整数参数,并无返回值。
操作数栈:[PrintStream]
局部变量表:0: 2, 1: 返回值

20: iload_1
将局部变量表索引为1的位置的值压入操作数栈中:[PrintStream, 返回值]
局部变量表:0: 2, 1: 返回值

21: ireturn
将操作数栈顶的值(返回值)作为方法的返回值返回。
操作数栈:[返回值]
局部变量表:0: 2, 1: 返回值

22: astore_2
将操作数栈顶的值(异常对象)弹出,并存储到局部变量表的索引为2的位置。
操作数栈:[]
局部变量表:0: 2, 1: 返回值, 2: 异常对象

23: iinc 0 by 1
将局部变量表索引为0的位置的值增加1。
操作数栈:[]
局部变量表:0: 3, 1: 返回值, 2: 异常对象

26: getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>
从静态字段java.lang.System.out中获取PrintStream对象,并将其压入操作数栈中:[PrintStream]
局部变量表:0: 3, 1: 返回值, 2: 异常对象

29: iload_0
将局部变量表索引为0的位置的值压入操作数栈中:[PrintStream, 3]
局部变量表:0: 3, 1: 返回值, 2: 异常对象

30: invokevirtual #16 <java/io/PrintStream.println : (I)V>
调用PrintStream对象的println方法,该方法接受一个整数参数,并无返回值。
操作数栈:[PrintStream]
局部变量表:0: 3, 1: 返回值, 2: 异常对象

33: aload_2
将局部变量表索引为2的位置的值(异常对象)压入操作数栈中:[PrintStream, 3, 异常对象]
局部变量表:0: 3, 1: 返回值, 2: 异常对象

34: athrow
将操作数栈顶的值(异常对象)抛出。
操作数栈:[异常对象]
局部变量表:0: 3, 1: 返回值, 2: 异常对象

首先将常量0压入操作数栈中并存储到局部变量表的第0个索引位置。然后通过iinc指令将局部变量表的第0个索引位置的值增加1。

随后,将第0个索引位置的值压入操作数栈中,并传递给方法调用invokestatic。

调用方法后,将调用返回值存储到局部变量表的第一个索引位置。

然后,通过getstatic和iload指令将PrintStream对象和返回值压入操作数栈中,并调用PrintStream的println方法打印出返回值。

最后,在异常处理块中,将异常对象存储到局部变量表的第二个索引位置,并通过athrow指令抛出该异常。

值传递的问题

众所周知Java的参数传递类型只有值传递 , 那么对于文章开头的代码 , 如果我们换成包装类或者自定义类型会怎么样?

换成包装类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Main {


public static void main(String[] args) {
Integer res = addInteger();
System.out.printf("Integer::main : %d\n",res);
}

static Integer addInteger() {
Integer val = 0;
try {
val++;
return returnInteger(val);
} finally {
val++;
System.out.printf("Integer::finally : %d\n",val);
}
}
static Integer returnInteger(Integer val){
System.out.printf("Integer::return : %d\n",val);
return val;
}
}

运行结果为

1
2
3
Integer::return : 1
Integer::finally : 2
Integer::main : 1

这是因为Java的包装类在进行计算的时候会自动进行拆解和封装

换成Person类

接着我们替换为自定义的类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
public class Main {

public static void main(String[] args) {
Person val = addPerson();// @698
System.out.printf("Person::address:%s ,main : %d\n",Integer.toHexString(val.hashCode()),val.age);
}

static Person addPerson(){
Person val = new Person();
try {
val.age++;
return returnVal(val);
} finally {
val.age++;
System.out.printf("Person::address:%s ,finally : %d\n",Integer.toHexString(val.hashCode()),val.age);
}
}

static Person returnVal(Person val){
System.out.printf("Person::address:%s ,return : %d\n",Integer.toHexString(val.hashCode()),val.age);
return val;
}

static class Person{
int age;
String name ="person";
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Person person = (Person) o;
return age == person.age && Objects.equals(name, person.name);
}
@Override
public int hashCode() {
// 通过hashcode获取地址, 这里排除了age防止产生干扰
return Objects.hash(name);
}
}
}

运行结果为

1
2
3
Person::address:c4e39b74 ,return : 1
Person::address:c4e39b74 ,finally : 2
Person::address:c4e39b74 ,main : 2

反编译代码为

1
2
3
4
5
6
7
8
9
10
11
12
13
static Person addPerson() {
Person val = new Person();

Person var1;
try {
++val.age;
var1 = returnVal(val);
} finally {
++val.age;
System.out.printf("Person::address:%s ,finally : %d\n", Integer.toHexString(val.hashCode()), val.age);
}
return var1;
}

通过上面的代码也就能非常形象的理解具体的过程了 , 返回的时候会复制一个 引用 , 但是由于Java只有值传递, 所以操作的始终都是同一个Person对象 , 因此最后main方法中打印的结果还是2

实际的执行顺序还是

return => finally => 上一级方法

不过需要注意的就是如果是操作对象需要考虑到值传递的问题

参考