从Java字节码分析return与finally的执行流程
本篇注意探讨的问题是 finally与return代码哪个先执行?
先说结论 : 先执行return中的代码
执行顺序的问题 , 非常容易解决 , 请查看下面的代码
1 | public static void main(String[] args) { |
运行的结果是
1 | Int::return : 1 |
执行顺序的问题解决了, 那么这里你可能会问 , 为什么 main中返回的结果是1呢?
还是先说结论,
由于Java中的参数传递只有值传递 , return的时候会先拷贝一个 val 的引用 , main函数中获取到的就是这个val的引用(对于基本数据类型是直接copy的)
这里我们可以查看反编译的代码
1 | static int add() { |
可以看到最后返回的并不是我们预先定义的val , 而是一个新的变量 => var1。
JVM内存区域
如果已经了解请直接跳到下一部分
为了方便理解字节码部分的内容, 这里简单叙述
变量类型
Java虚拟机栈
与程序计数器一样,Java 虚拟机栈(后文简称栈)也是线程私有的,它的生命周期和线程相同,随着线程的创建而创建,随着线程的死亡而死亡。
除了一些 Native 方法调用是通过本地方法栈实现的,其他所有的 Java 方法调用都是通过栈来实现的(也需要和其他运行时数据区域比如程序计数器配合)。
方法调用的数据需要通过栈进行传递,每一次方法调用都会有一个对应的栈帧被压入栈中,每一个方法调用结束后,都会有一个栈帧被弹出。
栈由一个个栈帧组成,而每个栈帧中都拥有:局部变量表、操作数栈、动态链接、方法返回地址。
Java 方法有两种返回方式,一种是 return 语句正常返回,一种是抛出异常。
不管哪种返回方式,都会导致栈帧被弹出。也就是说, 栈帧随着方法调用而创建,随着方法结束而销毁。无论方法正常完成还是异常完成都算作方法结束。
除了 StackOverFlowError
错误之外,栈还可能会出现OutOfMemoryError
错误,这是因为如果栈的内存大小可以动态扩展, 如果虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError
异常。
- 例如下面的deep()就会导致 StackOverFlowError
1 | public static void main(String[] args) { |
-
下面的代码就会出现 OutOfMemoryError ( 8byte , 1MB大概是 )
通过Runtime获取到当前最大的内存容量, 接着定义 超过内存大小的数组 , 出现了内存溢出错误。 8Byte * 1<<30 => 1<<33 Byte
1 | public static void main(String[] args) { |
局部变量表 主要存放了编译期可知的各种数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型,
它不同于对象本身,可能是一个指向对象起始地址的引用指针,也可能是指向一个代表对象的句柄或其他与此对象相关的位置)。
和数据结构上的栈类似,两者都是先进后出的数据结构,只支持出栈和入栈两种操作。
操作数栈 主要作为方法调用的中转站使用,用于存放方法执行过程中产生的中间计算结果。另外,计算过程中产生的临时变量也会放在操作数栈中。
动态链接 主要服务一个方法需要调用其他方法的场景。Class 文件的常量池里保存有大量的符号引用比如方法引用的符号引用。当一个方法要调用其他方法,需要将常量池中指向方法的符号引用转化为其在内存地址中的直接引用。动态链接的作用就是为了将符号引用转换为调用方法的直接引用,这个过程也被称为 动态连接
线程私有的:
- 程序计数器
- 虚拟机栈
- 本地方法栈
线程共享的:
- 堆 :
- 方法区 : 首先类编译后产生.class的字节码文件,执行类加载器把.class的字节码文件加载到方法区内存(方法的代码片段以及整个类的代码片段都是存储到方法区内存当中)
- 直接内存 (非运行时数据区的一部分)
堆
Java 虚拟机所管理的内存中最大的一块,堆是所有线程共享的一块内存区域,在虚拟机启动时创建。此内存区域的唯一目的就是存放对象实例,几乎所有的对象实例以及数组都在这里分配内存。
字节码解读
有了预备的JVM知识之后, 我们来解读字节码文件
这里通过IDEA jclasslib 插件来查看字节码
1 | 0 iconst_0 |
首先需要知道其中几个关键的 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 anint
, 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 | 0 iconst_0 |
iconst_0
:该指令将常量值 0 压入操作数栈顶。也就是初始化一个变量为 0。istore_0
:将栈顶的值存储到局部变量 0 (table[0])中。将变量 0 的初始值存储到局部变量表中。iinc 0 by 1
:将局部变量 0 (table[0])的值增加 1。这里是将变量 0 的值递增。iload_0
:将局部变量 0 (table[0])的值加载到栈顶,准备执行后续操作。istore_1
:将栈顶的值存储到局部变量 1 (table[1])中。getstatic #2 <java/lang/System.out : Ljava/io/PrintStream;>
:获取System.out
的静态字段,即标准输出流。这里用于获取标准输出流。iload_0
:将局部变量 0 (table[0])的值加载到栈顶,准备执行后续操作。invokedynamic #6 <makeConcatWithConstants, BootstrapMethods #1>
:动态调用makeConcatWithConstants
方法,并将栈顶的参数传递给该方法。这里调用了一个动态方法,并将栈顶的参数传递给这个方法。invokevirtual #5 <java/io/PrintStream.println : (Ljava/lang/String;)V>
:调用PrintStream.println
方法,将栈顶的字符串参数打印到标准输出。这里执行的是调用println
方法,将栈顶的字符串参数打印到标准输出。iload_1
:将局部变量 1 (table[1])的值加载到栈顶,准备作为方法的返回值。ireturn
:将局部变量 1 (table[1])的值作为方法的返回值。astore_2
:将栈顶的值存储到局部变量 2 中。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 | public class Main { |
运行结果为
1 | Integer::return : 1 |
这是因为Java的包装类在进行计算的时候会自动进行拆解和封装
换成Person类
接着我们替换为自定义的类
1 | public class Main { |
运行结果为
1 | Person::address:c4e39b74 ,return : 1 |
反编译代码为
1 | static Person addPerson() { |
通过上面的代码也就能非常形象的理解具体的过程了 , 返回的时候会复制一个 引用 , 但是由于Java只有值传递, 所以操作的始终都是同一个Person对象 , 因此最后main方法中打印的结果还是2
实际的执行顺序还是
return => finally => 上一级方法
不过需要注意的就是如果是操作对象需要考虑到值传递的问题