记得18年第一次读周志明著的《深入理解Java虚拟机》让我有种醐醍灌顶的感觉,后又读了几遍,每次都感觉自己受益匪浅,但还是像之前说的一样,能说出来的才是自己的。因此,这也是为什么虽然网上现在有很多JVM,我却还坚持写的原因。要真正理解JVM,还是推荐看这本书。
首先,最好的永远是官方文档。有关JVM的内存结构的官方文档:Chapter 2. The Structure of the Java Virtual Machine2.5之前的可以忽略不看,讲的是Java中的数据类型,以及它们的取值范围,占用字节数大小等。
2.5 的 Run-Time Data Areas,就是JVM运行时数据区域了,也就是这篇文章的重点,Java运行时的内存结构。
这篇文章主要也是参考官方文档和《深入理解Java虚拟机》编写的。
首先总览一下运行时的数区域。
运行时数据区域
该图是网上找的,侵权删。
我直接就按上图的顺序来介绍了。
- 方法区(Method Area)
- 堆(Heap)
- 虚拟机栈(Java Virtual Machine Stacks)
- 程序计数器(The pc Register)
- 本地方法栈(Native Method Stacks)
- 还有一个官方文档中说的, 运行时常量池(Run-Time Constant Pool),虽然不在上述的五个中,但是也很重要,它是属于方法区的。
方法区(Method Area)
方法区,首先明确它是线程共享的,它是已编译代码的存储区域,类似于操作系统进程的“文本”碎片。它存储已被加载的类信息,运行时常量池,静态变量,常量,即时编译器编译后的代码等数据。
方法区在最开始虚拟机开启的时候创建。方法区在逻辑上是堆的一部分,但是一般会把它和堆区分开。可以选择不进行垃圾收集或压缩。方法区可以是固定大小的,也可以是伸缩的,需要的时候变大,不需要就收缩。同时,方法区的物理存储位置不需要连续。
该区域在内存不足时会抛出OOM(OutOfMemory)异常。
运行时常量池(Run-Time Constant Pool)
由于运行时常量池是方法区的一部分,因此,就放在它下面讲了。class文件中除了有类的版本信息、字段、方法,接口等信息,还有就是常量池(constant pool),它包含几种类型的常量(所以叫常量池),范围为从编译期生成的各种数值常量到运行时使用的字段和字段引用。
堆(Heap)
堆,也是线程共享的,它是所有存放类实例和数组的地方,也是GC的主要区域。
虽然堆中区域并不细分,但是一般还是会按GC分为:新生代,老年代,其中新生代,又会被分成一个Eden区和两个From Survivor区和To Survivor区等。
和方法区一样,在最开始虚拟机开启的时候创建。它的物理存储位置不需要连续,堆的大小也是可以固定或伸缩。也会在内存空间不足时抛出OOM异常。
虚拟机栈(Java Virtual Machine Stacks)
虚拟机栈,与方法区和堆不同,是线程私有的,它与线程同一时间创建,生命周期与线程相同。在每个方法被调用的时候都会创建一个frame,这个frame用于存储动态链接,方法出口(方法返回值、捕获异常),每个frame有自己的局部变量表,操作数栈,以及当前方法所属类的运行时常量池的引用。
解释一下一些名词吧。
frame
通常叫栈帧,它在方法被调用的时候创建,在方法结束或者调用其它方法时停用,一个线程中活动的栈桢只有一个(称为当前帧),即正在执行的一个,不过想也想得到,一个线程不可能同时执行两个方法。在方法结束之后,如果有返回值的话,会将其结果返回给前一个栈桢,然后销毁。
局部变量表
其实就是一个存储局部变量的数组,这个数组的长度在编译时就能确认,数组中每个value,存放的是各种基本数据类型,引用,或者returnAddress。其中long和double会占用连续的两个value。有一个实用的场景,可以更深刻的刻画印象。在方法被调用的时候,参数的传递就是使用这个数组。
操作数栈
这个还是可以顾名思义的,包含一个后进先出(LIFO)堆栈。它的最大深度在编译时就确定了,一般将常量或变量从局部变量或字段加载到操作数堆栈上。其他指令从操作数堆栈中获取操作数,对它们进行操作,并将结果push回操作数堆栈。操作数栈还用于准备传递给方法的参数以及接收方法结果。
程序计数器(The pc Register)
程序计数器,是线程私有的,占用的内存也比较小。具体作用可看,官方给的解释:
If that method is not native, the pc register contains the address of the Java Virtual Machine instruction currently being executed. If the method currently being executed by the thread is native, the value of the Java Virtual Machine’s pc register is undefined.
如果是Java方法,那么记录的是正在执行的虚拟机字节码指令地址,如果是Native方法,那么为Undefined。
它也是唯一不会抛出OOM异常的。
本地方法栈(Native Method Stacks)
它和虚拟机栈类似,区别只在于虚拟机栈为Java方法,而本地方法栈为虚拟机使用到的Native方法服务。
总结
其实篇幅也不大,简单的总结一下吧。
- 线程私有: 程序计数器,虚拟机栈和本地方法栈。
线程共享:方法区、堆。
线程私有是不会引起线程安全问题的(这不废话)。 - OOM(OutOfMemory)异常,OOM异常就是内存溢出,简单来说就是剩余的内存小于需要的内存导致的。说到内存溢出,就必须提一下内存泄漏了,简单的说,内存泄漏是应该释放的内存没有释放。内存泄漏有一个比较经典的例子:ThreadLocal,这里就不展开讲ThreadLocal了。内存泄漏通常会导致内存溢出,毕竟内存就那么大,你占着块位置又不释放,久而久之可用内存就会越来越小,可不就内存溢出了吗。
OK,这篇文章就到这里。
最后,谢谢观看。本人才疏学浅,如有错误之处,欢迎指正,共同进步。