文章目录
一、垃圾回收概述
垃圾回收(Garbage Collection, GC)是Java语言的核心特性之一,它自动管理内存分配和回收,极大减轻了开发者的负担。在Java中,内存管理由Java虚拟机(JVM)负责,而不是由程序员手动控制。
1.1 为什么需要垃圾回收
- 防止内存泄漏:自动回收不再使用的对象
- 提高开发效率:开发者无需手动管理内存
- 提升系统稳定性:减少因内存问题导致的程序崩溃
1.2 垃圾回收的基本原理
垃圾回收主要解决三个问题:
- 哪些内存需要回收?(对象是否存活判断)
- 什么时候回收?
- 如何回收?
二、对象存活判断算法
在垃圾回收前,首先需要确定哪些对象是"存活"的,哪些是"垃圾"。
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)
步骤:
- 标记阶段:标记所有从GC Roots可达的对象
- 清除阶段:回收未被标记的对象占用的空间
特点:
- 最基础的收集算法
- 标记和清除过程效率都不高
- 会产生大量不连续的内存碎片
// 伪代码表示
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)
步骤:
- 标记阶段:与标记-清除相同
- 整理阶段:让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存
特点:
- 解决了内存碎片问题
- 移动对象需要更新引用,开销较大
- 适合对象存活率高的场景(如老年代)
// 伪代码表示
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)
特点:
- 以获取最短回收停顿时间为目标
- 基于标记-清除算法实现
- 运作过程复杂,分为四个步骤:
- 初始标记(Stop The World)
- 并发标记
- 重新标记(Stop The World)
- 并发清除
优点:
- 并发收集,低停顿
缺点:
- 对CPU资源敏感
- 无法处理浮动垃圾
- 会产生内存碎片
4.7 G1收集器(Garbage-First)
特点:
- 面向服务端应用的垃圾收集器
- 将堆划分为多个大小相等的Region
- 可预测的停顿时间模型
- 整体基于标记-整理,局部基于复制算法
运作步骤:
- 初始标记
- 并发标记
- 最终标记
- 筛选回收
// 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 开发层面
-
减少对象创建:
- 避免不必要的对象创建
- 重用对象(如使用对象池)
-
合理使用集合:
- 初始化时指定合适容量
- 及时清理无用集合
-
注意内存泄漏:
- 监听器、缓存等需要显式清理
- 避免长生命周期对象持有短生命周期对象的引用
6.2 配置层面
-
合理设置堆大小:
- 避免设置过小导致频繁GC
- 避免设置过大导致长时间Full GC
-
选择合适的收集器:
- 吞吐量优先:Parallel Scavenge + Parallel Old
- 响应时间优先:ParNew + CMS
- 大内存服务端:G1
-
监控与调优:
- 使用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开发者,持续关注和学习这些新技术将有助于我们构建更高效、更稳定的应用系统。