graph LR
A["JVM内存"]
B["运行时数据区"]
C["线程私有"]
D["线程共享"]
E["虚拟机栈"]
F["方法区"]
G["堆"]
A --> B
A --> 直接内存
B --> C
B --> D
C --> E
C --> 本地方法栈
C --> 程序计数器
D --> G
D --> F
JVM在执行Java程序时会为其划分一块内存区域,这块内存区域又可以划分为运行时数据区和直接内存。
运行时数据区中有线程私有(虚拟机栈、本地方法栈和程序计数器)和线程共享(堆和方法区)两大类,线程共享意味着线程不安全,在并发编程中需要注意。
程序计数器
- 当前线程所执行字节码的行号指示器;
- JVM的多线程是通过线程切换并分配时间片执行来实现的。在任何一个时刻,一个 cpu 只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器;
- 不会产生 OOM 异常。
虚拟机栈
-
描述 Java 方法执行的内存模型:每个方法在执行时会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接和方法出口等,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。
-
通常说的 Java 栈是指虚拟机栈,或虚拟机栈中的局部变量表部分。
-
发生在这个区域的两种异常状况:
- StackOverflowError:线程请求的栈深度大于虚拟机所允许的深度时抛出;
- OutOfMemoryError:虚拟机栈在动态扩展过程中无法获取足够内存时抛出。
局部变量表
- 局部变量表存放了编译期可知的各种基本数据类型、对象引用类型和 returnAddress 类型。
- 局部变量表所需要的内存空间在编译期间完成分配,当进入一个方法时,这个方法需要在帧中分配多大的局部变量空间是完全确定的,在方法运行期间不会改变局部变量表的大小。
本地方法栈
- 与虚拟机栈作用类似,但本地方法栈是为 Native 方法服务。
- HotSpot VM 直接把本地方法栈和虚拟机栈合二为一,使用同一块区域调用栈帧的压入和弹出操作,可以减少数据结构复杂性,提高代码执行的一致性和性能,在内存管理和异常处理上也更加简洁。
堆
- 几乎所有对象实例和数组都要在堆上分配,堆是JVM管理的最大一块内存,也是垃圾回收的主要区域。(栈上分配和标量替换不会在堆上分配对象)
- 根据分代收集算法,现代收集器将堆内存划分为老年代和年轻代,默认占比为 2:1。年轻代被分为 Eden 区和 Survivor 区,Survivor 区又被分为 S0 和 S2 两个区域。Eden 区和 S0 区和 S1 区默认占比 8:1:1。划分的目的是更好地回收内存,更快的分配内存,与存放的内容无关。
- 如果堆中没有内存完成实例分配,并且堆也无法再扩展时,将会抛出 OOM 异常。
graph LR
A["老年代(占比2/3)"]
B["年轻代(占比1/3)"]
C["Eden(占比4/5)"]
D["Survivor(占比1/5)"]
E["S0(占比1/2)"]
F["S1(占比1/2)"]
堆 --> A
堆 --> B
B --> C
B --> D
D --> E
D --> F
方法区
- 非堆,用于存储被 JVM 加载的类信息、常量、静态常量、即时编译器编译后的代码等数据,是存储类元数据的重要区域;
- 在 HotSpot JVM 中曾通过“永久代”管理方法区,以便使用与堆相同的垃圾收集机制,但这会带来内存溢出等问题。后来使用本地内存实现方法区;
- 方法区的内存回收主要针对常量池和类的卸载,但回收效果不理想,且类型卸载的条件十分严格。如果方法区内存不足,会抛出 OOM;
- 运行时常量池是方法区中的一个区域,用于存储已被加载的类文件中的字面量和符号引用,并且在运行时可以动态添加新的常量,若常量池无法扩展则可能导致 OOM。
直接内存
- 直接内存不在 JVM 规范定义的内存区域中,但实践中重要且可能导致 OOM;
- JDK 1.4 引入的 NIO 允许 Java 通过 DirectByteBuffer 直接访问堆外内存,提高性能;
- 直接内存分配不受Java堆大小限制,但受限于系统总内存和寻址能力;
- 在配置虚拟机参数时容易忽略直接内存,导致各个内存区域总和大于物理内存限制,进而导致 OOM。