Java的垃圾回收(GC)机制是每个开发者都绕不开的话题。它既复杂又重要,它是区别于C/C++等语言的重要特性之一,它让开发者从繁琐的内存管理中解放出来。
一、什么需要垃圾回收?
在C/C++中,开发者需要手动分配和释放内存,这经常导致两个问题:
1、 内存泄漏:忘记释放不再使用的内存
2、 野指针:释放后仍然访问该内存区域
Java通过自动垃圾回收机制解决了这些问题。就像有个清洁工(GC)定期来打扫房间(内存),帮你清理掉不再使用的物品(对象)
二、垃圾加收的基本原理
如何判断对象是"垃圾"?GC首先要确定哪些对象可以被回收,主要有两种算法:
1. 引用计数法(简单但Java未采用)
// 伪代码示例
Object a = new Object(); // 对象A引用计数=1
Object b = a; // 对象A引用计数=2
b = null; // 对象A引用计数=1
a = null; // 对象A引用计数=0 → 可回收
缺点:无法解决循环引用问题。
2. 可达性分析法(Java采用)从GC Roots出发,遍历引用链,不可达的对象即为可回收对象。
public class GCDemo {
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; // 原obj1引用的对象现在不可达
localObj = null; // 原localObj引用的对象现在不可达
}
}
常见的GC Roots包括:
- 本地变量表中的引用(如方法内的局部变量)
- 活动线程(如当前正在运行的线程)
- 方法区中的静态变量(如Person.name)
三、垃圾回收算法
1. 标记-清除(Mark-Sweep)
- 标记所有可达对象
- 清除未标记对象
缺点:产生内存碎片
2. 复制(Copying)
将内存分为两块,只使用其中一块。GC时把存活对象复制到另一块,然后清空当前块。
优点:无碎片
缺点:内存利用率低
3. 标记-整理(Mark-Compact)
1、 标记所有可达对象
2、 将所有存活对象向一端移动
3、 清理边界外的内存
优点:无碎片,内存利用率高
缺点:移动对象成本高
4. 分代收集(Generational)
Java虚拟机的实际实现,将堆分为:
- 新生代(Young Generation):新创建的对象
- Eden区
Survivor区(From/To) - 老年代(Old Generation):长期存活的对象
- 永久代/元空间(方法区)
分代策略:
- 新生代(Young Generation):存放新创建的对象,采用复制算法(效率高)。
- 老年代(Old Generation):存放存活时间长的对象,采用标记-整理算法(减少碎片)。
- 元空间(Metaspace):存放类元数据(替代旧版的永久代)。
分代回收的流程:
1、 Minor GC(Young GC):回收新生代,频率高、速度快。
2、 Major GC(Full GC):回收老年代,耗时长、影响性能。
3、 G1的混合GC:同时回收部分Young和Old的Region。
四、Java中的垃圾收集器
1. Serial收集器
单线程,适合客户端应用,简单高效。
2. Parallel收集器
多线程并行GC,注重吞吐量。
3. CMS(Concurrent Mark-Sweep)
并发标记清除,减少停顿时间,但会产生碎片。
4. G1(Garbage-First)
面向服务端的收集器,将堆划分为多个Region,可预测停顿时间。
5. ZGC
Java 11+引入的低延迟收集器,停顿时间不超过10ms。
五、新生代、老年代的垃圾回收器
以下收集器用于新生代:
- Serial(单线程):
- 算法:复制算法。
特点:单线程工作,适合小内存场景(如Client模式)。
使用场景:简单高效,但吞吐量低,适用于桌面应用。
- ParNew(多线程):
- 算法:复制算法。
特点:Serial的多线程版本,与CMS老年代收集器配合使用。
使用场景:Server模式下需低停顿的场景(如Web应用)。
- Parallel Scavenge(吞吐量优先):
- 算法:复制算法。
特点:多线程并行回收,目标是最大化吞吐量(CPU用于用户代码的时间占比)。
使用场景:后台批量处理任务(如大数据计算)。
以下收集器用于老年代:
- Serial Old(单线程):
- 算法:标记-整理或标记-清除。
特点:Serial的单线程老年代版本,适合小内存场景。
使用场景:与Serial配合,用于简单应用。
- Parallel Old(吞吐量优先):
- 算法:标记-整理。
特点:Parallel Scavenge的配套老年代收集器,多线程并行,目标是吞吐量。
使用场景:需高吞吐量的后台任务。
- CMS(Concurrent Mark Sweep,并发标记清除):
- 算法:标记-清除(老年代)。
特点:以低停顿为目标,采用并发回收(与用户线程交替执行)。
缺点:内存碎片化严重,可能导致频繁Full GC。
使用场景:对响应时间敏感的场景(如Web服务器)。 - G1(Garbage-First,分区回收):
- 算法:混合使用复制和标记-整理。
特点:将堆内存划分为多个 Region,优先回收垃圾最多的区域。
适用性:兼顾吞吐量和低延迟,适合大内存应用。
老年代处理:G1不分严格的新老年代,但通过 Region 分区实现类似逻辑。
六、结语
理解Java垃圾回收机制不仅能帮助我们写出更健壮的代码,还能在性能调优时事半功倍。记住,GC不是万能的,良好的编程习惯才是避免内存问题的根本。优秀的Java开发者不仅要会写代码,还要理解代码在JVM中的生命周期。