"在Java世界里,你只管创建对象,清洁工作交给GC!" —— Java垃圾回收机制(GC)的座右铭
一、为什么需要垃圾回收?
想象你在游乐园里玩射击游戏(创建对象),打中的气球(对象)会挂满墙壁(内存)。如果不清理,很快墙壁就会爆满!在Java中:
public class BalloonGame {
public static void main(String[] args) {
while (true) {
String balloon = new String("🎈"); // 疯狂创建气球对象
}
}
}
如果没有垃圾回收,这个程序很快会抛出OutOfMemoryError
。GC就是那个勤劳的清洁工,在你玩耍时默默打扫战场!
二、垃圾识别:谁是垃圾?
1. 引用计数法(不常用)
给每个对象配计数器,统计被引用次数:
Object A = new Object(); // A计数=1
Object B = A; // A计数=2
A = null; // A计数=1
B = null; // A计数=0 → 垃圾!
缺点:无法解决循环引用问题
class Lovers {
Lovers partner;
}
Lovers romeo = new Lovers(); // 计数=1
Lovers juliet = new Lovers(); // 计数=1
romeo.partner = juliet; // juliet计数=2
juliet.partner = romeo; // romeo计数=2
romeo = null; // romeo计数=1
juliet = null; // juliet计数=1 → 永远无法回收!
2. 可达性分析(Java实际使用)
从GC Roots出发扫描对象链,不在链上的就是垃圾:
GC Roots包括:
虚拟机栈中的局部变量
方法区中静态变量
方法区中常量
本地方法栈中JNI引用
三、垃圾回收算法:清洁工的打扫策略
1. 标记-清除(Mark-Sweep) - "随地贴小广告"
缺点:产生内存碎片 → 就像拼图游戏缺了几块
2. 复制算法(Copying) - "搬家大法"
特点:没碎片但浪费空间(需双倍内存)
3. 标记-整理(Mark-Compact) - "大扫除"
优点:无碎片且空间利用率高
四、分代收集:垃圾回收的核心策略
Java将堆内存分为不同"年龄段":
// 堆内存结构示例
class Heap {
YoungGeneration youngGen = new YoungGeneration(); // 新生代
OldGeneration oldGen = new OldGeneration(); // 老年代
}
class YoungGeneration {
EdenSpace eden = new EdenSpace(); // 伊甸园
SurvivorSpace survivor0 = new SurvivorSpace(); // 幸存区1
SurvivorSpace survivor1 = new SurvivorSpace(); // 幸存区2
}
1. 新生代(Young Generation) - "幼儿园"
特点:98%对象活不过第一轮GC
算法:复制算法
区域:
Eden区(80%):对象出生地
Survivor0(10%):幸存者营地1
Survivor1(10%):幸存者营地2
对象旅程示例:
public class LifeJourney {
public static void main(String[] args) {
// 第一阶段:出生在Eden区
Object baby = new Object();
// 触发Minor GC后...
// 幸存对象搬到Survivor0区(年龄+1)
// 再经历15次GC后...
// 对象年龄达到阈值(默认15),晋升老年代
}
}
2. 老年代(Old Generation) - "养老院"
特点:存放长期存活对象
算法:标记-整理或标记-清除
触发条件:
大对象直接进入(如超大数组)
新生代对象熬过15次GC
Survivor区放不下存活对象
3. 永久代/元空间(PermGen/Metaspace) - "博物馆"
存放类信息、常量池等
Java 8后元空间使用本地内存
五、经典垃圾收集器:清洁工团队
CMS工作流程:
六、实战:GC日志分析
启用GC日志:
java -XX:+PrintGCDetails -Xloggc:gc.log YourApp
典型日志解读:
[GC (Allocation Failure)
[PSYoungGen: 8192K->1000K(9216K)]
8192K->2000K(19456K),
0.0051234 secs]
PSYoungGen
:使用Parallel Scavenge收集新生代8192K->1000K
:GC前占用8MB → GC后1MB(9216K)
:新生代总大小9MB0.0051234 secs
:GC耗时5ms
七、内存泄漏的典型案例
即使有GC,内存泄漏仍可能发生:
案例1:静态集合引用
class MemoryLeak {
static List<Object> blackHole = new ArrayList<>();
void createLeak() {
while(true) {
blackHole.add(new byte[1024*1024]); // 永久存活!
}
}
}
案例2:未关闭资源
void readFile() {
try {
InputStream is = new FileInputStream("huge_file.txt");
// 使用后忘记关闭...
} catch (IOException e) { /*...*/ }
}
案例3:监听器未移除
class Button {
List<ClickListener> listeners = new ArrayList<>();
void addListener(ClickListener l) {
listeners.add(l);
}
// 但缺少removeListener方法...
}
八、GC优化技巧
选择合适的GC:
小内存应用:Serial GC
低延迟要求:CMS/G1/ZGC
高吞吐量:Parallel GC
合理设置堆大小:
-Xms4g -Xmx4g # 初始堆=最大堆,避免动态调整
调整新生代比例:
-XX:NewRatio=2 # 老年代:新生代=2:1 -XX:SurvivorRatio=8 # Eden:Survivor=8:1:1
避免大对象分配:
// 不佳:一次性分配超大数组 byte[] hugeArray = new byte[1024 * 1024 * 500]; // 500MB! // 更佳:分块处理 for (int i=0; i<500; i++) { byte[] chunk = new byte[1024 * 1024]; // 每次1MB process(chunk); }
九、GC的哲学思考
"垃圾回收不仅是技术,更是平衡艺术: 在空间与时间、停顿与吞吐、 预测与适应之间寻找黄金点"
Java的GC机制经历了从"Stop-the-World"到"并发收集",再到如今ZGC的"亚毫秒停顿"的进化,体现了计算机科学对优雅和效率的不懈追求。
"理解GC,就是掌握Java内存管理的终极奥义"。
评论区