运行时数据区有哪些部分组成?
1. 堆
1.1. 堆中存放的是什么?
对象实例存放在Java堆[1](Java Heap)中。堆是线程共享的。
1.2. 堆内存是如何划分的?
堆内存被划分为新生代和老年代两部分,新生代又被划分为一个Eden
区和两个大小相同的Survior
区。将堆内存划分为新生代和老年代主要是为了提高垃圾收集的效率,基于以下两个假说:
- 弱分代假说:大部分对象都是朝生夕灭的。
- 强分代假说:熬过越多次垃圾收集的对象越难消亡。
虚拟机参数:
-Xms
:指定虚拟机堆内存的初始大小。-Xmx
:指定虚拟机堆内存的最大大小。
1.3. 什么是内存泄漏和内存溢出?
- 内存泄漏(Memory Leak)是指应该被回收的对象没有被回收,内存泄漏会导致内存溢出。
- 内存溢出(Memory Overflow)是指堆内存空间不足,无法创建新的对象实例。
可以通过转储并分析堆的内存快照文件来排查内存泄漏和内存溢出产生的原因。
1.4. 堆可能会发生的异常?
当创建的对象实例占用的内存大小大于堆的内存大小时,会抛出OutOfMemoryError: Java heap space
。
import lombok.extern.slf4j.Slf4j;
@Slf4j
public class OOMJavaHeapSpaceTest {
public static void main(String[] args) {
// -Xms10m -Xmx10m
log.info("Size of heap: {}MB", 10);
byte[] _5MB = new byte[1024 * 1024 * 5];
log.info("Assigned: {}MB", _5MB.length / (1024 * 1024));
byte[] _3MB = new byte[1024 * 1024 * 3];
log.info("Assigned: {}MB", _3MB.length / (1024 * 1024));
byte[] _2MB = new byte[1024 * 1024 * 2];
log.info("Assigned: {}MB", _2MB.length / (1024 * 1024));
}
}
在上面的程序中,我们指定虚拟机的堆内存大小为10MB,并创建内存大小分别为5MB,3MB,2MB的数组,运行结果如下:
Size of heap: 10MB
Assigned: 5MB
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at com.remeio.upsnippet.jvm.runarea.heap.OOMJavaHeapSpaceTest.main(OOMJavaHeapSpaceTest.java:13)
2. 方法区
2.1. 方法区中存放的是什么?
类的元数据信息、编译后的代码、静态变量、运行时常量池存放在方法区[2](Method Area)中。方法区是线程共享的。
2.2. 永久代和元空间有什么区别?
在JDK1.7及之前的版本中,方法区的实现是永久代(PermGen);在JDK1.8及之后的版本中,方法区的实现是元空间(Metaspace)。两者有以下区别:
- 存储区域不同:永久代占用的是堆内存;元空间占用的是本地内存,不受堆内存大小的限制。元空间大小取决于本地内存大小,解决了永久代大小不易指定的问题。
- 存储内容不同:相比于JDK1.6的永久代,JDK1.7的永久代将运行时常量池和静态变量移到了堆中;而JDK1.8则去除了永久代,使用元空间代替。
2.3. 方法区可能会发生的异常?
- 在JDK1.8之前的版本中,当永久代内存不足时,会抛出
OutOfMemoryError: PermGen space
。 - 在JDK1.8及之后的版本中,当元空间内存不足时,会抛出
OutOfMemoryError: Metaspace
。
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
public class OOMMetaspaceTest {
public static void main(String[] args) {
// jdk1.8
// -verbose:class -XX:MaxMetaspaceSize=10m -XX:MetaspaceSize=10M
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(OOMMetaspaceTest.class);
// 不使用缓存
enhancer.setUseCache(false);
enhancer.setCallback((MethodInterceptor) (o, method, objects, methodProxy) -> method.invoke(o, objects));
enhancer.create();
}
}
}
在上面的程序中,我们使用CGlib
不停地创建代理类,直到发生OOM
,运行结果如下:
...
[Loaded com.remeio.upsnippet.jvm.runarea.methodarea.OOMMetaspaceTest$$EnhancerByCGLIB$$7cd09a91_548 from file:/D:/project/remeio/upsnippet/upsnippet-jvm/target/classes/]
[Loaded com.remeio.upsnippet.jvm.runarea.methodarea.OOMMetaspaceTest$$EnhancerByCGLIB$$7cd09a91_549 from file:/D:/project/remeio/upsnippet/upsnippet-jvm/target/classes/]
[Loaded com.remeio.upsnippet.jvm.runarea.methodarea.OOMMetaspaceTest$$EnhancerByCGLIB$$7cd09a91_550 from file:/D:/project/remeio/upsnippet/upsnippet-jvm/target/classes/]
Exception in thread "main" java.lang.OutOfMemoryError: Metaspace
虚拟机参数:
-XX:PermSize
:在JDK1.7及之前的版本中,指定永久代的初始大小。-XX:MaxPermSize
:在JDK1.7及之前的版本中,指定永久代的最大大小。-XX:MetaspaceSize
:在JDK1.8及之后的版本中,指定元空间的初始大小。-XX:MaxMetaspaceSize
:在JDK1.8及之后的版本中,指定元空间的最大大小。
2.4. 运行时常量池和字符串常量池有什么区别?
在JDK1.8及之后的版本中,运行时常量池和字符串常量池存放在堆中;每个类都会有一个运行时常量池,而整个虚拟机只有一个字符串常量池。
3. Java虚拟机栈
3.1. Java虚拟机栈中存放的是什么?
每个线程都有一个Java虚拟机栈[3](Java Stack),调用方法时会压入栈帧[6](Frame),方法执行完毕时会弹出栈帧,Java虚拟机栈是线程私有的。每个栈帧包括以下内容:
- 局部变量表。
- 操作数栈。
- 动态连接。
- 方法返回地址等。
3.2. Java虚拟机栈可能会发生的异常?
- 当创建过多线程不能创建新的线程时,会抛出
OutOfMemoryError: unable to create native thread
。 - 当Java虚拟机栈内存不足时,会抛出
StackOverflowError
。
public class StackOverflowTest {
private static int count = 1;
public static void main(String[] args) {
try {
// -Xss108k
pushStackFrame();
} catch (Error e) {
System.err.println(e);
System.out.println(count);
}
}
private static void pushStackFrame() {
int variable = 1;
int variable2 = 1;
int variable3 = 1;
int variable4 = 1;
int variable5 = 1;
int variable6 = 1;
count++;
pushStackFrame();
}
}
在上面的程序中,我们不断向main
线程的Java虚拟机栈中压入栈帧,直到抛出StackOverflowError
,运行结果如下:
java.lang.StackOverflowError
683
栈帧越小,每个线程拥有的Java虚拟机栈所能容纳的栈帧越多。若上面的程序的pushStackFrame
方法中只声明一个变量,运行结果如下:
java.lang.StackOverflowError
985
虚拟机参数:
-Xss
:指定每个线程的Java虚拟机栈的大小。
4. 本地方法栈
本地方法栈[4]和Java虚拟机栈类似,在调用本地方法时,会向本地方法栈中压入栈帧。本地方法栈是线程私有的。
5. 程序计数器
程序计数器[5]用来记录当前线程执行方法的位置,用于线程切换回当前线程时,继续执行方法。程序计数器是线程私有的。
6. 直接内存
6.1. 什么是直接内存?
直接内存不属于堆内内存,是直接向操作系统申请的内存,用于NIO。
6.2. 直接内存和堆内存有什么区别?
- 在内存分配方面,由于堆内存是提前分配,所以比操作直接内存快。
- 在内存读写方面,操作堆内存,需要将本地内存转为堆内存,再将堆内存转为本地内存;而直接内存是直接操作本地内存,比堆内存高效。
7. JDK工具
7.1. 如何查看当前系统中有哪些正在运行的Java进程?
通过jps -mlvV
命令可以打印当前系统中正在运行的Java进程。
$ jps -lvVm
2195793 sun.tools.jps.Jps -lvVm -Dapplication.home=/usr/lib/jvm/java-8-openjdk-amd64 -Xms8m
710359 application-0.0.1-SNAPSHOT.jar
7.2. 如何查看Java进程的虚拟机参数?
通过jinfo -flags <pid>
命令可以打印指定Java进程的虚拟机参数。
$ jinfo -flags 917356
VM Flags:
-XX:CICompilerCount=4 -XX:InitialHeapSize=534773760 -XX:MaxHeapSize=8552185856 -XX:MaxNewSize=2850553856 -XX:MinHeapDeltaBytes=524288 -XX:NewSize=1782
57920 -XX:OldSize=356515840 -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
7.3. 如何查看Java进程的堆和方法区的内存信息?
通过jmap -heap <pid>
命令可以打印指定Java进程的堆的配置和内存信息。
$ jmap -heap 23060
Attaching to process ID 23060, please wait...
Debugger attached successfully.
Server compiler detected.
JVM version is 25.361-b09
using thread-local object allocation.
Parallel GC with 8 thread(s)
Heap Configuration:
MinHeapFreeRatio = 0
MaxHeapFreeRatio = 100
MaxHeapSize = 4244635648 (4048.0MB)
NewSize = 88604672 (84.5MB)
MaxNewSize = 1414529024 (1349.0MB)
OldSize = 177733632 (169.5MB)
NewRatio = 2
SurvivorRatio = 8
MetaspaceSize = 21807104 (20.796875MB)
CompressedClassSpaceSize = 1073741824 (1024.0MB)
MaxMetaspaceSize = 17592186044415 MB
G1HeapRegionSize = 0 (0.0MB)
Heap Usage:
PS Young Generation
Eden Space:
capacity = 66584576 (63.5MB)
used = 6658704 (6.3502349853515625MB)
free = 59925872 (57.14976501464844MB)
10.000370055671752% used
From Space:
capacity = 11010048 (10.5MB)
used = 0 (0.0MB)
free = 11010048 (10.5MB)
0.0% used
To Space:
capacity = 11010048 (10.5MB)
used = 0 (0.0MB)
free = 11010048 (10.5MB)
0.0% used
PS Old Generation
capacity = 177733632 (169.5MB)
used = 0 (0.0MB)
free = 177733632 (169.5MB)
0.0% used
3189 interned Strings occupying 261688 bytes.
如上示例,从打印信息的Heap Configuration
部分可以看到该Java进程的堆的配置:
- 堆的最大内存
MaxHeapSize
为4048MB。 - 新生代的最大内存
MaxNewSize
为1349MB。 - 老年代与新生代内存大小的比值
NewRatio
为2。 Eden
区与Survivor
区的比值SurvivorRatio
为8。- 元空间的最大内存
MaxMetaspaceSize
为16GB。
从打印信息的Heap Usage
部分可以看到该Java进程堆分配的内存为254MB,包括以下两个部分:
- 新生代
PS Young Generation
分配的内存为84.5MB,包括以下三个部分:Eden Space
分配的内存为63.5MB。From Space
分配的内为10.5MB。To Space
分配的内存为10.5MB。
- 老年代
PS Old Generation
分配的内存大小为169.5MB。
7.4. 如何为堆创建快照?
通过jmap -dump:format=b,file=<file> <pid>
命令可以转储堆的内存快照文件。通过jhat <file>
命令或MAT
,JProfiler
等工具分析堆的内存快照文件。下图是使用MAT
分析程序是否发生了内存泄漏:

虚拟机参数:
-XX:+HeapDumpOnOutOfMemoryError
:开启当程序出现OOM
时自动转储堆的内存快照文件。-XX:HeapDumpPath=<file>
:指定堆的内存快照文件的输出路径。
7.5. 如何查看Java进程的堆栈信息?
通过jstack <pid>
命令查看Java进程的堆栈信息。
$ jstack 917356
2023-09-09 13:54:47
Full thread dump OpenJDK 64-Bit Server VM (25.372-b07 mixed mode):
...
"main" #1 prio=5 os_prio=0 tid=0x0000024c391bf800 nid=0xdff20 waiting on condition [0x000000e53c9ff000]
java.lang.Thread.State: TIMED_WAITING (sleeping)
at java.lang.Thread.sleep(Native Method)
at com.remeio.upsnippet.jvm.runarea.heap.HeapDemo.main(HeapDemo.java:7)
"VM Thread" os_prio=2 tid=0x0000024c5d736800 nid=0xe0004 runnable
"GC task thread#0 (ParallelGC)" os_prio=0 tid=0x0000024c391d6800 nid=0xdffb0 runnable
"GC task thread#1 (ParallelGC)" os_prio=0 tid=0x0000024c391d8000 nid=0xdff18 runnable
...
"VM Periodic Task Thread" os_prio=2 tid=0x0000024c5fe1f800 nid=0xe0040 waiting on condition
JNI global references: 12
7.6. 如何查看Java进程的gc信息?
通过jstat -gc <pid> <interval> <count>
打印Java进程的gc信息。
> jstat -gc 23060 1000 3
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
10752.0 10752.0 0.0 0.0 65024.0 6502.6 173568.0 0.0 4480.0 775.7 384.0 76.0 0 0.000 0 0.000 0.000
10752.0 10752.0 0.0 0.0 65024.0 6502.6 173568.0 0.0 4480.0 775.7 384.0 76.0 0 0.000 0 0.000 0.000
10752.0 10752.0 0.0 0.0 65024.0 6502.6 173568.0 0.0 4480.0 775.7 384.0 76.0 0 0.000 0 0.000 0.000
7.7. 可视化工具有哪些?
可视化工具有jconsole、visualvm、Jprofiler、Arthas等。
8. 参考文档
- 1.Chapter 2. The Structure of the Java Virtual Machine - Heap ↩
- 2.Chapter 2. The Structure of the Java Virtual Machine - Method Area ↩
- 3.Chapter 2. The Structure of the Java Virtual Machine - Java Virtual Machine Stacks ↩
- 4.Chapter 2. The Structure of the Java Virtual Machine - Native Method Stacks ↩
- 5.Chapter 2. The Structure of the Java Virtual Machine - The pc Register ↩
- 6.Chapter 2. The Structure of the Java Virtual Machine - Frames ↩