弄懂 JVM 垃圾回收,是你告别 CRUD 的第一步

张开发
2026/6/17 13:03:24 15 分钟阅读
弄懂 JVM 垃圾回收,是你告别 CRUD 的第一步
很多写了几年业务代码的兄弟对 JVM 的垃圾回收GC都有一个巨大的误解“既然 Java 帮我自动回收内存了那我管它底层怎么扫地干嘛我只要负责new对象不就行了”如果你抱着这个想法那线上一旦出现接口偶尔超时、CPU 突然飙升 100%、或者半夜突然收到 OOM内存溢出报警你绝对会像没头苍蝇一样抓狂。今天咱们不背那些干巴巴的八股文。我们就从**“怎么找垃圾”、“怎么扫垃圾”以及“懂了这些到底对咱们天天写代码有什么用”**这三个直击灵魂的角度把 JVM 垃圾回收的底裤彻底扒掉一、 是什么JVM 到底是怎么揪出“内存垃圾”的在 JVM 这个大工厂里内存是极其宝贵的资源。如果你申请了内存但不用了这就叫垃圾。那么保洁阿姨GC 线程怎么判断你是不是垃圾呢致命误区引用计数法Reference Counting很多人的第一直觉是给每个对象贴个数字有一个人引用它就 1没人引用了变成 0就当垃圾收走。被打脸的现实这种方法有个致命 Bug 叫**“循环引用”**。假设 A 引用了 BB 又引用了 A但外界根本没人用它俩。这俩货的计数器永远是 1保洁阿姨看着它俩干瞪眼这块内存就永远泄露了。JVM 早就抛弃了这种弱智玩法现代基石可达性分析算法Reachability Analysis现代 JVM 使用的是极其霸道的“顺藤摸瓜”策略。JVM 在内部选定了一小撮绝对不能被回收的“超级大靠山”统称为GC Roots比如正在执行的方法里的局部变量、类的静态变量等。大白话直觉保洁阿姨只认大靠山。她从 GC Roots 开始拉一根绳子往下捋。只要你能顺着这根绳子和 GC Roots 攀上关系引用链可达你就是良民绝对安全。如果你和所有的大靠山都断了联系哪怕你和另外几个废柴紧紧抱团循环引用在阿姨眼里也是一堆死掉的“孤岛”统统拉去火化二、 核心机制保洁阿姨的三大“扫地绝学”找出了垃圾接下来怎么清理JVM 根据不同区域的特性配备了三套不同的打扫方案。1. 标记-清除Mark-Sweep最懒的做法怎么扫第一遍走访给所有垃圾贴上罚单标记第二遍走访直接把贴罚单的垃圾原地炸毁清除。致命缺陷内存碎片原地炸毁后可用内存变得坑坑洼洼。下次你要new一个大数组发现虽然总空间够但连不起来只能被迫再次触发 GC。2. 复制算法Copying空间换时间的土豪玩法怎么扫把房间一劈两半平时只用左边。大扫除时把左边活着的好东西一股脑全搬到右边并且紧紧挨着码放整齐。然后把左边直接引爆全清空。实战场景专用于新生代因为新生代 98% 的对象都是“朝生夕死”的活下来的极少搬运成本极低。最大优势是绝对没有内存碎片。3. 标记-整理Mark-Compact强迫症的福音怎么扫发现垃圾后不急着炸而是像玩“俄罗斯方块”一样把所有活着的对象全部往内存的一侧推紧紧贴在一起。最后把边界线以外的垃圾一刀切掉。实战场景专用于老年代老年代里全是活了很久的老油条如果用复制算法太浪费空间。推一次虽然慢会引发较长的系统停顿 STW但一劳永逸。三、 极简实战几行代码亲手制造一次“内存暗杀”光说不练假把式。我们来看看如果不懂 GC Roots是如何在不经意间把系统干崩溃的。Javaimport java.util.ArrayList; import java.util.List; public class GCRootLeakDemo { // 致命毒药这是一个 static 变量 // 在 JVM 规范里类的静态属性是铁打的 GC Root大靠山 private static final Listbyte[] MEMORY_LEAK_LIST new ArrayList(); public static void main(String[] args) throws InterruptedException { System.out.println( 危险业务上线 ); while (true) { // 模拟用户请求每次请求产生 1MB 的临时数据 byte[] tempData new byte[1024 * 1024]; // 实习生为了所谓“统计”或“缓存”把临时数据塞进了静态集合里 MEMORY_LEAK_LIST.add(tempData); System.out.println(处理了一次请求向静态集合塞入 1MB 数据...); Thread.sleep(100); // 注意因为 tempData 被 static 的 MEMORY_LEAK_LIST 死死抓住 // 哪怕这次请求结束了这 1MB 数据依然与 GC Root 可达 // 保洁阿姨根本不敢动它最终必将 OOM } } } /** * 运行输出搭配 -Xmx50m 运行 * 危险业务上线 * 处理了一次请求向静态集合塞入 1MB 数据... * ... (几十次之后) * Exception in thread main java.lang.OutOfMemoryError: Java heap space */看懂了吗这就是线上最经典的内存泄漏场景因为你不懂静态变量是 GC Root无限制地往里面塞东西导致垃圾永远无法被回收硬生生把内存撑爆了。四、 灵魂拷问懂了 GC对天天写 CRUD 的你有什么意义如果你能把上面的逻辑盘明白你的代码段位就已经超越了 80% 的同行。了解 GC 对开发者的真实意义体现在以下三大“避坑”直觉中业务开发常见操作不懂 GC 的灾难后果懂 GC 的神仙操作降维打击滥用全局缓存直接弄个static Map存用户 Token越存越多由于是 GC Root老年代塞满系统一天崩一次。明白生命周期改用 Redis或者使用具备淘汰机制的本地缓存如 Guava Cache / Caffeine。超大对象的创建查数据库时动不动就select *捞几十万条数据塞进 List。知道超大对象会直接绕过新生代砸进老年代引发极耗时的 Full GC 导致接口卡死。果断改用分页查询ThreadLocal 的使用线程池里用完ThreadLocal不调用remove()。知道 ThreadLocalMap 的 Key 是弱引用但 Value 是强引用。线程不销毁Value 就一直与线程GC Root可达导致隐蔽的内存泄漏用完必在finally里remove接口偶发性超时接口平时 20ms偶尔几秒钟不响应到处抓包查网络。意识到可能是触发了老年代的 GC 导致了STW (Stop-The-World 全局停顿)。直接去查 GC 日志调整年轻代比例或更换 G1 回收器来平滑停顿时间。总结一句话学习垃圾回收绝不是为了去造一个 JVM。而是让你在写每一行new Object()、定义每一个static变量时脑子里都能清晰地看到这块内存未来的生老病死。这种掌控感才是高级工程师和代码搬运工的核心壁垒。

更多文章