前言
Java 虚拟机定义了程序运行期间会使用到的运行时数据区域。其中有些区域随着虚拟机的启动而创建,随着虚拟机退出而销毁。另外一些区域则跟线程一一对应,随着线程的开始和结束而创建和销毁。
运行时数据区
程序计数器(Program Counter Register)
它可以看作是当前线程所执行的字节码的行号指示器。在 Java 虚拟机的概念模型里,字节码解释器工作时,就是通过改变计数器的值来选取下一条需要执行的字节码指令。它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖计数器来完成。
Java 中多线程的恢复,需要由程序计数器记录需要恢复的正确位置,所以每个线程都有自己独立的程序计数器。它占用的内存空间很小,但至少能保存一个 returnAddress 类型的数据或者一个与平台相关的本地指针的值。在线程执行 Java 方法时,计数器记录的是字节码指令的地址;执行本地(Native)方法时,计数器的值是 undefined。
程序计数器是唯一一个在《Java虚拟机规范》中没有规定任何 OutOfMemoryError
情况的区域。
Java 虚拟机栈(Java Virtual Machine Stack)
虚拟机栈是线程私有的,有着和所属的线程一样的生命周期。虚拟机栈描述着线程执行的方法,每当有一个方法被执行,就有一个栈帧被加入。当方法执行结束,栈帧也就被移除。
栈帧中保存了局部变量表,操作数栈、动态连接、方法出口等信息。局部变量表存放了编译器可知的各种基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用(reference 类型)和 returnAddress 类型(指向了一条字节码指令的地址)。局部变量表以局部变量槽(Slot)存储变量数据,64 位的 long 和 double 都会占用两个槽。在编译期间,局部变量表所需的内存大小就已经定下来了,所以在执行方法时,栈帧需要分配多大的局部变量空间是可以确定的。
《Java虚拟机规范》 规定这个区域的两种异常:
StackOverflowError
:线程请求的栈深度大于虚拟机所允许的深度。OutOfMemoryError
:如果虚拟机可以动态扩容(HotSpot 不可以),当栈扩展无法申请到足够的内存时。
本地方法栈(Native Method Stack)
与虚拟机栈相似,只是作用于本地方法。HotSpot 中直接把本地方法栈和虚拟机栈合二为一。
《Java虚拟机规范》 规定这个区域的两种异常:
StackOverflowError
:线程请求的栈深度大于虚拟机所允许的深度。OutOfMemoryError
:如果虚拟机可以动态扩容(HotSpot 不可以),当栈扩展无法申请到足够的内存时。
堆(Heap)
堆是虚拟机所管理的区域,内存最大的那一块,同时也是垃圾收集器管理的内存区域。在虚拟机启动时创建,所有线程共享的区域。几乎多有的对象、数组都在这块区域上分配。
对象的创建是频繁的,为了保证线程的安全,效率分配堆内存用于创建对象,堆可以划分出多个线程私有的分配缓冲区(Thread Local Allocation Buffer,TLAB)。
《Java虚拟机规范》 规定这个区域的异常:
OutOfMemoryError
:如果没有内存可以完成实例分配,而且堆无法扩展时。
方法区(Method Area)
线程共享的区域,用于存储已被虚拟机记载的类信息,常量,类变量、即时编译器编译后的代码缓存等数据。JDK 8 以前的 “永久代” 可以理解为方法区的实现。在 JDK 6 时,已经有计划放弃永久代,逐步采用本地内存(Native Memory)来实现。在 JDK 7 的 HotSpot,把原本存放在永久代的字符串常量池、静态变量等移出。到了 JDK 8,才完全废弃永久代,改用在本地内存中实现的元空间(Meta-space),将 JDK 7 还剩余的部分内容全部移到元空间。
《Java虚拟机规范》 规定这个区域的异常:
OutOfMemoryError
:如果没有内存可以分配时。
运行时常量池(Runtime Constant Pool)
运行时常量池是方法区的一部分。Class 文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项信息是常量池表(Constant Pool Table),用于存放编译器生成的各种字面量和符号引用,这部分内容将在类加载后,存放到方法区的运行时常量池中。
直接内存(Direct Memory)
直接内存不是虚拟机运行时数据区的一部分,不受堆大小的限制,但在各内存区域总和大于物理内存限制时,会导致 OutOfMemoryError
。
JDK 1.4 引入的 NIO,可以使用 Native 函数库直接分配堆外内存,然后通过存储在 Java 堆里的 DirectByteBuffer
对象操作这块内存,避免 Java 堆和 Native 堆来回复制数据,以提高性能。
参考
[1] 周志明. 深入理解Java虚拟机:JVM高级特性与最佳实践(第3版). 2.2 运行时数据区域
[2] Tim Lindholm/Frank Yellin/Gilad Bracha/Alex Buckley. Java虚拟机规范(Java SE 8版). 2.5 运行时数据区