JVM 内存模型与垃圾回收机制详解

举报
yd_223002268 发表于 2026/09/01 08:45:42 2026/09/01
【摘要】 JVM 内存模型与垃圾回收机制详解 前言JVM(Java Virtual Machine)是 Java 生态的基石。理解 JVM 内存模型与垃圾回收(GC)机制,是写出高性能、低延迟、不泄漏的 Java 应用的前提。本文将从内存区域划分、对象内存布局、GC 算法、主流收集器、调优实战等维度,系统讲解 JVM 内存与 GC 的方方面面。 一、JVM 内存区域划分JVM 在运行时将内存划分为若...

JVM 内存模型与垃圾回收机制详解

前言

JVM(Java Virtual Machine)是 Java 生态的基石。理解 JVM 内存模型与垃圾回收(GC)机制,是写出高性能、低延迟、不泄漏的 Java 应用的前提。本文将从内存区域划分、对象内存布局、GC 算法、主流收集器、调优实战等维度,系统讲解 JVM 内存与 GC 的方方面面。


一、JVM 内存区域划分

JVM 在运行时将内存划分为若干区域,不同区域承担不同职责,生命周期也各不相同。

1.1 总览

┌─────────────────────────────────────────────┐
│                JVM 运行时内存                │
├──────────────┬──────────────────────────────┤
│  线程私有     │  线程共享                     │
├──────────────┼──────────────────────────────┤
│  程序计数器   │  堆(Heap)                   │
│  虚拟机栈     │  方法区(Method Area)        │
│  本地方法栈   │                               │
└──────────────┴──────────────────────────────┘

1.2 程序计数器(Program Counter Register)

  • 作用:记录当前线程执行的字节码行号,分支、循环、跳转、异常处理都依赖它。
  • 特性:线程私有,唯一不会发生 OOM 的区域(容量固定)。
  • 意义:线程切换后能恢复到正确执行位置。

1.3 虚拟机栈(VM Stack)

  • 作用:存放方法栈帧。每个方法调用创建一个栈帧,包含:

    • 局部变量表:存放基本类型、对象引用、returnAddress。
    • 操作数栈:执行字节码指令的工作区。
    • 动态链接:指向运行时常量池中该方法的引用。
    • 方法出口:方法返回信息。
  • 异常

    • StackOverflowError:栈深度超过限制(如无限递归)。
    • OutOfMemoryError:栈容量动态扩展时内存不足。
  • 线程私有,生命周期与线程相同。

1.4 本地方法栈(Native Method Stack)

  • 作用:为 Native 方法(C/C++ 实现)服务,与虚拟机栈作用类似。
  • 特性:线程私有,HotSpot 中与虚拟机栈合二为一。

1.5 堆(Heap)

  • 作用:存放对象实例和数组,是 GC 的主战场。
  • 特性:线程共享,所有线程都可访问。
  • 分代结构(HotSpot):
┌──────────────────────────────────────────┐
│                   堆                     │
├─────────────────┬────────────────────────┤
│  新生代          │  老年代                │
  (Young Gen)      (Old Gen)             │
├───────┬─────────┤                        │
│ Eden  │ Survivor│                        │
│       │ S0 | S1 │                        │
└───────┴─────────┴────────────────────────┘
  • 异常OutOfMemoryError: Java heap space

1.6 方法区(Method Area)

  • 作用:存放类信息、常量池、静态变量、JIT 编译后的代码。
  • 演进
    • JDK 7 及以前:HotSpot 用"永久代"(PermGen)实现,易 OOM。
    • JDK 8+:移除永久代,改用"元空间"(Metaspace),使用本地内存,容量受限于机器物理内存。
  • 异常OutOfMemoryError: Metaspace(JDK 8+)。

1.7 运行时常量池

  • 方法区的一部分,存放编译期生成的各种字面量和符号引用。
  • JDK 7+ 字符串常量池被移至堆中。

1.8 直接内存(Direct Memory)

  • 作用:NIO 使用 ByteBuffer.allocateDirect() 分配的堆外内存,避免堆内外数据拷贝,提升 IO 性能。
  • 特性:不归 JVM GC 直接管理(通过 Cleaner 机制回收),但受 -XX:MaxDirectMemorySize 限制。
  • 异常OutOfMemoryError: Direct buffer memory

二、对象内存布局

在 HotSpot 中,一个 Java 对象在堆中由三部分组成:

┌──────────────────────────────────┐
│  对象头(Object Header)         │
├──────────────────────────────────┤
│  实例数据(Instance Data)       │
├──────────────────────────────────┤
│  对齐填充(Padding)             │
└──────────────────────────────────┘

2.1 对象头

  • Mark Word:存放 hashCode、GC 分代年龄、锁状态标记、偏向锁线程 ID 等,64 位 JVM 占 8 字节。
  • 类型指针(Klass Pointer):指向类元数据,开启指针压缩时占 4 字节,否则 8 字节。
  • 数组长度:仅数组对象有,占 4 字节。

2.2 实例数据

存放对象字段值,字段顺序受字段类型、继承顺序影响,相同宽度的字段会被分配在一起(紧凑布局)。

2.3 对齐填充

对象大小必须是 8 字节的整数倍,不足则补齐。

2.4 计算示例

一个空对象 Object 在开启指针压缩的 64 位 JVM 上:

  • Mark Word:8 字节
  • Klass Pointer:4 字节
  • 实例数据:0 字节
  • Padding:4 字节
  • 合计:16 字节

这也是为什么频繁创建小对象会带来显著内存开销。


三、对象访问定位

JVM 规范未规定对象访问方式,HotSpot 默认使用直接指针

reference  ──→  对象实例(堆)  ──→  类元数据(方法区)

另一种方式是句柄

reference  ──→  句柄池  ──→  对象实例(堆)
                      ──→  类元数据(方法区)
方式 优势 劣势
直接指针 访问快,少一次间接 GC 移动对象时需更新所有引用
句柄 GC 移动对象只需改句柄 多一次间接寻址,访问稍慢

HotSpot 选择直接指针,牺牲 GC 效率换取访问速度。


四、GC 根节点(GC Roots)

GC 从 GC Roots 出发,通过引用链判断对象存活。可作为 GC Roots 的对象:

  1. 虚拟机栈中引用的对象(局部变量、参数)。
  2. 本地方法栈中 JNI 引用的对象。
  3. 方法区中类静态属性引用的对象。
  4. 方法区中常量引用的对象。
  5. 同步锁(synchronized)持有的对象。
  6. JVM 内部引用(如基本类型异常对象、类加载器)。

关键:GC Roots 是可达性分析的起点,对象只要从任一 GC Roots 可达,就不会被回收。


五、引用类型

Java 提供四种引用强度,影响 GC 回收时机:

引用类型 回收时机 典型用途
强引用 Object 永不回收(除非置 null) 普通对象引用
软引用 SoftReference 内存不足时回收 内存敏感缓存
弱引用 WeakReference 下次 GC 即回收 WeakHashMap、监听器
虚引用 PhantomReference 随时回收,get() 永远返回 null 跟踪对象被回收的时机

5.1 软引用缓存示例

public class ImageCache {
    private final Map<String, SoftReference<BufferedImage>> cache = new ConcurrentHashMap<>();

    public void put(String key, BufferedImage image) {
        cache.put(key, new SoftReference<>(image));
    }

    public BufferedImage get(String key) {
        SoftReference<BufferedImage> ref = cache.get(key);
        if (ref == null) return null;
        BufferedImage img = ref.get();
        if (img == null) {
            cache.remove(key); // 已被 GC 回收
        }
        return img;
    }
}

六、GC 算法

6.1 标记-清除(Mark-Sweep)

1. 标记:从 GC Roots 遍历,标记所有存活对象
2. 清除:遍历堆,回收未标记对象
  • 缺点:产生内存碎片,分配大对象时可能触发提前 GC。
  • 效率:随堆增大而下降。

6.2 标记-复制(Copying)

1. 将内存分为两块
2. GC 时把存活对象复制到另一块
3. 清空原区域
  • 优点:无碎片,分配快(指针碰撞)。
  • 缺点:可用内存减半。
  • 适用:新生代,因为新生代对象存活率低,复制开销小。

6.3 标记-整理(Mark-Compact)

1. 标记存活对象
2. 将存活对象向一端移动,整理紧凑
3. 清理边界外空间
  • 优点:无碎片,不浪费空间。
  • 缺点:移动对象需更新引用,STW 时间长。
  • 适用:老年代。

6.4 分代收集

HotSpot 采用分代策略,不同区域用不同算法:

  • 新生代:标记-复制(Eden + Survivor0 + Survivor1,比例默认 8:1:1)。
  • 老年代:标记-清除 或 标记-整理。

新生代 GC 流程

1. 新对象优先分配到 Eden
2. Eden 满触发 Minor GC
3. Eden + Survivor 中存活对象复制到另一 Survivor
4. 年龄 +1,达到阈值(默认 15)晋升老年代
5. 大对象直接进入老年代(避免在新生代来回复制)

七、GC 收集器详解

7.1 收集器对比总览

收集器 区域 算法 线程 STW 适用场景
Serial 新生代/老年代 复制/整理 单线程 全程 客户端、小堆
ParNew 新生代 复制 多线程 部分 配合 CMS
Parallel Scavenge 新生代 复制 多线程 部分 吞吐量优先
Parallel Old 老年代 整理 多线程 部分 吞吐量优先
CMS 老年代 标记-清除 多线程 部分 低延迟
G1 全堆 整体复制+局部整理 多线程 部分 大堆、低延迟
ZGC 全堆 染色指针+读屏障 多线程 <10ms 超低延迟、大堆
Shenandoah 全堆 转发指针 多线程 <10ms 超低延迟、大堆

7.2 CMS(Concurrent Mark Sweep)

目标:最小化 STW 时间,适合对延迟敏感的应用。

四个阶段

1. 初始标记(STW):标记 GC Roots 直接关联对象,速度快
2. 并发标记:从 GC Roots 遍历对象图,与用户线程并发
3. 重新标记(STW):修正并发标记期间变动的引用
4. 并发清除:清除未标记对象,与用户线程并发

优点:并发标记和清除,STW 时间短。

缺点

  • 基于"标记-清除",产生碎片。
  • 对 CPU 敏感,并发占用用户线程。
  • 无法处理浮动垃圾(并发标记期间产生的新垃圾)。
  • JDK 9 起被标记为废弃,JDK 14 移除。

7.3 G1(Garbage First)

目标:在可控延迟下处理大堆(4GB+),取代 CMS。

内存布局:放弃固定分代,将堆划分为多个大小相等的 Region:

┌──┬──┬──┬──┬──┬──┬──┬──┬──┬──┐
│EESOOHHOESE=Eden S=Survivor O=Old H=Humongous
├──┼──┼──┼──┼──┼──┼──┼──┼──┼──┤
│OEOSOEOOEO │
└──┴──┴──┴──┴──┴──┴──┴──┴──┴──┘

每个 Region 动态扮演 Eden/Survivor/Old 角色,Humongous 存放大对象。

GC 流程

1. 初始标记(STW):标记 GC Roots 直接关联,借助 Minor GC
2. 根区域扫描:扫描 Survivor 引用的 Old Region
3. 并发标记:遍历对象图
4. 重新标记(STW):处理 SATB 记录
5. 筛选回收(STW):按"回收收益"排序,回收价值高的 Region(Garbage First)

特点

  • 可预测停顿:-XX:MaxGCPauseMillis 设定目标停顿时间,G1 优先回收收益高的 Region。
  • 整体看是标记-整理,局部看是复制,无碎片。
  • JDK 9+ 默认收集器。

7.4 ZGC(Z Garbage Collector)

目标:亚毫秒级停顿,支持 TB 级堆。

核心技术

  • 染色指针:在 64 位指针的高位嵌入标记信息(Marked0/Marked1/Remapped/Finalizable)。
  • 读屏障:每次读取对象引用时检查指针颜色,按需转发。
  • 并发整理:对象移动与用户线程并发执行。

性能(JDK 15+ 生产可用):

  • 堆范围:8MB ~ 16TB。
  • STW 时间:< 10ms,且不随堆增大而增长。
  • 吞吐量开销:约 15%。

7.5 Shenandoah

由 RedHat 开发,与 ZGC 目标类似,核心用转写屏障(Brooks Pointer)实现并发整理:

  • 每个对象多一个转发指针,指向自身或新副本。
  • GC 移动对象时,先复制到新位置,再更新转发指针。
  • 用户线程访问时通过转发指针自动定位新对象。

八、内存分配策略

8.1 对象分配流程

1. TLAB 分配(Thread Local Allocation Buffer)
   └─ 成功 → 返回
2. Eden 区分配(指针碰撞)
   └─ 成功 → 返回
3. 触发 Minor GC
4. 大对象 → 直接老年代(-XX:PretenureSizeThreshold)
5. 老年代分配
   └─ 失败 → Full GC

8.2 逃逸分析与栈上分配

JIT 编译器通过逃逸分析判断对象是否逃逸出方法/线程:

  • 未逃逸:可在栈上分配,方法结束自动回收,无需 GC。
  • 可替换为标量:对象拆解为基本类型变量,进一步优化。
public int add() {
    Point p = new Point(1, 2);  // 未逃逸,可能栈上分配
    return p.x + p.y;
}

8.3 TLAB 优化

每个线程在 Eden 区预分配一小块(TLAB),线程内对象分配无需同步,避免指针碰撞的 CAS 开销。


九、常见 OOM 类型与排查

9.1 OOM 类型

类型 原因 解决方向
Java heap space 堆内存不足,对象过多 增大堆、排查泄漏、优化缓存
Metaspace 类元数据过多 检查动态代理、类加载器泄漏
GC overhead limit GC 回收效率低(98% 时间 GC 但回收 <2%) 排查内存泄漏
Direct buffer memory 堆外内存不足 检查 NIO 使用、限制 DirectMemory
StackOverflowError 栈深度超限 检查递归、增大栈
unable to create new native thread 线程数超系统限制 控制线程数、用线程池

9.2 排查工具

  • jmapjmap -histo:live <pid> 查看对象直方图。
  • jstackjstack <pid> 查看线程栈,定位死锁、阻塞。
  • jstatjstat -gcutil <pid> 1000 实时查看 GC 情况。
  • MAT:分析 heap dump,定位泄漏引用链。
  • Arthas:在线诊断,dashboardthreadheapdump 等命令。

9.3 内存泄漏典型场景

// 1. 静态集合持有对象
static List<Object> cache = new ArrayList<>();
cache.add(bigObject);  // 永不释放

// 2. 未关闭资源
InputStream is = new FileInputStream("file");
// 忘记 is.close(),Finalizer 延迟释放

// 3. 监听器未反注册
button.addListener(listener);
// 移除组件时未 removeListener

// 4. ThreadLocal 未清理
threadLocal.set(bigObject);
// 线程池中线程复用,ThreadLocal 持续累积

// 5. 内部类持有外部引用
class Outer {
    class Inner { }  // Inner 隐式持有 Outer.this
}

十、GC 调优实战

10.1 常用参数

参数 作用
-Xms / -Xmx 初始堆 / 最大堆
-Xmn 新生代大小
-XX:NewRatio 老年代:新生代 比例
-XX:SurvivorRatio Eden:Survivor 比例
-XX:MaxTenuringThreshold 晋升老年代年龄阈值
-XX:PretenureSizeThreshold 大对象直接进老年代阈值
-XX:+UseG1GC 启用 G1
-XX:MaxGCPauseMillis G1 目标停顿时间
-XX:+UseZGC 启用 ZGC
-XX:+HeapDumpOnOutOfMemoryError OOM 时自动 dump
-XX:HeapDumpPath dump 文件路径

10.2 调优原则

  1. 先保证不泄漏,再谈调优:架构与代码层面避免内存泄漏是根本。
  2. 堆大小适度:并非越大越好,堆大意味着 GC 单次耗时长。一般让堆在物理内存的 50%-70%。
  3. 新生代不宜过小:过小导致频繁 Minor GC,对象过早晋升老年代。
  4. 优先 G1/ZGC:JDK 11+ 默认 G1,大堆低延迟场景用 ZGC。
  5. 监控驱动调优:基于 GC 日志和监控数据调整,而非拍脑袋。

10.3 场景化配置

吞吐量优先(批处理)

-Xms4g -Xmx4g
-XX:+UseParallelGC
-XX:MaxGCPauseMillis=200
-XX:GCTimeRatio=99

延迟优先(Web 服务)

-Xms4g -Xmx4g
-XX:+UseG1GC
-XX:MaxGCPauseMillis=100
-XX:G1HeapRegionSize=8m

超低延迟(金融、游戏)

-Xms8g -Xmx8g
-XX:+UseZGC
-XX:ZCollectionInterval=5

10.4 GC 日志分析

启用 GC 日志(JDK 9+ 统一格式):

-Xlog:gc*=info:file=gc.log:time,uptime,level,tags:filecount=10,filesize=100m

关注指标:

  • 停顿时间Pause Young / Pause Old 的实际耗时。
  • GC 频率:相邻 GC 间隔,过密说明堆或新生代过小。
  • 回收效率:GC 前后堆使用量差值。
  • 晋升速率:老年代增长速度,过快说明新生代过小或 Survivor 不够。

十一、对象生命周期与 finalize 替代

11.1 finalize 的问题

@Override
protected void finalize() throws Throwable {
    // 不推荐!
}
  • 执行时机不确定,由 Finalizer 线程异步调用。
  • Finalizer 线程优先级低,可能导致对象长期不回收,引发 OOM。
  • 异常被吞没,无法感知。
  • JDK 9 起废弃,JDK 18 移除。

11.2 替代方案:Cleaner

public class FileResource implements AutoCloseable {

    private static final Cleaner cleaner = Cleaner.create();
    private final Cleaner.Cleanable cleanable;
    private final FileChannel channel;

    public FileResource(String path) throws IOException {
        this.channel = FileChannel.open(Path.of(path));
        this.cleanable = cleaner.register(this, new CleanupAction(channel));
    }

    @Override
    public void close() {
        cleanable.clean();  // 显式关闭
    }

    private static class CleanupAction implements Runnable {
        private final FileChannel channel;
        CleanupAction(FileChannel channel) { this.channel = channel; }

        @Override
        public void run() {
            try {
                channel.close();
            } catch (IOException e) {
                // 记录日志
            }
        }
    }
}

最佳实践:优先 try-with-resources 显式管理资源,Cleaner 作为兜底。


十二、实战案例:Full GC 频繁排查

12.1 现象

某服务每 10 分钟一次 Full GC,每次停顿 3 秒,接口 P99 抖动明显。

12.2 排查步骤

  1. 查看 GC 日志
jstat -gcutil <pid> 1000

发现老年代使用率持续在 90%+,每次 Full GC 后只下降到 80%。

  1. dump 堆分析
jmap -dump:format=b,file=heap.hprof <pid>

用 MAT 打开,发现 HashMap 占用 60% 堆,其中 50 万个 UserSession 对象。

  1. 定位代码
public class SessionManager {
    private static final Map<String, UserSession> sessions = new HashMap<>();

    public static void put(String token, UserSession session) {
        sessions.put(token, session);
    }
    // 缺少 remove / 过期清理逻辑
}

SessionManager 用静态 HashMap 缓存会话,从未清理过期会话,导致内存泄漏。

  1. 修复
public class SessionManager {
    private static final Cache<String, UserSession> sessions = Caffeine.newBuilder()
        .maximumSize(100_000)
        .expireAfterAccess(30, TimeUnit.MINUTES)
        .build();

    public static void put(String token, UserSession session) {
        sessions.put(token, session);
    }

    public static UserSession get(String token) {
        return sessions.getIfPresent(token);
    }
}

改用 Caffeine 缓存,设置容量上限和过期策略。

  1. 验证:Full GC 频率降至每小时一次,停顿 < 200ms。

十三、总结

JVM 内存与 GC 是 Java 性能调优的核心。要点回顾:

  • 内存区域:堆、方法区、栈、直接内存各司其职,OOM 类型对应不同区域。
  • 对象布局:对象头 + 实例数据 + 对齐填充,小对象开销不容忽视。
  • GC 算法:复制(新生代)、整理(老年代)、分代组合是主流。
  • 收集器演进:Serial → Parallel → CMS → G1 → ZGC/Shenandoah,趋势是并发、低延迟、大堆。
  • 调优核心:先治泄漏,再调参数;监控驱动,场景化配置。
  • 新趋势:ZGC 在 JDK 21 引入分代模式,吞吐与延迟兼顾,未来可期。

理解 JVM 不是为了背参数,而是为了在问题发生时能快速定位、合理决策。把内存模型、GC 管法、收集器特性内化为思维模型,比记住任何调优模板都重要。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。