每日禅语
佛说,给你修路的,是你自己;埋葬你的,也是你自己;帮助你的,是你自己;毁灭你的,也是你自己;成就你的,自然还是你自己。所以佛说:自作自受,自性自度!
文章背景
在我们Java Coder编写java代码的时候,通过编译器点击运行,然后就可以输出想要的结果,但是如果你仔细询问这段代码是如何通过java代码,变成.class文件,再通过JVM执行输出结果的时候,大部分人都不能很清楚的说出运行机制和运行原理。或者对于大部分面试者来说,JVM了解的都是JVM八股文信息,但是你要深究为什么需要这些区域,这些区域如果关联起来运行的,大部分人都难以说出其重点。本文就是以此为基础,通过源码一步一步分析程序的底层运行原理。
通过一道面试题分析底层:输出的结构是多少?
public class Client {
public static void main(String[] args) {
int count = 0;
for (int i = 0 ; i <10 ;i ++){
count = count++;
System.out.println("count=" + count);
}
}
}
大部分人的第一印象答案:输出1-9,但是如果你让程序运行以后你会发现,结果和你想的大相径庭,竟然是输出的10个0,为什么结果和预期的差距这么大。后面的内容将会根据这段代码分析JVM的内存区域模型,从而让你知其然而知其所以然。
Java文件编译成class文件以后如何从磁盘读入内存中
Java字节码详细解析
javap -c .\Demo.class
public static void main(java.lang.String[]);
Code:
0: iconst_0 // 将常量 0 压入操作数栈,栈顶是 0
1: istore_1 // 将栈顶的 0 存储到局部变量 1 (即变量 count)
2: iconst_0 // 将常量 0 压入操作数栈,栈顶是 0
3: istore_2 // 将栈顶的 0 存储到局部变量 2 (即变量 i)
4: iload_2 // 加载局部变量 2 (即 i) 到操作数栈
5: bipush 10 // 将常量 10 压入操作数栈
7: if_icmpge 33 // 比较栈顶两个整数 (i和 10),如果 i >= 10,跳转到字节码地址 33 (即结束循环)
10: iload_1 // 加载局部变量 1 (即 count) 到操作数栈
11: iinc 1, 1 // 将局部变量 1 (count) 增加 1
14: istore_1 // 将栈顶的值 (更新后的 count) 存储回局部变量 1
15: getstatic #7 // 获取静态字段 System.out (即输出流)
18: iload_1 // 加载局部变量 1 (即 count) 到操作数栈
19: invokedynamic #13, 0 // 使用动态方法调用输出 (这里是用 makeConcatWithConstants 动态拼接字符串)
24: invokevirtual #17 // 调用 PrintStream 的 println 方法,将拼接的字符串打印出来
27: iinc 2, 1 // 将局部变量 2 (i) 增加 1
30: goto 4 // 跳转回到字节码地址 4,继续循环
33: return // 返回,结束程序
代码片段2:
public class Client {
public static void main(String[] args) {
int count = 0;
for (int i = 0 ; i <10 ;i ++){
count = ++count;
System.out.println("count=" + count);
}
}
}
public static void main(java.lang.String[]);
Code:
0: iconst_0
1: istore_1
2: iconst_0
3: istore_2
4: iload_2
5: bipush 10
7: if_icmpge 33
10: iinc 1, 1
13: iload_1
14: istore_1
15: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
18: iload_1
19: invokedynamic #13, 0 // InvokeDynamic #0:makeConcatWithConstants:(I)Ljava/lang/String;
24: invokevirtual #17 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
27: iinc 2, 1
30: goto 4
33: return
}
count++的字节码操作:
10: iload_1 // 加载局部变量 1 (即 count) 到操作数栈
11: iinc 1, 1 // 将局部变量 1 (count) 增加 1
14: istore_1 // 将栈顶的值 (更新后的 count) 存储回局部变量 1
总结:
先将局部变量表位置为1的count的值为0加载到操作数栈
再将局部变量1中的count + 1
再将操作数栈的值为1又更新到局部变量表1位置,虽然第二步已经将值加1,但是此时又给覆盖了,所以值始终为0
++count的字节码操作:
iinc 1, 1 // 将局部变量1的值加1
iload_1 // 将局部变量1的值加载到操作数栈
istore_1 // 将操作数栈顶的值存储到局部变量1
总结:
将局部变量表位置为1的count的值加1
再将局部变量表位置为1的count的值加载到操作数栈
再将操作数栈的值更新到局部变量表1位置,此时值就变为了1
操作数栈的作用:操作数栈的主要作用是提供一个地方来暂时存储数据,供执行各种指令时使用。它支持多种操作,如加载数据、执行算术运算、比较数据等。
局部变量表:用于存储方法的局部变量及一些相关信息。它在方法执行期间提供对局部变量的访问,并与操作数栈一起支持字节码的执行
局部变量表的数据结构
public class LocalVarExample {
public LocalVarExample() {
}
public void testMethod(int a, long b) {
int c = a + 1;
float d = (float)c * 2.5F;
}
}
局部变量表的数据结构
1.线性数组
- 数组结构 :
局部变量表以数组的形式实现,数组的每个元素称为一个 Slot(槽位) 。
- 每个槽位固定为 32 位(4 字节)。
- 一个槽位可以存储:
- 一个 32 位的基本数据类型(如
int
、float
等)。- 一个引用类型(如对象引用)。
- 一部分特殊数据(如
returnAddress
,用于jsr
和ret
指令)。- 64 位数据类型(
long
和double
)会占用两个连续的槽位。2. 索引定位
- 每个局部变量通过索引(Index)定位。
- 索引从 0 开始。
- 方法的第一个参数存储在索引为 0 的槽位,后续参数依次存储。
- 方法参数结束后,局部变量存储在后续槽位。
3. 数据类型和槽位
局部变量表中没有显式记录变量的类型,但类型信息隐含在字节码指令中:
- 例如,
iload_1
表示从索引 1 的槽位加载一个int
类型变量。局部变量表的内存分配
初始化:
- 局部变量表的大小在方法调用时确定,大小由方法的
max_locals
指定。max_locals
的值由编译器在编译时计算,表示方法所需的局部变量槽位的总数。参数分配:
- 静态方法:
- 方法参数按顺序分配到槽位。
- 实例方法:
- 索引 0 存储
this
引用,其他参数按顺序分配。局部变量分配:
- 方法体中声明的局部变量在调用时分配到空闲的槽位。
计算局部变量表的大小局部变量表的大小取决于:
- 方法的参数。
- 方法体内声明的局部变量。
- 变量类型的大小。
规则:
- 每个局部变量占用一个或多个 Slot :
- 32 位变量(
int
,float
,reference
,returnAddress
):占用 1 个 Slot。- 64 位变量(
long
,double
):占用 2 个连续的 Slot。- 静态方法不包含
this
引用。- 非静态方法的第一个 Slot 用于存储
this
引用。局部变量表的操作
加载和存储
局部变量表与操作数栈之间通过加载(
load
)和存储(store
)指令交互:
- 加载指令 (如
iload
,fload
,aload
):
- 从局部变量表加载数据到操作数栈。
- 存储指令 (如
istore
,fstore
,astore
):
- 从操作数栈存储数据到局部变量表。
2. 类型相关性
- 加载和存储指令类型敏感。例如:
iload_1
表示从槽位 1 加载int
类型。dload_2
表示从槽位 2 和 3 加载double
类型。