概述
java 虚拟机在 java 程序执行过程中会将内存划分为若干个不同的数据区域,如下图所示:
程序计数器
程序计数器是一块较小的内存空间,他存储了正在执行的虚拟机字节码指令的地址。 通过改变程序计数器来选取下一条需要执行的字节码指令。 这块内存被每个线程私有,且是唯一不会抛出 OutOfMemoryError 的内存区域。
java 虚拟机栈
java 虚拟机栈描述的是 java 方法执行的内存模型,每个方法在执行的同时都会创建一个栈帧,用于存储方法局部变量表、操作数、动态链接、方法出口等信息。 这部分内存是线程私有的,其生命周期与线程生命周期相同,通过虚拟机运行参数中的 -Xss 参数可以设定他的大小。 每调用一个方法,则这个方法在线程私有的 java 虚拟机栈中创建一个栈帧,方法调用结束则出栈。 虚拟机栈空间中的局部变量表以 Slot 为单位进行内存的分配,每个 Slot 32bit,他在编译期间完成内存的分配。
如果线程请求的栈深度大于虚拟机所允许的深度,就会抛出 StackOverflowError 异常。 如果虚拟机可以动态扩展,在动态扩展时无法申请到足够内存,则会抛出 OutOfMemoryError 异常。
本地方法栈
本地方法栈与虚拟机栈非常像,他只为 native 方法提供存储空间,有的实现中本地方法栈与虚拟机栈使用的是相同的内存空间。 通过 -Xoss 参数可以设置本地方法栈的大小。 在 HotSpot 虚拟机中,并不区分虚拟机栈和本地方法栈,所以 -Xoss 是无效的。 如果栈帧过大或是栈容量太小,就会抛出 StackOverflowError 异常。
java 堆
对于大多数应用来说,java 堆是 jvm 管理的内存中最大的一块。 java 的堆是所有线程共享的内存区域,在虚拟机启动时创建,他的唯一用途就是创建对象的实例,几乎所有对象实例都在这里分配。 这里也是垃圾收集器管理的主要区域,因此,java 堆也常常称为 GC 堆。 java 堆中还可细分为新生代和老年代,甚至进一步细分为很多空间,从分配角度划分,java 堆可以划分出多个线程私有的分配缓冲区(TLAB) 按照 java 虚拟机规范,java 堆处于物理上不连续的内存空间中,在逻辑上他是连续的整块内存。 当前主流 jvm 实现中,java 堆是可扩展大小的,通过 -Xmx 和 —Xms 控制,可以参看 jvm 参数配置相关的日志。 如果堆无法扩展则会抛出 OutOfMemoryError 异常。
方法区
方法区与 java 堆一样,是各个线程共享的内存区域,他用于存储已经被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。 java 虚拟机规范中把方法区描述为堆的一个逻辑部分,但是他却有一个别名 — Non-Heap,“非堆”。 方法区与 java 堆一样,不要求使用连续内存,但在逻辑上是连续的,并且可以无需使用垃圾收集,有的实现中,会对常量池进行内存的回收,对类型进行卸载。 可以通过 -XX:PermSize 和 -XX:MaxPermSize 限制方法去的大小。 如果方法区无法满足内存分配需求,就会抛出 OutOfMemoryError 异常。
参考资料
《深入理解 Java 虚拟机 — jvm 高级特性与最佳实践(第 2 版)》。