尧图网络 高端网站定制 · 原创设计
免费咨询热线
400-888-6620
免费获取方案
垃圾收集(GC)原理与调优实战:从算法到线上内存排查
最近有个朋友跑来找我说线上服务的堆内存曲线像爬山早上还是平的到下午就开始一路往上顶重启能好半天过阵子又旧病复发。我让他先别急着加机器扩内存把 GC 日志开了看一眼结果果然又是老问题。很多人听到“垃圾收集”Garbage CollectionGC就觉得这是虚拟机内部的事跟写业务代码的人没关系其实恰恰相反。只要你还得让程序跑得稳、跑得快你就躲不开要理解它。这篇文章我会从原理一直聊到实际排查经验把三类基础算法、根可达性分析、分代回收的逻辑都拆开讲最后再分享几个我实际调内存问题时的操作步骤和踩坑心得。无论你是刚接触 GC 的学生、写过一段时间业务代码但没系统梳理过对象回收的开发者还是正在准备技术面试的人这篇应该都能让你少走点弯路。1. 从手动释放到自动回收垃圾收集到底解决了什么1.1 手动内存管理的三个典型困境在没有垃圾收集的语言里内存是要自己借、自己还的。每次malloc或者new出来的对象本质上就是从系统手里借了一块地用完得亲手还回去。这件事听起来简单做起来却非常反人性因为你要在代码的每一个退出路径上都记得归还而且还得保证归还的时机不多不少。最常见的三个坑我估计写过底层代码的人都遇到过第一种是忘了归还叫内存泄漏对象永远占着位置程序越跑越胖第二种是还早了别人还在用这块内存结果内容已经被改写这就是悬垂指针第三种更狠同一块内存还了两次直接让进程崩溃。你可以把内存想象成一间临时借来的房子忘了退房房东就没法把这个房间租给别人退早了你室友晚上回来发现钥匙已经换了进不去同一间房退两次物业系统直接报错。在没有垃圾收集的年代这些问题只能靠开发者的自律和大量的代码审查去规避。但人总会犯错尤其是代码规模一大谁还分得清某个对象到底还有没有人在引用。1.2 自动回收的价值把判断交给运行时自动内存管理的思想其实很早就有最早可以追溯到 1960 年代的一批动态语言实现它们尝试让运行时自己去判断“这个对象还活不活”活着的留下死掉的回收。这就是垃圾收集的本质运行时不依赖开发者手动释放而是自动找出那些“再也没有人使用”的对象并回收它们占用的空间。这个转变最大的价值不是省了开发者几行代码而是把“对象是否还活着”的判断从人脑转移到了程序机制里。人判断靠记性机制判断靠规则规则虽然会消耗一些额外资源但稳定性和可靠性远远超过人肉维护。当然GC 不是银弹它也有成本。垃圾收集需要占用 CPU 来扫描对象、标记状态、搬运内存还可能在回收过程中让应用程序暂停一下。理解这些成本以及成本花在了哪里正是我们后面要聊的重点。2. 引用计数、标记-清除、标记-压缩三种经典思路的取舍2.1 引用计数实时性强但怕“循环引用”引用计数Reference Counting是所有垃圾收集算法里最直观的一种思路。它的核心逻辑很简单每个对象在创建时挂一个计数器其他对象每引用它一次计数器加一引用断开时计数器减一当计数器归零说明没有任何人再引用它立刻回收。这个方案的优点是实时性强内存一但没人引用马上就能释放不需要等到某个特定时机才动手。实现上也不需要全堆扫描每个对象的生命周期都比较清晰。不少语言和框架在局部场景下还在使用或借鉴引用计数。但它有两个明显的坑。第一个是循环引用。假如 A 对象持有了 BB 又持有了 A而且这两个对象已经从根上断开了但它们的引用计数互相撑着永远不为零就谁也回收不了形成死锁一样的泄漏。第二个是维护计数器的开销在多线程环境下计数器加减往往需要原子操作竞争激烈时会影响性能。所以现实中的引用计数方案通常还要配一个额外的机制去处理循环引用比如定时扫描那些“有可能互指”的对象。2.2 标记-清除从根源出发先标记后回收标记-清除Mark-Sweep算法是另一个经典思路它分两个阶段工作。第一个阶段是标记。从一组确定的“根”出发沿着对象的引用链一路走把所有还能碰到的对象都做上记号。这就好比你要统计一栋楼里还有哪些房间住着人你会从大门口开始顺着楼层通道逐个房间查看凡是能走到的地方都画个勾。第二个阶段是清除遍历整个内存区域把没有做过记号的对象回收掉。这种方案天然解决了循环引用问题因为判断依据不是“谁引用了我”而是“能不能从根到达这里”。不过它也有自己的短板标记和清除都要遍历大量对象停顿时间往往比引用计数长更麻烦的是清除之后内存空间会变得七零八落碎片化严重就好比一整层楼的房间被随机退租了大半后面想分配一间大办公室时可能反而找不出连续空间。2.3 标记-压缩与复制算法解决碎片但各有代价为了解决碎片问题出现了两种改良思路。标记-压缩Mark-Compact在做完标记之后不直接清扫描区域而是把存活对象往一端搬运然后把另一端一次性清空。这样内存始终是连续的分配新对象时找指针往后挪就行但搬运存活对象的成本不低而且整个过程中应用程序通常要暂停。另一种是复制算法Copying。它将内存分成两个区域平时只用一块回收时把存活对象整个复制到另一块再清空当前块。实现简单、无碎片而且只要遍历存活对象即可但代价是需要浪费一半内存空间来当“备用区”。你可以把它想象成搬家时先租个临时仓库把要留下的东西搬过去再回来清空老房子东西搬得轻松可仓库租金是实打实的成本。三种算法其实没有一个全能的所以现在主流托管运行时里的垃圾收集器基本都是它们的组合变体。理解每种方案的痛点才能理解为什么后面会有分代回收这种设计。算法回收依据主要优点主要缺点典型适用场景引用计数计数器归零实时回收、实现直观循环引用、线程竞争开销小型对象、无环场景标记-清除可达性分析解决循环引用、实现简单内存碎片、全堆扫描老年代、整堆回收标记-压缩可达性分析无碎片、空间利用率高搬运成本高、停顿明显老年代整理复制算法可达性分析无碎片、回收高效浪费一半空间对象存活率低的年轻代3. 谁是垃圾由谁来定GC Roots 与可达性分析3.1 根集合从哪些地方出发找活对象搞清楚了基础算法接下来一个关键问题就是标记是从哪些点开始的这个起始点集合在垃圾收集里叫 GC Roots也就是根集合。根集合包含的东西其实是可穷举的比如当前正在执行的方法里被局部变量引用的对象静态变量引用的对象还有一些底层运行时结构持有的引用像是线程栈上的对象引用等。这些东西有一个共同点它们是程序当前正在使用或可以立即使用的入口。从这些根出发沿着引用关系一路走下去所有能走到的对象都是“活”的走不到的就是“死”的。这个判定方法在业界叫可达性分析它在逻辑上要比引用计数严谨得多因为判断的是“这张引用关系网里你是否还有路径连接到程序入口”。你可以把根集合想象成房间的电源总闸从总闸出发能点亮的那串灯泡就是活对象没有连上总闸的线路即便看起来还通着电也只是虚电随时可以拆掉。3.2 三色标记法让并发标记不混乱可达性分析听起来不算复杂但真实场景里一个难点在于标记过程中程序还在运行对象的引用关系时刻在变。为了在并发环境下把标记做对做细现代垃圾收集器普遍采用三色标记法。它把对象分成三种颜色白色代表还没扫描到灰色代表已经发现但它引用的对象还没处理完黑色代表这个对象和它引用的对象都处理完了。标记开始时所有对象都是白色根对象变成灰色。然后循环做两件事取出一个灰色对象把它变成黑色同时把白色引用对象变成灰色。等到没有灰色对象时剩下的白色就是真正没人可达的了。这个过程看起来简单但并发环境下会有“漏标”风险。怎么避免通常要配合写屏障也就是在引用关系发生改变时额外做一些记录或补救动作保证新建立的引用也能被后续标记感知到。三色标记是理解现代垃圾收集器并发机制的钥匙你后面如果去看各种回收器的停顿描述会发现很多术语都绕不开它。3.3 引用类型给回收留出“协商空间”除了“强引用”这种默认的生死与共的模式主流托管运行时还提供了软引用、弱引用、虚引用这几类特殊引用用来表达“可以协商”的对象生命周期。软引用的对象在内存充足时不会急着回收内存紧张时就会被清理掉适合做缓存类数据弱引用的对象只要被垃圾收集一扫描发现它只有弱引用可达就直接回收适合做“有最好、没有也没关系”的旁路数据虚引用最特殊它本身几乎不决定对象生死更多是配合底层内存管理做回收通知用。我自己的理解是强引用是“这是我的东西谁也别动”软引用是“平时留着真没钱了再卖”弱引用是“路边小广告没人在意就撕掉”。理解这一层对后面排查内存问题非常有帮助因为很多“GC 收了但没效果”的案例本质上是对象其实是通过强引用被根拽住了。4. 现实世界的 GC分代回收与停顿博弈4.1 弱分代假说为什么大多数对象活不过第一轮前面提到复制算法有个空间浪费问题为什么现实中的垃圾收集器还是大量使用复制思想关键就在于一个统计规律绝大多数对象的存活时间极短。比如你在循环里临时创建的对象、方法调用产生的中转数据往往在创建之后几个操作内就再也没人用了。这个规律叫弱分代假说。既然大多数对象很快死掉那么“把内存分成两块、用复制算法回收年轻对象”就很划算。存活对象比例不高需要复制的就少即使浪费一半空间换来的是可以在密集的年轻对象回收中获得很高的效率。反过来那些经过多轮回收还活着的对象大概率会活很久就叫老年代对象处理它们的算法就要换思路了。4.2 年轻代里的 Eden 与 Survivor复制算法的工程化改造直接分两块区域做复制会浪费一半内存所以工程实践上做了优化。年轻代被进一步分成 Eden 区和两块 Survivor 区新对象统一在 Eden 区分配。进行一次年轻代回收时把 Eden 里存活的对象复制到一块 Survivor而之前已经在另一块 Survivor 里活过一轮的对象则提升年龄超过阈值的直接升入老年代。这样做的好处是不需要把整块年轻代空间都当成“备用区”浪费的空间被压缩到两块 Survivor 之一同时还能借助年龄统计把真正长命的对象逐步送到老年代。你去看主流托管运行时核心思路大致都是这个框架局部用复制算法应对“朝生夕死”的对象整体用标记-压缩或标记-清除去管理老年代。这样设计最直接的效果是回收动作大部分时候只发生在年轻代这个小范围里停顿时间相对短。老年代则因为对象少、存活率高回收频率相对低每次停顿虽然长一些但不会频繁打扰程序。4.3 吞吐优先还是延迟优先回收器的两条技术路线理解了分代结构再去看不同垃圾收集器的差异就清楚了。它们本质上是同一套原理下对“吞吐量”和“停顿时间”做了不同的取舍。偏向吞吐量的方案会尽量把回收工作集中起来批量做GC 期间暂停应用但总消耗时间相对小适合后台任务、离线计算的场景。偏向延迟的方案则会把标记和清除的大部分工作拆散与应用线程交替执行尽力缩短单次停顿适合在线服务这种对响应时间敏感的场景。还有一类分区式思路不再严格按连续的年轻代和老年代整块处理而是把堆切成更小的区域每次只回收部分区域用于精细控制停顿。回收器风格核心追求停顿表现适合场景串行回收最简单、单线程长时间停顿小型应用、开发环境并行回收高吞吐量每次停顿较长但总时间少后台任务、离线计算并发标记清除低延迟较短停顿多数与应用并发在线业务、交互服务分区式回收停顿可预期细粒度停顿兼顾吞吐大堆、大规模服务5. 从日志到行动GC 调优的完整排查思路5.1 第一步打开 GC 日志别再用“重启大法”糊弄问题前面提到朋友那个线上服务的问题我最开始的建议就是先开 GC 日志。很多团队直到内存告警了才想起来看日志结果日志保留时间不够或者干脆没开最后只能靠重启缓解。这不是解决问题的办法只是把问题往后推。主流托管运行时基本都支持通过启动参数打开 GC 日志。以常见的 Java 虚拟机为例新版本可以直接用-Xlog:gc*这种模式把日志输出到文件设置日志文件的滚动和保留份数。我习惯把 GC 日志放到独立目录单独设置保留策略避免它在运行一段时间后把磁盘写爆。日志信息包括每次回收发生的时间点、回收前用了多少内存、回收后还剩多少、总共耗时多长这些是判断内存健康程度的第一手材料。打开 GC 日志本身对性能的影响微乎其微却能在出问题时给你留下最可靠的证据链。对于任何要长期运行的服务我都建议把 GC 日志当做和业务日志同样重要的基础设施来对待。5.2 第二步读懂日志里的关键信号拿到 GC 日志之后不要抓瞎。我一般先看几类指标单次 GC 的停顿时间是否稳定年轻代回收频率是否过高老年代回收即 Full GC 出现了多少次以及在老年代回收前后堆占用是否一直维持在高位不回落。有个概念要搞清楚就是分配速率。分配速率是单位时间内新对象占用的内存量它比“堆用了多少”更能反映代码层面的健康度。如果 GC 频率突然上升往往不是 GC 本身出了问题而是分配速率被某段代码拉高了。比如一个循环里无意识地创建了大量临时对象堆再大也扛不住。除了看日志还可以用运行时自带的监控命令观察内存池的使用趋势比如查看各个内存区域的使用率、老年代对象年龄分布等。把这些数据和 GC 日志对照起来看才能判断当前的问题是“堆整体偏小”还是“某个区域明显异常”。5.3 第三步分清“堆太小”与“真有泄漏”很多新手一看到 Full GC 频繁第一反应就是调大堆内存。但实践里这个方向经常是错的。GC 频繁无非两种原因一是对象总量确实超过了当前堆的容量二是存在泄漏程序已经不再使用的对象却始终被引用链拽着导致堆里“活对象”越来越多。怎么区分我通常看这样一个趋势把 GC 日志里每次回收之后的堆占用拉成一条曲线。如果曲线整体平缓只是偶尔冲到高峰那多半是峰值压力大堆容量可以适当调大。如果曲线持续爬升、每次回收后占用都比上一次高那就不是堆大小的问题了而是有对象一直在累积再怎么调堆也只是推迟崩溃。确认是泄漏以后需要用堆转储工具拿到内存快照分析那些占据大量空间的到底是什么类型的对象再顺着引用链找到它是被谁持有。这一步才是根因所在。我见过不少案例表面上是 GC 线程忙个不停实际元凶就是一个静态容器不断塞对象或者某个缓存用了强引用没有清理策略。5.4 我踩过的三个调优误区第一个误区是堆越大越好。大堆确实能延缓触发 GC 的时间但单次回收的扫描范围也会变大停顿时间往往会更长。在线服务里宁可让 GC 稍微频繁一点也不能让单次停顿冲上秒级。第二个误区是一上来就调参数。正确的顺序应该是先确认问题到底是分配过快、对象不释放、还是堆容量不够。没有日志和快照做依据的调参基本靠猜运气好解决了运气不好下一个爆点藏在别处。第三个误区是忽略分配速率的恶化。分配速率突然上升常常是代码改动引入的比如一个更频繁的循环、一次更大的集合遍历。遇到这种问题即使你把所有 GC 参数都调一遍也不如从代码层面把临时对象的产生量降下来有效。6. 一个典型的“GC 背锅”案例与我的排查习惯6.1 缓存用了强引用GC 只能干瞪眼前阵子有个项目组说他们的服务频繁 Full GC老年代一直在涨怀疑是垃圾收集器不行想换个延迟更低的回收器。我去看 GC 日志发现老年代每次回收之后占用都会比以前高一截这个趋势基本可以排除回收器本身的问题。后来做了堆转储发现某个全局缓存结构里存了大量业务对象而这些对象被取出来后业务代码仍然长期持有引用等于把缓存里的对象又“借”了一份出去一直不还。根因清楚了以后方案其实很简单把缓存里的引用改成弱引用语义让对象在外部不再使用时能够被正常回收同时给缓存加上容量上限和过期机制。最终效果立竿见影老年代占用曲线直接回落Full GC 次数大幅下降压根不需要换回收器。这种案例我看过不止一次所以现在最怕听到的不是“GC 调不动”而是“你知道给我换个回收器就行”这种话。抛开证据链谈调优多半是无用功。6.2 我的排查顺序已经固定成四条线现在我自己处理内存问题基本固定一套流程。第一步看 GC 日志确认回收暂停、回收前后占用和频率。第二步看分配速率判断是不是代码层面产生了大量临时对象。第三步看堆转储确认大对象分布和引用链。最后一步才是调参数而且每改一个参数都要观察一段时间再做结论。这套流程看起来朴素但比到处搜“XX 回收器参数怎么配”靠谱得多。如果是团队协作我还会建议把 GC 日志采集、关键指标监控提前接好而不是等出事故了再去回溯。内存问题和性能问题一样越早暴露越好处理回头去找证据是最费劲的。6.3 最后分享一个我判断 GC 是否异常的土办法如果你刚开始接触 GC连日志都不想看可以先加一个粗略的监控把“每秒新分配了多少字节”和“老年代回收后占用变化”记录到定时任务里持续观察几天。正常服务的年轻代分配速率会有波动但老年代回收后的占用水平应该是相对稳定的。一旦发现老年代回收后的占用水平持续走高哪怕走得很慢也值得警惕这往往比单次 Full GC 持续时间长更能说明问题。花五分钟把这个监控挂上可能比你在配置文件里反复折腾半天参数要有价值得多。我自己这些年调 GC 最大的体会就是垃圾收集本身不可怕无视证据、凭感觉调参才可怕。
RELATED

相关推荐

车辆行人检测课设全攻略:从YOLO选型到部署答辩

车辆行人检测课设全攻略:从YOLO选型到部署答辩

又到课程设计季,车辆行人目标检测这种经典题目一下子就成了很多计算机视觉方向同学的刚需。你可能会在各种地方看到类似“车辆行人目标检测系统 深度学习 计算机视觉 课程设计 数据集 代码(源码lw部署文档讲解等)”这样的教程链接,我也接到过不少同学的私…

📅 2026/10/22 18:09:21
AI Agent编程实战:从工具人到自动干活,小白也能上手

AI Agent编程实战:从工具人到自动干活,小白也能上手

从“问答机器”到“自动干活的员工”:AI Agent到底给编程带来了什么 如果你最近刷技术社区,大概率已经看到过类似的标题——“AI Agent彻底颠覆编程开发”“传统AI秒变工具人”“小白也能写出神仙代码”。老实说,第一次看到这类说法我是带点怀…

📅 2026/10/22 18:04:17
YOLOv8引入可变形自注意力:小目标检测涨3.4个点的实操复盘

YOLOv8引入可变形自注意力:小目标检测涨3.4个点的实操复盘

第一次把可变形自注意力模块写进YOLOv8的yaml时,我得到的第一组结果并不好看:mAP50不涨反跌,掉了大约0.7个点。后来把恒等连接去掉、把插入范围从“每个阶段都加”收敛到P4/P5高层特征,同一组实验的mAP50直接抬升3.4个点&#xff…

📅 2026/10/22 18:04:17
MORE NEWS

更多资讯

📰

大数据面试核心考点:HDFS读写、Spark调度与Flink状态机制全解析

去面大数据岗位,不管是校招还是社招,问来问去其实就那么几类问题。早年大家还在背“Hadoop生态有哪些组件”,现在面试官更爱追问原理、场景题和手写SQL。我在一线做了几年数仓和实时计算,也面过不少人,今天就把大数据面…

📰

人工智能、医学工程和教育:医工交叉的闭环逻辑与落地实践

一、这本书的出现,本质上是在解决一场跨学科的对话困境 我有幸在整理电子书库时翻到这本《人工智能、医学工程与教育》,起初以为它只是把三个热门关键词打包在一起的合集,翻了几章后才发现,它实际上是用一整本书的篇幅&#xff0c…

📰

OpenClaw自动记忆引擎:行为触发式知识沉淀系统

1. 项目概述:这不是一个“记忆软件”,而是一套可嵌入、可演化的自动记忆行为引擎“OpenClaw 自动记忆系统设计的技术探讨”——光看标题,很多人第一反应是“又一个笔记AI摘要工具?”但实际完全不是。我接触过这个项目的早期原型后…

📰

Ubuntu 18.04远程访问实战:MobaXterm SSH连接配置与排查指南

1. 先别急着敲命令,把整条链路想清楚1.1 你面对的不只是“装个系统”这么简单“装Ubuntu 18.04通过MobaXterm的ssh远程访问”这个标题看起来很直白,但真正在电脑面前操作过一轮就会发现,这里面至少叠了三层任务:第一层是把Ubuntu …

📰

免安装Android Studio,用命令行工具管理Android SDK与模拟器

简介:这是一份面向Windows平台的Android命令行工具包,专为不需要安装完整Android Studio的开发者设计,可解决SDK组件下载、应用编译、设备连接与调试等常用开发问题。压缩包共108个文件,其中95个为jar格式的工具组件,1…

📰

SSM商城项目实战:Java初学者掌握Spring+MyBatis核心配置的跳板

简介:这是一套基于SSM(SpringSpringMVCMyBatis)框架与MySQL数据库开发的完整线上商城系统,含前台用户端与后台管理平台,面向Java Web初学者及中级开发者,适用于课程设计、毕业项目或SSM技术栈实战训练。资源…

TODAY

今日更新

THIS WEEK

本周精选

THIS MONTH

本月热门

读完文章,想聊聊您的网站?

告诉我们您的行业与需求,资深顾问一对一梳理方案与报价,全程免费。

📞 💬