线程共享:堆(内有字符串常量池)、方法区(内有运行时常量池)、直接内存(非运行时数据区的一部分)。
上图主要是 JDK 1.8 之后的 JVM 运行时数据区域。
程序计数器
指向程序当前运行的位置,可以看作是当前线程所执行的字节码的行号指示器。
解释器通过改变程序计数器来依次读取指令,实现程序的顺序、选择、循环、异常处理。多线程时因为其记录了当前线程执行的位置,从而线程切换时依然可以知道上次运行的位置。
程序计数器是线程私有的内存,唯一一个不会出现 OOM 的内存区域,生命周期随着线程的创建开始,随着线程结束而销毁。
虚拟机栈
存储函数运行过程中的局部变量表、操作数栈、动态链接、返回地址,每个方法的这一套记录都是以栈帧的形式在运行时被压入栈中,方法的生命周期结束之后,就会弹出其在栈中对应的栈帧并释放内存空间。
- 局部变量表:存放编译器可知的数据类型(boolean、byte、char、short、int、float、long、double)、对象引用。
- 操作数栈:存放方法中产生的中间计算结果和临时变量。
- 动态链接:用于方法之间的调用时,把符号引用转换为调用方法的直接引用。Class 文件的常量池中存放了大量的符号引用,当一个方法调用其他方法时,需要把常量池中指向方法的符号引用转化为其在内存地址中的直接引用。
虚拟机栈是线程私有的,生命周期和线程一致。线程请求栈深度超过最大深度,抛出 StackOverFlowError ;HotSpot 虚拟机中不允许动态扩展栈容量,申请站空间申请成功就不会 OOM,但是申请失败仍然会抛出 OOM。
本地方法栈
和虚拟机栈类似,不过本地方法栈是存储 C++ native 方法运行时候的栈区。在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一,本地方法执行的时候也会创建一个栈帧并入栈。
方法区
JVM 运行时数据区域一块逻辑区域,存储静态方法、静态变量以及 ClassLoader 等等全局的一些信息。其中有运行时常量池,存储编译期间产生的 Class 对象和字面量、运行期间产生的一部分符号引用转换为直接引用、字符串常量。
虚拟机要使用一个类的时候,先要读取并解析 Class 文件获取相关信息,再将信息存入到方法区。方法区会存储已经被虚拟机加载的类信息、字段信息、方法信息、常量、静态变量、即时编译器编译后的代码缓存等数据。
PS:JDK8 之后改为使用元空间实现方法区,JDK7 之后字符串常量池和静态变量已经从永久代改为放在堆中。
方法区的内存紧张的时候,会对小部分 Class 对象进行卸载,需要的时候重新加载,还会对字符串常量值进行部分回收。
JDK8 之前永久代的实现,受限于 JVM 内存的限制,而 JDK8 之后的元空间实现使用的是本地内存,受本机可用内存的限制。如果元空间不指定初始大小的话,会根据运行时的程序需求动态的调整大小,但是如果不指定元空间最大内存,随着越来越多类的创建,虚拟机可能会耗尽系统资源。
堆
虚拟机管理的最大一块内存,存储对象实例,实例的回收通过 GC 来管理。堆内存通常细分为新生代(Eden 和两个 Survivor)、老年代(Tenured)、永久代(PermGen 从 JDK8 开始永久代被元空间取代)。
一般来说,new 对象首先会分配在 Eden 区域,经过一次新生代垃圾回收之后,如果对象还存活,则分配到 S0 或 S1,并且对象的年龄加一(Eden 区 >> Survivor 区后对象初始年龄变为 1),年龄增加到阈值(默认 15,可以通过 -XX:MaxTenuringThreshold 来设置)就会被晋升到老年代中。
PS:“Hotspot 遍历所有对象时,按照年龄从小到大对其所占用的大小进行累加,当累加到某个年龄时,所累加的大小超过了 Survivor 区的一半,则取这个年龄和 MaxTenuringThreshold
中更小的一个值,作为新的晋升年龄阈值”。
堆事线程共享的,随着虚拟机启动被创建。最容易出现 OOM 的区域,java.lang.OutOfMemoryError: GC Overhead Limit Exceeded
:当 JVM 花太多时间执行垃圾回收并且只能回收很少的堆空间时,就会发生此错误。java.lang.OutOfMemoryError: Java heap space
:假如在创建新的对象时, 堆内存中的空间不足以存放新创建的对象, 就会引发此错误。可以使用 -Xmx:
参数控制堆的大小。
运行时常量池
JDK7 之前是存在于基于永久代实现的方法区中,JDK8 之后在基于元空间实现的方法区中(本地内存的一部分)。运行时常量池是方法区的一部分,受到方法区内存的限制,常量池申请不到内存时,会抛出 OOM 。
Class 文件中除了类的版本、字段、方法、接口等描述信息,还有存放编译器生成的各种字面量和符号引用的常量池表。
常量池表会在类加载之后存放到方法区的运行时常量池中。
- 符号引用:一种使用时可以无歧义地定位到目标的字面量,与虚拟机的实现无关,引用的目标不一定已经加载到虚拟机内存中。
- 直接引用:可以指向目标的指针、相对偏移量或者能间接定位到目标的句柄。与虚拟机的实现和内存布局有关,有了直接引用,那引用的目标必定已经加载在虚拟机内存中了。
字符串常量池
HotSpot 中字符串常量池是一个固定大小的 HashTable(容量可以通过 -XX:StringTableSize
控制),保存的是字符串(key)和字符串对象引用(value)的映射关系,其中字符串对象引用指向堆中的字符串对象。
JDK6 及之前字符串常量池放在基于永久代实现的方法区中,JDK7 开始字符串常量池和静态变量都从方法区移动到了 Java 堆中。
直接内存
通过 JNI 的方式在本地内存上分配的,并不是虚拟机运行时数据区的一部分,但是也可能导致 OOM 。
直接内存的分配不受 Java 堆的限制,但是会受到本机内存大小和处理器寻址空间的限制。