Java虚拟机(一) —— 运行时数据区
JVM构成
运行时数据区
Java 虚拟机的内存空间分为 5 个部分:
- 程序计数器
- Java 虚拟机栈
- 本地方法栈
- 堆
- 方法区
程序计数器(PC)
为什么需要程序计数器
因为Java虚拟机的多线程是通过线程轮流切换
并分配处理器执行时间
的方式来实现的。任意时刻,一个处理器只执行一条指令,为了进程切换后恢复到正确的执行位置,所以才有了程序计数器
作用
记录当前线程的执行字节码的位置,线程私有,也就是每个线程都有一个单独的计数器来记录。
Java虚拟机栈
描述Java方法执行的内存模型。
方法在执行的时候会创建一个栈帧,方法的调用到完成,对应着栈帧
在虚拟机栈中入栈
到出栈
的过程。
一个线程的方法调用可能很长,很多方法都处于执行状态。对于执行引擎来说,只有处于栈顶的栈帧(当前栈帧
)才有效,与之相关联的方法是当前方法
。
栈帧用于存储局部变量表
、操作数栈
、动态链接
、方法出口
等信息。
局部变量表
局部变量表存放编译期可知的基本数据类型
、对象引用
和return Address类型
局部变量表所需要的内存空间在编译期间完成分配。
特点
- 线程私有
- 线程请求的栈深度大于JVM允许深度,抛出
StackOverflowError
异常 - 虚拟机栈动态扩展时,无法申请足够的内存,抛出
OutOfMemoryError
异常
堆
堆的特点
- 线程共享
- 垃圾回收的主要场所
用来存放对象的内存空间,几乎所有的对象都存储在堆中。
对象在堆内创建的流程
对象在创建时,会放到Eden区,当Eden区满了,会进行MinorGC,生存的对象会放到Survivor From区中,若Survivor From区中无可用内存,
则再进行MonorGC,对Eden、From区进行清理,存活的对象放入Survivor To区,To就转换为From区,原先的From区就变为To区。
- MinorGC时,大对象直接进入老年代
- 大对象若From区放不下,会直接放到老年代,避免大对象在From和To区来回复制,影响性能。
- 长期存活的对象直接进入老年代
- 对象每经过一次MinorGC,年龄+1,增加到15岁,进入老年代,
-XX:MaxTenuringThreshold
设置年龄阈值
- 对象每经过一次MinorGC,年龄+1,增加到15岁,进入老年代,
对象创建分配程序演示
使用vm参数
-Xms20M
-Xmx20M
-XX:+PrintGCDetails
程序实例1:创建的对象首先分配到Eden区
public class ObjectAllocationDemo {
public static void main(String[] args) {
byte[] a = new byte[5 * 1024];
}
}
运行结果
对象填满Eden区
public class ObjectAllocationDemo {
public static void main(String[] args) {
byte[] a = new byte[1024* 2 * 1024];
byte[] b = new byte[1024*2 * 1024];
byte[] c = new byte[300* 1024];
}
}
程序实例2:Eden区满了以后,会进行一次MinorGC,将存活对象放到From区,From区放不下,就放到Old代空间。
public class ObjectAllocationDemo {
public static void main(String[] args) {
byte[] a = new byte[1024* 2 * 1024];
byte[] b = new byte[1024*2 * 1024];
byte[] c = new byte[300* 1024];
byte[] d = new byte[90* 1024];
}
}
运行结果
栈操作流程分析(一)
public class TestStack {
public int math() {
int a = 1;
int b = 2;
int c = a + b;
return c;
}
public static void main(String[] args) {
TestStack testStack = new TestStack();
System.out.println(testStack.math());
}
}
执行javac TestStack.java
和javap -c -l TestStack > teststack.txt
后
得到teststack.txt
Compiled from "TestStack.java"
public class com.fonxian.base.jvm.TestStack {
public com.fonxian.base.jvm.TestStack();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
public int math();
Code:
// 将int类型常量1压入栈
// 当我把代码改成int a = 9时,得到的结果是bipush 9 istore_1
0: iconst_1
// istore 将int类型值存入局部变量1,这里的意思是放到第1个局部变量中
1: istore_1
2: iconst_2
3: istore_2
// iload_1 从局部变量1中装载int类型值
4: iload_1
5: iload_2
// iadd 执行int类型的加法
6: iadd
7: istore_3
8: iload_3
// ireturn 从方法中返回int类型的数据
9: ireturn
LineNumberTable:
line 10: 0
line 11: 2
line 12: 4
line 13: 8
public static void main(java.lang.String[]);
Code:
// new 创建一个新对象
0: new #2 // class com/fonxian/base/jvm/TestStack
// 复制栈顶部一个字长内容
3: dup
// invokespecial 根据编译时类型来调用实例方法
4: invokespecial #3 // Method "<init>":()V
7: astore_1
8: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
11: aload_1
12: invokevirtual #5 // Method math:()I
15: invokevirtual #6 // Method java/io/PrintStream.println:(I)V
18: return
LineNumberTable:
line 17: 0
line 18: 8
line 19: 18
}
流程示意
执行第0行代码iconst_1
时
执行第1行代码istore_1
时
执行第5行代码iload_2
时
执行第6行代码iadd
时
栈操作流程分析(二)
public class IntegerDemo {
public void test() {
int a = 10000;
Integer b = 1000;
System.out.println(a);
System.out.println(b);
}
public static void main(String[] args) {
new IntegerDemo().test();
}
}
反编译后得到
Compiled from "IntegerDemo.java"
public class com.fonxian.base.jvm.IntegerDemo {
public com.fonxian.base.jvm.IntegerDemo();
Code:
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LineNumberTable:
line 7: 0
public void test();
Code:
0: sipush 10000
3: istore_1
4: sipush 1000
7: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
10: astore_2
11: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
14: iload_1
15: invokevirtual #4 // Method java/io/PrintStream.println:(I)V
18: getstatic #3 // Field java/lang/System.out:Ljava/io/PrintStream;
21: aload_2
22: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/Object;)V
25: return
LineNumberTable:
line 11: 0
line 12: 4
line 13: 11
line 14: 18
line 15: 25
public static void main(java.lang.String[]);
Code:
0: new #6 // class com/fonxian/base/jvm/IntegerDemo
3: dup
4: invokespecial #7 // Method "<init>":()V
7: invokevirtual #8 // Method test:()V
10: return
LineNumberTable:
line 18: 0
line 19: 10
}
JVM指令
当int取值-15采用iconst指令,取值-128127采用bipush指令,取值-3276832767采用sipush指令,取值-21474836482147483647采用 ldc 指令。
指令 | 作用 |
---|---|
iconst_1 | 将int类型常量1压入(操作数)栈 |
istore_1 | istore_1 将int类型值存入局部变量1,局部变量1可能是任何int类型数 |
ireturn | 从方法中返回int类型的数据 |
iadd | 执行int类型的加法 |
bipush | 将一个8位带符号整数压入栈 |
参考文档
《深入理解Java虚拟机》
关于作者
后端程序员,五年开发经验,从事互联网金融方向。技术公众号「清泉白石」。如果您在阅读文章时有什么疑问或者发现文章的错误,欢迎在公众号里给我留言。