Skip to content

Java 垃圾回收?

约 1617 字大约 5 分钟

JVM字节美团

2025-04-18

⭐ 题目日期:

美团 - 2024/4/12,字节 - 2024/12/10

📝 题解:

一、核心概念

  1. 堆内存结构
    年轻代(Young Generation):存放新创建的对象,分为 Eden区 和两个 Survivor区(S0/S1)
    老年代(Old Generation):存放长期存活的对象(经过多次Young GC未被回收的对象)。
    元空间(Metaspace):JDK8+替代永久代(PermGen),存放类元数据、常量池等。

  2. 垃圾回收目标
    自动回收:释放无引用对象占用的内存,防止内存泄漏。
    内存整理:减少内存碎片,提高内存利用率。

  3. Stop-The-World(STW)
    • 垃圾回收过程中会暂停所有应用线程,影响响应时间。
    • 低延迟垃圾收集器(如G1、ZGC)通过并发标记、增量回收减少STW时间。


二、垃圾判定算法

  1. 引用计数法(Reference Counting)
    • 每个对象维护一个引用计数器,引用增减时更新计数器。
    缺点:无法解决循环引用问题(如A→B,B→A,但无外部引用)。

  2. 可达性分析(Reachability Analysis)
    • 从 GC Roots 出发,遍历对象引用链,未被引用的对象判定为垃圾。
    GC Roots 包括
    ◦ 虚拟机栈(栈帧中的局部变量)引用的对象。
    ◦ 方法区中静态变量、常量引用的对象。
    ◦ JNI(本地方法)引用的对象。


三、垃圾回收算法

  1. 标记-清除(Mark-Sweep)
    步骤:标记所有存活对象 → 清除未标记对象。
    缺点:内存碎片化,可能触发频繁Full GC。

  2. 复制算法(Copying)
    步骤:将存活对象从Eden和Survivor区复制到另一块Survivor区,清空原空间。
    优点:无碎片,适用于对象存活率低的场景(如年轻代)。
    缺点:空间利用率低(需预留50%内存)。

  3. 标记-整理(Mark-Compact)
    步骤:标记存活对象 → 将对象向内存一端移动 → 清理边界外内存。
    优点:避免碎片,适用于老年代。
    缺点:移动对象耗时,STW时间较长。

  4. 分代收集(Generational Collection)
    核心思想:根据对象存活周期划分内存区域,采用不同算法。
    年轻代:使用复制算法(Minor GC)。
    老年代:使用标记-清除或标记-整理算法(Full GC)。


四、垃圾收集器

  1. Serial 收集器
    单线程,通过复制算法处理年轻代,标记-整理处理老年代。
    适用场景:单核客户端应用(如Java桌面程序)。

  2. Parallel Scavenge / Parallel Old
    多线程并行回收,关注吞吐量(吞吐量 = 用户代码时间 / (用户代码时间 + GC时间))。
    参数控制-XX:MaxGCPauseMillis(最大GC停顿时间),-XX:GCTimeRatio(吞吐量目标)。

  3. CMS(Concurrent Mark-Sweep)
    目标:减少老年代回收的STW时间,通过并发标记实现低停顿。
    回收流程
    1. 初始标记(STW):标记GC Roots直接关联的对象。
    2. 并发标记:遍历对象图(与应用线程并发执行)。
    3. 重新标记(STW):修正并发标记期间变动的引用。
    4. 并发清除:清理垃圾(并发执行)。
    缺点:内存碎片、CPU敏感、并发失败时退化为Serial Old。

  4. G1(Garbage-First)
    内存划分:将堆划分为多个 Region,每个Region可以是Eden、Survivor或Old类型。
    核心流程
    1. 并发全局标记(Global Marking):识别各Region存活对象比例。
    2. 混合回收(Mixed GC):优先回收垃圾比例高的Region。
    优点:可预测的停顿时间(-XX:MaxGCPauseMillis),高吞吐与低延迟平衡。
    适用场景:大堆(>4GB)、响应时间敏感的应用。

  5. ZGC(Z Garbage Collector)
    目标:亚毫秒级STW停顿(<10ms),支持TB级堆内存。
    核心技术
    染色指针(Colored Pointers):在指针中存储元数据,避免对象移动时的STW。
    并发压缩:在应用线程运行时整理内存。
    参数启用-XX:+UseZGC(JDK15+生产可用)。

  6. Shenandoah
    • 类似ZGC的低延迟收集器,通过 Brooks指针 和并发压缩实现低停顿。
    参数启用-XX:+UseShenandoahGC(需额外安装)。


五、内存分配与回收策略

  1. 对象分配规则
    优先在Eden区分配:多数对象“朝生夕死”。
    大对象直接进入老年代:避免在Eden区反复复制(通过-XX:PretenureSizeThreshold设置阈值)。
    长期存活对象进入老年代:对象年龄计数器(默认15次Minor GC后晋升,通过-XX:MaxTenuringThreshold调整)。

  2. Minor GC 与 Full GC
    Minor GC:回收年轻代,触发条件为Eden区满。
    Full GC:回收整个堆(包括老年代和元空间),触发条件:
    ◦ 老年代空间不足。
    ◦ 元空间不足。
    System.gc()显式调用(可通过-XX:+DisableExplicitGC禁用)。

  3. 空间分配担保
    • Minor GC前检查老年代剩余空间是否足够容纳年轻代所有对象。
    • 若不足,触发Full GC(通过-XX:-HandlePromotionFailure控制策略)。


六、调优与实践

  1. 常见调优参数
    • 堆大小:-Xms(初始堆)、-Xmx(最大堆)。
    • 年轻代大小:-Xmn
    • 元空间:-XX:MetaspaceSize-XX:MaxMetaspaceSize
    • 收集器选择:-XX:+UseG1GC-XX:+UseConcMarkSweepGC

  2. 监控工具
    命令行jstat -gc <pid>(实时GC统计)、jmap -heap <pid>(堆内存快照)。
    图形化工具:VisualVM、MAT(Memory Analyzer Tool)分析堆转储(Heap Dump)。

  3. 避免内存泄漏
    • 排查长生命周期集合类(如静态Map缓存未清理)。
    • 避免未关闭的资源(数据库连接、文件流)。
    • 使用弱引用(WeakReference)管理缓存。


七、高级话题

  1. 三色标记算法
    白色:未访问对象。
    灰色:已访问但子节点未完全处理。
    黑色:已访问且子节点处理完成。
    并发标记问题:漏标(应用线程修改引用导致存活对象被误删),通过 增量更新(CMS)原始快照(G1) 解决。

  2. 安全点(Safepoint)与安全区域(Safe Region)
    安全点:线程执行到特定位置(如方法调用、循环跳转)时才能暂停进行GC。
    安全区域:线程在一段代码中引用关系不会变化,可随时响应GC。

  3. 跨代引用(Inter-Generational References)
    • 老年代对象引用年轻代对象时,需避免每次Minor GC扫描整个老年代。
    解决方案:记忆集(Remembered Set)记录跨代引用(G1中每个Region维护一个RSet)。

  4. ZGC 的 NUMA 感知
    • 针对非统一内存访问(NUMA)架构优化,优先在本地内存分配对象。