深入理解Java虚拟机(一)-JVM内存模型
本文是在阅读了《深入理解Java虚拟机》第2版之后,根据自己的了解记录的心得,如果有哪里理解的不正确欢迎指正和讨论。这里主要讨论关于JVM内存模型相关的理解。
jvm也就是俗称的java虚拟机,通常来说在java程序在运行时程序内部的数据都是保存在jvm的内存中,那么jvm是怎么保存运行中的java程序的数据的。我们可以看看jvm运行时的数据区都有哪些。
JVM运行时数据区域
运行时数据区JDK1.8之后的差异(方法区)
方法区
方法区:是所有线程共享的一块内存区域,用来存储已经被虚拟机加载的类信息,常量,静态常量,即时编译器编译后的代码等数据。方法区也有一个别名叫做‘非堆’(Non-Heap)和堆区分开来。
方法区是各个线程共享的内存区域,在虚拟机启动的时候创建
永久代 or 元数据区
其实无论是永久带还是元数据区都是对于方法区的实现,方法区是JVM运行时数据区的规范,JDK1.8使用元数据区实现,JDK1.7使用永久带实现。 对于Hotspot虚拟机来说
- 在JDK1.7的时候把方法区叫做‘永久代’,会发生我们常见的 java.lang.OutOfMemoryError: PermGen space 异常,可以通过启动参数来控制方法区的大小:
-XX:PermSize 设置方法区最小空间 -XX:MaxPermSize 设置方法区最大空间
- 在JDK1.8使用元数据区取代了永久代。但是方法区的概念还是存在的,也就是说JDK1.8中时使用元空间来实现方法区的,元空间的本质和永久代类似,都是对JVM规范中方法区的实现。
–XX:MetaspaceSize 设置元数据区空间
- 不过元空间与永久代之间最大的区别在于元数据空间并不在虚拟机中,而是使用本地内存。 JDK8之后就没有永久代,并且将老年代与元空间剥离。元空间放置于本地的内存中,因此元空间的最大空间就是系统的内存空间了,从而不会再出现像永久代的内存溢出错误了。一般对64 位服务器JVM来说默认值为21MB。
JDK1.8 之后的方法区为何变化如此之大?
主要由于永久代(PermGen)内存经常会溢出,引发java.lang.OutOfMemoryError: PermGen,为了让JVM不再经常出现这样的OOM错误,在JDK1.8中采用元数据区来实现方法区。
还有需要注意一点的是虽然用元数据替代永久代并不是说完全完全,因此,你还必须监控你的内存消耗情况,因为一旦发生泄漏,会占用你的大量本地内存,有可能会出现更加糟糕情况
运行时常量池
运行时常量池是方法区的一部分,class信息除了类的版本,字段,方法,接口等描述信息,还有一部分是常量池,用于存储在编译器生成的各种字面量和符号引用,这部分内容在字节码文件被加载到内存中之后,方法区会保存这些信息,从而变成运行时常量池。
堆
jvm堆是虚拟机中最大的一块内存,在jvm启动的时候创建,是所有线程共享的一块内存区域,对象实例和数组都在堆上面分配的。关于堆内存的更多解释参考深入理解Java虚拟机(三)-JVM内存分配和回收
栈
虚拟机栈是一个线程执行的区域,线程私有随着线程创建而创建,也就是说Java线程的运行状态由虚拟机栈来保存。 线程中每一个被线程执行的方法,是当前线程对应栈中的一个栈帧,也就是说每一个方法对应一个栈帧,线程中方法的调用对应该方法栈帧在栈中入栈和出栈的过程。 每个栈帧都包涵:
- 局部变量表:方法中定义的局部变量以及方法的参数,基本数据类型,对象的引用(对象的引用地址或者代表对象的句柄);
- 操作数栈:以压栈和出栈的方式存储操作数的;
- 动态链接:每个栈帧都包含一个指向运行时常量池中该栈帧中所属方法的引用,持有这个引用是为了支持方法调用过程中的动态链接;
- 方法出口:记录当本方法执行完之后,回到主方法时改从哪一行执行;
通过一个小例子看下,将TryJVM.java通过
javac -g:vars TryJVM.java进行编译,然后通过javap -v TryJVM.class来查看编译后的类信息。注意如果直接使用javac TryJVM.java生成的class文件是看不到局部变量表的
public class TryJVM {
public static void main(String[] args) {
int i = 10;
int j = 20;
int k = i + j;
TryJVM jvm = new TryJVM();
jvm.test1();
}
public void test1(){
int a = 0;
a++;
int b = a + 10;
}
javac -g:vars TryJVM.java & javap -v TryJVM.class
➜ git:(master) ✗ javac -g:vars TryJVM.java
➜ git:(master) ✗ javap -v TryJVM.class
Classfile /Users/qujianfei/code/smartops/gateway/src/main/java/com/anchnet/smartops/TryJVM.class
Last modified 2022-5-3; size 518 bytes
MD5 checksum 1eef8d84aa51969167f606a04783127f
public class com.anchnet.smartops.TryJVM
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
#1 = Methodref #5.#24 // java/lang/Object."<init>":()V
#2 = Class #25 // com/anchnet/smartops/TryJVM
#3 = Methodref #2.#24 // com/anchnet/smartops/TryJVM."<init>":()V
#4 = Methodref #2.#26 // com/anchnet/smartops/TryJVM.test1:()V
#5 = Class #27 // java/lang/Object
#6 = Utf8 <init>
#7 = Utf8 ()V
#8 = Utf8 Code
#9 = Utf8 LocalVariableTable
#10 = Utf8 this
#11 = Utf8 Lcom/anchnet/smartops/TryJVM;
#12 = Utf8 main
#13 = Utf8 ([Ljava/lang/String;)V
#14 = Utf8 args
#15 = Utf8 [Ljava/lang/String;
#16 = Utf8 i
#17 = Utf8 I
#18 = Utf8 j
#19 = Utf8 k
#20 = Utf8 jvm
#21 = Utf8 test1
#22 = Utf8 a
#23 = Utf8 b
#24 = NameAndType #6:#7 // "<init>":()V
#25 = Utf8 com/anchnet/smartops/TryJVM
#26 = NameAndType #21:#7 // test1:()V
#27 = Utf8 java/lang/Object
{
public com.anchnet.smartops.TryJVM();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=1, locals=1, args_size=1
0: aload_0
1: invokespecial #1 // Method java/lang/Object."<init>":()V
4: return
LocalVariableTable:
Start Length Slot Name Signature
0 5 0 this Lcom/anchnet/smartops/TryJVM;
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=5, args_size=1
0: bipush 10
2: istore_1
3: bipush 20
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: new #2 // class com/anchnet/smartops/TryJVM
13: dup
14: invokespecial #3 // Method "<init>":()V
17: astore 4
19: aload 4
21: invokevirtual #4 // Method test1:()V
24: return
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 args [Ljava/lang/String;
3 22 1 i I
6 19 2 j I
10 15 3 k I
19 6 4 jvm Lcom/anchnet/smartops/TryJVM;
public void test1();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: iconst_0
1: istore_1
2: iinc 1, 1
5: iload_1
6: bipush 10
8: iadd
9: istore_2
10: return
LocalVariableTable:
Start Length Slot Name Signature
0 11 0 this Lcom/anchnet/smartops/TryJVM;
2 9 1 a I
10 1 2 b I
}
class文件中的Constant pool是常量池,当字节码文件被加载到内存中之后,方法区会存放字节码文件中的Constant pool,这时候就成为了运行时常量池。 这里以main方法指令解析
public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=5, args_size=1
0: bipush 10
2: istore_1
3: bipush 20
5: istore_2
6: iload_1
7: iload_2
8: iadd
9: istore_3
10: new #2 // class com/anchnet/smartops/TryJVM
13: dup
14: invokespecial #3 // Method "<init>":()V
17: astore 4
19: aload 4
21: invokevirtual #4 // Method test1:()V
24: return
LocalVariableTable:
Start Length Slot Name Signature
0 25 0 args [Ljava/lang/String;
3 22 1 i I
6 19 2 j I
10 15 3 k I
19 6 4 jvm Lcom/anchnet/smartops/TryJVM;
- 0:将int类型常量10压入操作数栈顶
- 2:将int类型值存储局部变量表slot1
- 3:将int类型常量20压入操作数栈顶
- 5:将int类型值存储局部变量slot2
- 6:从局部变量表中slot1装载int类型如栈
- 7:从局部变量表中slot2装载int类型如栈
- 8:执行int类型的加法结果入栈
- 9:将int类型值存储局部变量slot3
- 10:在jvm堆中为变量jvm分配空间,并将地址压入操作数栈顶
- 13:复制操作数栈顶值并压入栈顶(此时操作栈上有两个连续相同的TryJVM对象地址)
- 14:从操作栈顶弹出一个this的引用(两个连续TryJVM对象地址中靠近栈顶的一个)并调用实例话方法:()v
- 17:将栈顶剩余的TryJVM对象存入局部变量表slot4中
- 19:将局部变量表中slot4中引用类型jvm地址推送至操作数栈顶
- 21:调用虚方法,根据jvm对象地址查询其test1()方法并执行 (21:invokevirtual指向#4,#4在常量池中为TryJVM.test1:()V,因此21:invokevirtual可以最终找到TryJVM的test1()并且执行)
- 24:结束方法
- LocalVariableTable: 局部变量表
扩展 - 关于iconst和bipush
当int的值不一样,字节码处理整形的指令也不同
- 取值-1~5采用iconst指令
- 取值-128~127采用bipush指令
- 取值-32768~32767采用sipush指令
程序计数器
程序计数器记录线程或者程序执行的位置,保证多线程切换之后可以在回到线程之前执行的位置,各个线程都有各自的程序计数器,不同线程间的程序计数器互不影响线程私有。
- 如果线程执行的是java的方法,那么计数器记录的是正在执行的虚拟机字节码执行地址;
- 如果执行的是native方法,那么计数器值为空(undefined); 这部分内存区域是唯一一个在jvm中没有规定任何outOfMemoryError的情况的区域,也就是说如果outOfMemory不会发生在程序计数器。
本地方法栈
类似于java虚拟机栈,区别于jvm栈的是,jvm栈服务与jvm的java方法,本地方法栈服务与jvm用到的native方法,例如equals & hashcode
直接内存
这部分内存其实不是jvm的运行时数据区的一部分,而是机器的物理内存,主要的一个应用场景是NIO中的DirectByteBuffer来操作这一部分,例如我们读取一些大文件的时候,可以用直接内存,这样可以避免直接使用jvm内存而造成的内存不足产生outOfMemory,但是这部分内存大小受制于物理机本身内存大小限制,如果机器本身内存不足,也会报outOfMemoryError
方法区和栈指向堆的情况
- 栈指向堆
例如在栈帧中有一个变量是一个类的实例
Object obj = new Object(),我们知道这时候变量是存放在栈中的,但是对象对应的变量实际指向堆中的对象。 - 方法区指向堆
方法区中存放静态,常量等信息,例如
private static Object obj = new Object(),这时候obj这个静态变量存放在方法区中,但是他实际指向堆中的对象。