1. 垃圾收集器的前世今生:从手工管理到自动化
在Java诞生之初的1995年,内存管理还停留在石器时代。当时的主流语言如C++要求开发者手动调用malloc和free来分配释放内存,就像在悬崖边走钢丝——稍有不慎就会导致内存泄漏或野指针。Sun公司的工程师们决定打破这个局面,他们在JVM中设计了一套自动内存管理系统,而垃圾收集器(Garbage Collector,GC)就是这套系统的核心引擎。
最早的垃圾收集器采用了一种名为"标记-清除"(Mark-Sweep)的算法。想象一下图书馆管理员的工作:首先遍历所有书架(堆内存)标记哪些书(对象)还在被借阅(引用),然后清扫掉那些无人问津的书籍。这种朴素的方案虽然解决了手动管理的痛点,但存在两个致命缺陷:一是清扫后会产生内存碎片,就像书架上的空隙导致大本书无处安放;二是收集时必须暂停所有借阅操作(Stop-The-World),读者们不得不排队等待整理完成。
关键认知:垃圾收集器不是Java的专利,Lisp语言早在1959年就实现了自动内存管理。但Java将其带入了主流开发领域,这是编程史上的重要里程碑。
2. 第一代垃圾收集器:Serial家族的奠基之作
2.1 Serial收集器:单线程时代的开拓者
Serial收集器是JVM默认的祖传收集器,它的工作方式就像个固执的老管家——必须独自完成所有工作且不允许被打扰。当内存不足时,它会:
- 暂停所有应用线程(Stop-The-World)
- 启动单个GC线程进行标记-清除
- 恢复应用线程
这种设计在单核CPU的Pentium时代非常合理,因为多线程反而会因上下文切换降低效率。以下是通过JVM参数显式启用Serial收集器的命令:
bash复制java -XX:+UseSerialGC -jar your_app.jar
2.2 Serial Old:老年代的安全网
随着对象存活时间的差异被发现,内存自然分为了新生代(频繁回收)和老年代(较少回收)。Serial Old作为Serial收集器的老年代版本,采用了标记-整理(Mark-Compact)算法——就像整理衣柜时把所有衣服向一侧靠拢,避免产生内存碎片。这对长期服务的对象尤为重要,因为内存碎片会导致明明有足够空间却无法分配大对象。
3. 第二代收集器:并行化的性能突破
3.1 ParNew:多线程清扫新生代
当多核处理器成为主流,单线程收集明显成为瓶颈。ParNew收集器应运而生,它本质上是Serial的多线程版本,采用复制算法(Copying)处理新生代。就像请来一队清洁工同时打扫不同区域:
- 将新生代划分为Eden区和两个Survivor区
- GC时把Eden和Survivor存活对象复制到另一个Survivor
- 年龄达到阈值(默认15)的对象晋升到老年代
启用参数:
bash复制java -XX:+UseParNewGC -jar your_app.jar
实战经验:ParNew的线程数默认与CPU核心数相同,但在容器环境中需通过
-XX:ParallelGCThreads显式设置,因为JVM可能误读cgroup限制。
3.2 Parallel Scavenge/Old:吞吐量优先的王者
如果说ParNew关注的是缩短单次GC停顿时间,那么Parallel Scavenge则是为吞吐量而生。它通过自适应策略动态调整堆大小和晋升阈值,目标是达成:
code复制吞吐量 = 应用运行时间 / (应用运行时间 + GC时间)
这对批处理任务特别重要。其老年代搭档Parallel Old同样采用多线程标记-整理算法,形成完整的吞吐量优先方案:
bash复制java -XX:+UseParallelGC -XX:+UseParallelOldGC -jar your_app.jar
4. 第三代收集器:CMS的革命与局限
4.1 CMS的工作原理:与时间赛跑
Concurrent Mark-Sweep(CMS)收集器是JDK1.4推出的里程碑式作品,首次实现了大部分GC工作与应用线程并发执行。它的收集周期像一场精密编排的交响乐:
- 初始标记(STW):快速标记GC Roots直接关联对象
- 并发标记:遍历对象图,与应用线程并发执行
- 重新标记(STW):修正并发期间变动的引用
- 并发清除:清理不可达对象
启用方式:
bash复制java -XX:+UseConcMarkSweepGC -jar your_app.jar
4.2 CMS的致命缺陷:内存碎片与并发模式失败
我在生产环境曾遇到一个经典案例:某服务运行几天后突然Full GC耗时飙升。根本原因是CMS采用的标记-清除算法会产生内存碎片,当无法找到足够连续空间分配大对象时,会退化为Serial Old收集器进行全局整理(Concurrent Mode Failure)。解决方案是加入内存压缩:
bash复制-XX:+UseCMSCompactAtFullCollection
-XX:CMSFullGCsBeforeCompaction=5
5. 早期收集器的遗产与现代启示
虽然这些早期收集器已逐渐被G1、ZGC等取代,但它们的核心思想仍在延续:
- 分代假设:不同生命周期对象区别对待(新生代/老年代)
- 并行与并发:从完全STW到部分并发
- 空间整理:复制 vs 标记-清除 vs 标记-整理
在容器化环境中,如果仍需要使用早期收集器,建议配置:
bash复制# 保持堆大小稳定避免自适应调整
-XX:+UseContainerSupport
-XX:InitialRAMPercentage=50.0
-XX:MaxRAMPercentage=80.0
-XX:MinHeapFreeRatio=20
-XX:MaxHeapFreeRatio=40
理解这些历史演进,能帮助我们在面对"为什么G1要有Region"、"ZGC如何实现亚毫秒停顿"等问题时,拥有更深刻的视角。每个技术决策背后,都是前人踩过坑后的智慧结晶。
