JVM内存模型
大约 4 分钟
JVM内存模型
核心理论
1.1 JVM内存区域划分
JVM内存分为线程私有区域和线程共享区域:
- 线程私有:程序计数器、虚拟机栈、本地方法栈
- 线程共享:堆、方法区(JDK 8后为元空间)
- 直接内存:不属于JVM运行时数据区,但被NIO使用
1.2 各内存区域详解
- 程序计数器:当前线程执行字节码的行号指示器,唯一不会OOM的区域
- 虚拟机栈:每个方法调用创建栈帧,存储局部变量表、操作数栈、动态链接、方法出口
- 本地方法栈:为Native方法服务,HotSpot将其与虚拟机栈合二为一
- 堆:对象实例分配的主要区域,GC的主要战场,可分为新生代(Eden、Survivor)和老年代
- 方法区:存储类元信息、常量池、静态变量等,JDK 8用元空间替代永久代,元空间使用本地内存
- 运行时常量池:方法区的一部分,存储编译期生成的字面量和符号引用
1.3 内存分配策略
- 对象优先在Eden区分配:大对象(如长字符串、数组)直接进入老年代
- 长期存活对象进入老年代:通过年龄计数器判断,默认15岁晋升
- 动态对象年龄判定:Survivor区中相同年龄对象总和超过一半,年龄大于等于该年龄的对象进入老年代
- 空间分配担保:Minor GC前检查老年代最大可用连续空间是否大于新生代对象总大小
代码实践
2.1 内存区域OOM异常演示
public class MemoryOOMDemo {
// 堆内存溢出
static class HeapObject {}
public static void heapOOM() {
List<HeapObject> list = new ArrayList<>();
while (true) {
list.add(new HeapObject());
}
}
// 虚拟机栈溢出
private static int stackDepth = 0;
public static void stackSOF() {
try {
stackDepth++;
stackSOF();
} catch (StackOverflowError e) {
System.out.println("栈深度: " + stackDepth);
throw e;
}
}
// 方法区(元空间)溢出
public static void metaspaceOOM() {
while (true) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(HeapObject.class);
enhancer.setUseCache(false);
enhancer.setCallback((MethodInterceptor) (obj, method, args1, proxy) -> proxy.invokeSuper(obj, args1));
enhancer.create();
}
}
public static void main(String[] args) {
// 分别运行以下方法观察不同OOM异常
// heapOOM(); // -Xms20m -Xmx20m
// stackSOF(); // -Xss128k
// metaspaceOOM(); // -XX:MetaspaceSize=10m -XX:MaxMetaspaceSize=10m
}
}
2.2 对象内存布局分析
public class ObjectLayoutDemo {
public static void main(String[] args) {
Object obj = new Object();
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
// 演示对象头Mark Word变化
synchronized (obj) {
System.out.println("加锁后对象布局:");
System.out.println(ClassLayout.parseInstance(obj).toPrintable());
}
}
}
依赖:需要引入jol-core依赖
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.16</version>
</dependency>
设计思想
3.1 内存分代回收思想
基于对象生命周期的不同,将堆分为新生代和老年代:
- 新生代:对象存活时间短,回收频繁,采用标记-复制算法
- 老年代:对象存活时间长,回收较少,采用标记-清除或标记-整理算法 这种分代设计提高了垃圾回收效率,降低了内存碎片
3.2 栈帧结构设计
栈帧包含:
- 局部变量表:存储方法参数和局部变量
- 操作数栈:方法执行的工作区
- 动态链接:指向运行时常量池的方法引用
- 方法出口:方法正常或异常退出的位置 栈帧的设计实现了方法的独立执行环境和高效调用
3.3 元空间替代永久代的设计考量
JDK 8用元空间替代永久代的原因:
- 永久代大小难以确定,容易OOM
- 元空间使用本地内存,受系统内存限制
- 便于HotSpot与JRockit合并,统一内存管理
避坑指南
4.1 堆内存参数设置
- -Xms与-Xmx:建议设置为相同值,避免运行时动态调整堆大小
- 新生代与老年代比例:默认1:2,可通过-XX:NewRatio调整
- Survivor区比例:默认Eden:S0:S1=8:1:1,可通过-XX:SurvivorRatio调整
4.2 大对象处理
- 避免创建过大对象(如几MB的数组),可分块处理
- 使用-XX:PretenureSizeThreshold参数控制大对象直接进入老年代
- 注意NIO直接内存使用,避免DirectMemoryOOM
4.3 元空间优化
- JDK 8+通过-XX:MetaspaceSize和-XX:MaxMetaspaceSize控制元空间大小
- 避免频繁创建和卸载类,如动态代理、反射等场景
- 监控元空间使用,防止内存泄漏
深度思考题
- 为什么Survivor区需要两个(From和To)?
- 对象在内存中的布局是怎样的?对象头包含哪些信息?
- 什么是TLAB(Thread Local Allocation Buffer)?它的作用是什么?
思考题回答:
Survivor区设计两个是为了实现复制算法,解决内存碎片问题。每次GC时,将Eden和From区存活对象复制到To区,清空Eden和From区,然后From和To区角色互换。这样保证总有一个Survivor区为空,避免内存碎片。
对象内存布局包括:对象头(Mark Word、Klass Pointer)、实例数据和对齐填充。对象头包含哈希码、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID等信息。
TLAB是线程本地分配缓冲区,是堆中线程私有的一小块区域。它的作用是避免多线程竞争,提高对象分配效率。线程优先在TLAB中分配对象,TLAB用完后才使用共享区域分配。