Java垃圾回收算法详解:从基础到高级全面解析

Java垃圾回收算法全面解析与演进

一、垃圾回收概述

垃圾回收(Garbage Collection, GC)是Java语言的核心特性之一,它自动管理内存分配和回收,极大减轻了开发者的负担。在Java中,内存管理由Java虚拟机(JVM)负责,而不是由程序员手动控制。

1.1 为什么需要垃圾回收

  • 防止内存泄漏:自动回收不再使用的对象
  • 提高开发效率:开发者无需手动管理内存
  • 提升系统稳定性:减少因内存问题导致的程序崩溃

1.2 垃圾回收的基本原理

垃圾回收主要解决三个问题:

  1. 哪些内存需要回收?(对象是否存活判断)
  2. 什么时候回收?
  3. 如何回收?

二、对象存活判断算法

在垃圾回收前,首先需要确定哪些对象是"存活"的,哪些是"垃圾"。

2.1 引用计数法(Reference Counting)

原理:每个对象有一个引用计数器,当被引用时计数器加1,引用失效时减1。计数器为0的对象可被回收。

class ReferenceCounting {
    Object instance = null;
    
    public static void main(String[] args) {
        ReferenceCounting obj1 = new ReferenceCounting();
        ReferenceCounting obj2 = new ReferenceCounting();
        
        obj1.instance = obj2;  // obj2引用计数+1
        obj2.instance = obj1;  // obj1引用计数+1
        
        obj1 = null;  // obj1引用计数-1
        obj2 = null;  // obj2引用计数-1
        // 但此时两个对象的引用计数仍为1,无法回收(内存泄漏)
    }
}

缺点

  • 无法解决循环引用问题
  • 每次引用赋值都需要更新计数器,性能开销大

Java未采用此算法

2.2 可达性分析算法(Reachability Analysis)

原理:通过一系列称为"GC Roots"的对象作为起点,从这些节点向下搜索,搜索走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象不可用。

GC Roots包括

  • 虚拟机栈(栈帧中的局部变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即Native方法)引用的对象
class ReachabilityAnalysis {
    static Object staticObj = new Object();  // GC Root
    
    public static void main(String[] args) {
        Object localObj = new Object();  // GC Root
        
        Object obj1 = new Object();
        Object obj2 = new Object();
        
        obj1 = obj2;
        obj2 = obj1;  // 循环引用
        
        // 即使有循环引用,但从GC Roots不可达,仍会被回收
        obj1 = null;
        obj2 = null;
    }
}

优点

  • 解决了循环引用问题
  • 效率更高

Java主要采用此算法

三、垃圾回收算法分类

3.1 标记-清除算法(Mark-Sweep)

步骤

  1. 标记阶段:标记所有从GC Roots可达的对象
  2. 清除阶段:回收未被标记的对象占用的空间

特点

  • 最基础的收集算法
  • 标记和清除过程效率都不高
  • 会产生大量不连续的内存碎片
// 伪代码表示
void markSweep() {
    // 标记阶段
    for (Object obj : heap) {
        if (isReachable(obj)) {
            mark(obj);
        }
    }
    
    // 清除阶段
    for (Object obj : heap) {
        if (!isMarked(obj)) {
            free(obj);
        }
    }
}

3.2 复制算法(Copying)

原理:将内存分为大小相同的两块,每次只使用其中一块。当这一块内存用完了,就将还存活的对象复制到另一块上,然后再把已使用的内存空间一次清理掉。

特点

  • 实现简单,运行高效
  • 内存利用率只有一半
  • 适合对象存活率低的场景(如新生代)
// 伪代码表示
void copying() {
    // 假设heap分为from和to两个区域
    for (Object obj : fromSpace) {
        if (isReachable(obj)) {
            copy(obj, toSpace);
        }
    }
    swap(fromSpace, toSpace);
    clear(toSpace);
}

3.3 标记-整理算法(Mark-Compact)

步骤

  1. 标记阶段:与标记-清除相同
  2. 整理阶段:让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存

特点

  • 解决了内存碎片问题
  • 移动对象需要更新引用,开销较大
  • 适合对象存活率高的场景(如老年代)
// 伪代码表示
void markCompact() {
    // 标记阶段
    for (Object obj : heap) {
        if (isReachable(obj)) {
            mark(obj);
        }
    }
    
    // 整理阶段
    int newAddress = 0;
    for (Object obj : heap) {
        if (isMarked(obj)) {
            move(obj, newAddress);
            newAddress += obj.size;
        }
    }
    
    // 清理剩余空间
    free(newAddress, heap.end);
}

3.4 分代收集算法(Generational Collection)

原理:根据对象存活周期的不同将内存划分为几块(一般是新生代和老年代),然后根据各年代的特点采用最适当的收集算法。

新生代(Young Generation)

  • 特点:对象朝生夕死,存活率低
  • 算法:复制算法(如Serial、ParNew等收集器)

老年代(Old Generation)

  • 特点:对象存活率高
  • 算法:标记-清除或标记-整理(如CMS、Serial Old等收集器)
// 伪代码表示
void generationalGC() {
    // 新生代GC
    youngGenGC();
    
    // 如果对象在多次新生代GC后仍然存活,晋升到老年代
    if (object.age > AGE_THRESHOLD) {
        promoteToOldGen(object);
    }
    
    // 老年代GC(频率较低)
    if (oldGen.isFull()) {
        oldGenGC();
    }
}

四、经典垃圾收集器实现

4.1 Serial收集器

特点

  • 单线程收集器
  • 新生代采用复制算法,老年代采用标记-整理算法
  • 进行垃圾收集时,必须暂停所有工作线程(“Stop The World”)

适用场景

  • 客户端模式下的默认新生代收集器
  • 简单高效,对于单CPU环境最优

4.2 ParNew收集器

特点

  • Serial收集器的多线程版本
  • 新生代收集器,与CMS收集器配合工作
  • 在多CPU环境下性能优于Serial

4.3 Parallel Scavenge收集器

特点

  • 新生代收集器,使用复制算法
  • 关注吞吐量(运行用户代码时间/(运行用户代码时间+垃圾收集时间))
  • 适合后台运算而不需要太多交互的任务

4.4 Serial Old收集器

特点

  • Serial收集器的老年代版本
  • 单线程,使用标记-整理算法
  • 主要用于客户端模式

4.5 Parallel Old收集器

特点

  • Parallel Scavenge的老年代版本
  • 多线程,使用标记-整理算法
  • JDK 1.6后提供,注重吞吐量

4.6 CMS收集器(Concurrent Mark Sweep)

特点

  • 以获取最短回收停顿时间为目标
  • 基于标记-清除算法实现
  • 运作过程复杂,分为四个步骤:
    1. 初始标记(Stop The World)
    2. 并发标记
    3. 重新标记(Stop The World)
    4. 并发清除

优点

  • 并发收集,低停顿

缺点

  • 对CPU资源敏感
  • 无法处理浮动垃圾
  • 会产生内存碎片

4.7 G1收集器(Garbage-First)

特点

  • 面向服务端应用的垃圾收集器
  • 将堆划分为多个大小相等的Region
  • 可预测的停顿时间模型
  • 整体基于标记-整理,局部基于复制算法

运作步骤

  1. 初始标记
  2. 并发标记
  3. 最终标记
  4. 筛选回收
// G1的基本工作流程
void g1GC() {
    // 初始标记(STW)
    initialMark();
    
    // 并发标记
    concurrentMark();
    
    // 最终标记(STW)
    remark();
    
    // 筛选回收(STW)
    // 优先回收价值最大的Region
    cleanup();
}

五、垃圾回收相关参数

5.1 通用参数

  • -Xms:初始堆大小
  • -Xmx:最大堆大小
  • -Xmn:新生代大小
  • -XX:SurvivorRatio:Eden区与Survivor区比例
  • -XX:NewRatio:老年代与新生代比例

5.2 串行收集器参数

  • -XX:+UseSerialGC:使用Serial + Serial Old组合

5.3 并行收集器参数

  • -XX:+UseParallelGC:使用Parallel Scavenge + Serial Old组合
  • -XX:+UseParallelOldGC:使用Parallel Scavenge + Parallel Old组合
  • -XX:ParallelGCThreads:并行GC线程数
  • -XX:MaxGCPauseMillis:最大GC停顿时间目标
  • -XX:GCTimeRatio:GC时间占总时间比率

5.4 CMS收集器参数

  • -XX:+UseConcMarkSweepGC:使用ParNew + CMS + Serial Old组合
  • -XX:CMSInitiatingOccupancyFraction:触发CMS的老年代使用比例
  • -XX:+UseCMSCompactAtFullCollection:Full GC后压缩内存
  • -XX:CMSFullGCsBeforeCompaction:多少次Full GC后压缩内存

5.5 G1收集器参数

  • -XX:+UseG1GC:使用G1收集器
  • -XX:MaxGCPauseMillis:目标最大停顿时间
  • -XX:InitiatingHeapOccupancyPercent:触发并发GC周期的堆占用率
  • -XX:G1HeapRegionSize:设置Region大小

六、垃圾回收优化建议

6.1 开发层面

  1. 减少对象创建

    • 避免不必要的对象创建
    • 重用对象(如使用对象池)
  2. 合理使用集合

    • 初始化时指定合适容量
    • 及时清理无用集合
  3. 注意内存泄漏

    • 监听器、缓存等需要显式清理
    • 避免长生命周期对象持有短生命周期对象的引用

6.2 配置层面

  1. 合理设置堆大小

    • 避免设置过小导致频繁GC
    • 避免设置过大导致长时间Full GC
  2. 选择合适的收集器

    • 吞吐量优先:Parallel Scavenge + Parallel Old
    • 响应时间优先:ParNew + CMS
    • 大内存服务端:G1
  3. 监控与调优

    • 使用jstat、jvisualvm等工具监控GC情况
    • 分析GC日志(-XX:+PrintGCDetails)

七、Java 8到Java 17的GC演进

7.1 Java 8的GC

  • 默认组合:Parallel Scavenge + Parallel Old
  • 可选:CMS(已废弃)、G1(需要显式启用)

7.2 Java 9的GC改进

  • G1成为默认收集器
  • 引入GC日志统一框架(JEP 158)

7.3 Java 11的GC改进

  • 引入ZGC(实验性功能)
  • 移除CMS收集器

7.4 Java 12的GC改进

  • Shenandoah GC成为标准功能
  • G1改进:及时返回未使用的内存

7.5 Java 15的GC改进

  • ZGC和Shenandoah不再是实验性功能
  • 移除Solaris和SPARC端口

7.6 Java 17的GC改进

  • 进一步优化ZGC和Shenandoah
  • 移除实验性标记(完全支持)

八、新一代垃圾收集器

8.1 ZGC(Z Garbage Collector)

特点

  • 低延迟(目标<10ms)
  • 可扩展(支持TB级堆内存)
  • 并发标记-整理算法
  • 基于Region的内存布局
  • 使用染色指针技术

启用参数

  • -XX:+UseZGC

8.2 Shenandoah GC

特点

  • 低停顿时间
  • 并发压缩
  • 与应用程序线程并发执行大多数GC工作
  • 适用于大内存应用

启用参数

  • -XX:+UseShenandoahGC

九、GC日志分析

9.1 开启GC日志

-XX:+PrintGCDetails 
-XX:+PrintGCDateStamps 
-XX:+PrintGCTimeStamps 
-Xloggc:<file-path>

9.2 日志示例分析

2023-01-01T10:00:00.123+0800: 0.123: [GC (Allocation Failure) 
[PSYoungGen: 65536K->10752K(76288K)] 65536K->12345K(251392K), 
0.0045678 secs] [Times: user=0.02 sys=0.01, real=0.00 secs]

解读:

  • 发生时间:2023-01-01 10:00:00
  • JVM运行时间:0.123秒
  • GC原因:分配失败(Allocation Failure)
  • 新生代回收:PSYoungGen(Parallel Scavenge)
    • 回收前:65536K
    • 回收后:10752K
    • 总容量:76288K
  • 堆内存情况:
    • 回收前:65536K
    • 回收后:12345K
    • 总容量:251392K
  • 耗时:0.0045678秒
  • 时间统计:用户态0.02秒,内核态0.01秒,实际0.00秒

十、总结

Java垃圾回收技术经历了多年的发展,从最初的单线程Serial收集器到现在的ZGC、Shenandoah等低延迟收集器,不断满足着不同场景下的需求。理解各种垃圾回收算法和收集器的工作原理,对于Java应用的性能调优至关重要。

在实际应用中,应根据具体场景选择合适的垃圾收集器:

  • 小型应用或客户端程序:Serial/Serial Old
  • 注重吞吐量的服务器应用:Parallel Scavenge/Parallel Old
  • 需要低延迟的Web服务:CMS(Java 8)或G1(Java 9+)
  • 大内存服务(TB级):ZGC或Shenandoah

随着Java的持续发展,垃圾回收技术也在不断进步,未来可能会出现更高效、更智能的垃圾回收解决方案。作为Java开发者,持续关注和学习这些新技术将有助于我们构建更高效、更稳定的应用系统。
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

北辰alk

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值