news 2026/9/9 10:06:00

深入拆解G1垃圾回收器:从原理到调优实践

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
深入拆解G1垃圾回收器:从原理到调优实践

G1垃圾回收器从JDK 7u4开始进入实验状态,到JDK 9正式成为默认回收器,再到如今几乎成为Java服务端的标配。整个过程我算是全程经历过,早期用户怕它不稳定,后来是怕它不会调,现在大部分人其实是既没完全搞懂它怎么工作,又不得不面对它留下的各种GC日志和停顿数据。这篇东西我不打算做成官方文档的复读机,而是把我自己排查线上问题、翻源码、做压测时沉淀下来的对G1的理解写成一篇比较完整的拆解。适合刚接触G1的新手从头建立框架,也适合已经用过一阵子但某些环节还有点模糊的老手查漏补缺。

1. 为什么G1能取代传统回收器

先聊一个最基础但也最容易被忽略的问题:G1到底解决了什么痛点,能一路从实验品混到默认地位。我们以前用Parallel Scavenge和CMS,走的是"物理分代"的思路。年轻代、老年代是连续的内存块,回收时要么整块清理年轻代,要么在老年代里做标记清理,停顿时间不可控。CMS虽然号称并发收集,但它的碎片问题、Full GC退化问题在后期的长生命周期服务上非常难受。

G1把物理分代改成了逻辑分代,引入了Region这个概念。整个堆被切成一个个大小相等的小格子,默认2048个左右,每个Region在运行时可以属于Eden、Survivor、Old或者Humongous。正因为每个Region的身份是动态的,G1才能做到"不回收整个老年代,而是挑一部分Region来回收"。这个"挑"的动作,就是G1控制停顿时间的核心手段。

用大白话讲,以前的回收器是"全量清扫",G1是"精准拆弹"。它维护了一个优先级列表,每次垃圾回收只处理垃圾最多的那批Region,同时用停顿预测模型估算这次回收大概需要多长时间,如果预测超过你设定的目标停顿时间,它就减少回收的Region数量。整个过程优雅,但代价就是G1的内部数据结构非常复杂,尤其是记忆集(Remembered Set,简称RSet)和SATB快照这两个东西,是理解G1绕不过去的坎。

另外一个很多人忽视的特点是,G1的Full GC是退化的。真正意义上的Full GC是串行、单线程、STW全堆回收,效率极低。所以G1的设计哲学是:尽量通过Young GC和Mixed GC把垃圾处理完,避免进入Full GC。这也是我们在调优时最核心的指导思想。

2. 核心概念拆解:Region、RSet与SATB

2.1 Region的大小与分代流转

Region大小默认是1MB到32MB,必须是2的幂次方。JVM启动时会根据堆大小自动计算,目标是让堆大约分成2048个Region。假设你的堆是4GB,那么默认Region大小就是2MB。手动指定用-XX:G1HeapRegionSize,但我建议非特殊情况别手填,让G1自己算通常更合理,因为它是基于整个堆的生命周期评估的。

Region的分代身份是动态的。一个Region这次是Eden,YGC之后可能变成Survivor,再经过几轮可能晋升为Old。这种动态性带来一个好处:老年代不再要求物理连续。但也带来一个麻烦:跨Region引用怎么跟踪?以前老年代是一整块连续内存,引用扫描只需要知道边界就行,现在全乱了。这就是RSet存在的理由。

2.2 记忆集(RSet)为什么是G1的命脉

RSet的本质是"反向指针表",记录的是"哪些Region引用了当前Region里的对象"。每个Region都有一份RSet,当其他Region有对象引用当前Region时,这个引用关系会被记录在当前Region的RSet中。

这里必须说清楚一个重要的方向性:RSet是"谁引用了我",而不是"我引用了谁"。为什么需要反过来记?因为GC发生时,我们要找到某个Region的根对象。如果没有RSet,要遍历整个堆才能判断某个Region里的对象是否存活,这等于做全堆扫描,停顿时间直接失控。有了RSet,只要查当前Region的RSet,就知道有哪些外部引用指向这里,这些引用就是本次回收的根,不需要扫描所有Region。

RSet内部是分卡表(Card Table)实现的,每个卡页对应512字节的堆空间。一个卡页里只要有一个对象被跨Region引用,这个卡页就会被标记为脏卡,RSet记录的是这个卡页的位置而不是精确到对象级别。这种粗粒度的代价是,GC时要扫描整个脏卡页里所有的对象引用,精度虽然降低但性能可控,这是一个典型的"空间换时间、精读换性能"的工程取舍。

写屏障(Write Barrier)是维护RSet的机制。每次发生引用赋值时,JVM会执行一段屏障代码,检查引用是否跨Region,如果跨了就更新RSet。这个检查是相当频繁的,所以G1用了一个G1DirtyCardQueue来批量处理脏卡更新,后台有专门的线程处理这些队列,避免写屏障在应用线程里做太多同步操作。这块在并发高、引用写入频繁的场景下是需要特别关注的热点,你在JVM日志里看到的Ref ProcRedirty Cards耗时,就是这一套机制的代价。

2.3 SATB快照:并发标记的基石

G1的并发标记用的是SATB(Snapshot-At-The-Beginning)算法。先说结论:SATB的核心思想是,在标记开始时给堆打一个逻辑快照,这个快照保证标记过程中新分配的对象全部认为是存活的,被删除的引用关系不会导致对象被漏标(漏标会导致存活对象被错误回收,这是致命的)。

具体实现上,G1用三色标记法做标记:白色表示未访问,灰色表示已访问但引用未处理完,黑色表示已处理完。并发标记过程中,应用线程还在跑,引用关系不断变化,如果对象从黑色变成白色(即黑色对象引用的对象被并发修改了引用),就会发生漏标。

CMS当年是用增量更新(Incremental Update)解决这个问题,做法是记录"黑色对象新增了引用"这种变化,然后重新标记黑色对象。G1则走另一条路:把"灰色对象引用被删除"这件事记下来。因为SATB保证了标记开始时的一个快照,被删除的引用在快照里仍然是存在的,所以这些对象虽然实际上已经不可达,但仍然会被标记为存活。

这个设计有一个直接的后果:并发标记期间产生的浮动垃圾(Floating Garbage)需要留到下一轮GC才能回收。你以为回收干净了,其实老年代里还有一批"理论存活、实际已死"的对象占着空间。所以G1周期结束后老年代占用率往往不会降到预期水平,这是正常现象,不是GC出了问题。

我用一个日常场景来类比:SATB就像你出门前给家里拍了一张照片,等你回来核对物品清单时,即使中间有人把某个东西扔了,你看到照片里东西还在,就会认为它没有丢。代价就是你可能要多保管它一段时间。

3. G1垃圾回收的完整流程拆解

G1的回收流程不是单一动作,而是由多个不同类型的GC组合成的一套循环。下面分成四个阶段来讲:Young GC、并发标记周期、Mixed GC和Full GC。这几个阶段不是串行关系,而是根据堆状态穿插进行。

3.1 Young GC:最频繁的回收动作

Young GC在Eden区被填满时触发,过程是STW(Stop The World)的。它会选择所有Eden Region和一部分Survivor Region组成回收集合(Collection Set,简称CSet),然后做根扫描、RSet扫描、复制存活对象到空闲Region。

存活对象会根据年龄决定去处:没达到晋升年龄阈值的进入新的Survivor Region,达到晋升阈值或者放不下的晋升到Old Region。这里有个动态年龄判定,G1会统计Survivor中各个年龄对象的总大小,如果某个年龄的对象总大小超过Survivor容量的50%,就取这个年龄作为新的晋升阈值。你可以通过-XX:TargetSurvivorRatio调整这个百分比,默认是50%。

复制完成后,原来的Eden Region和Survivor Region会变成空闲Region,重新加入可用Region队列。这一步是G1区别于CMS的重要优势:复制算法天然没有内存碎片,回收完的空间是完整可用的Region。

G1的停顿预测模型在这里发挥作用。在YGC开始时,G1会基于历史数据预测本次回收的停顿时间。如果预测会超过-XX:MaxGCPauseMillis(默认200ms),G1会尝试把CSet控制在更小的范围内,或者调整年轻代大小来适应。这解释了为什么你设置了目标停顿时间后,年轻代会自动忽大忽小——不是Bug,这是特性。

实操建议:Young GC的触发时机基本不需要人工干预,但要留意日志里的Eden: 6144.0M(6144.0M)->0.0B(6144.0M)这类信息。如果Young GC极其频繁,说明年轻代太小或者分配速率太高,此时要看的不是GC参数,而是业务代码里是否在疯狂创建短生命周期对象。

3.2 并发标记周期:老年代回收的前置条件

并发标记周期是G1能做老年代回收的前提。它有五个阶段,这里逐一拆解。

第一个阶段是初始标记(Initial Mark)。它是伴随着一次Young GC一起执行的,所以不需要额外的STW,借的Young GC的STW窗口完成。这个阶段要做的事情很简单:把所有直接可达的根对象标记为灰色,把它们的RSet加入处理队列。因为这个阶段涉及RSet扫描,停顿时间会略微高于普通Young GC。

第二个阶段是根区域扫描(Root Region Scan)。这个阶段是并发的,扫描的是初始标记中记录的那些根Region中所有存活对象,顺着它们的引用关系往下标记。这里有一个需要特别注意的细节:这个阶段不能有新的Young GC发生,因为Young GC会移动对象,导致根集合变化。如果并发标记周期执行期间堆内存压力很大导致Young GC频繁,根区域扫描的线程就得频繁让路,整个周期会被拖长。

第三个阶段是并发标记(Concurrent Mark)。这个阶段和应用线程完全并发执行,顺着灰色对象往下遍历整个对象图。每个Region会记录自己的存活对象数量和存活字节数。同时SATB队列里的引用变化信息也会被处理。这个阶段如果耗时过长,通常不是G1本身的问题,而是对象图过于庞大。GC日志里Concurrent Mark的耗时超过几秒甚至十几秒,基本可以确定是堆里对象引用关系太复杂或者对象数量过多。

第四个阶段是重新标记(Remark)。这是STW的,主要处理两个事情:一是处理SATB队列中剩余的变化记录,二是做一次全局的强根扫描(应用线程的栈、JNI引用等)。全局强根扫描之所以必须STW,是因为只有应用线程停下来,JVM才能安全地遍历线程栈。这个阶段的耗时和线程数、类加载器数量强相关,多线程环境下通常能控制在几十毫秒到几百毫秒。

第五个阶段是清理(Cleanup)。这个阶段分两部分:全局并发清理和STW清理。并发部分统计每个Region的存活对象,按存活率排序,为后续Mixed GC选择Region做准备。STW部分负责处理RSet中的引用关系,以及把完全空闲的Region回收回来。这里有个常见误区:Cleanup阶段不回收任何有存活对象的Region,它只是"记账和整理房间"。

整个并发标记周期的触发条件由-XX:InitiatingHeapOccupancyPercent控制,默认是老年代占比达到45%。注意,这个值踩过的坑特别多,后面会单独讲。

3.3 Mixed GC:G1的"精准拆弹"阶段

并发标记周期结束后,G1知道了老年代里各个Region的垃圾比例。接下来不是一口气把老年代全回收了,而是根据回收收益来挑选Region,这些Region加上年轻代一起做回收,就叫Mixed GC。

Mixed GC的过程在机制上和Young GC类似:也是STW,也是复制存活对象,区别只在于CSet里除了Eden和Survivor Region,还包含了一部分Old Region。那选哪些Old Region呢?G1会按两个标准排序:垃圾比例高的优先,回收成本低的优先。-XX:G1MixedGCCountTarget控制一个标记周期内执行多少次Mixed GC,默认是8次,也就是拆成8轮慢慢回收。-XX:G1MixedGCLiveThresholdPercent控制只有存活率低于85%的Region才会被选进CSet,存活率高于这个值的Region,回收它代价大于收益,不如留着。

这里我要特别强调一个经常被误解的点:Mixed GC不是一种独立的GC类型,在GC日志里它仍然显示为GC pause (mixed),你可以把它理解为"老年代辅助参与回收的Young GC"。如果你在日志里长时间只看到GC pause (young)而看不到GC pause (mixed),说明并发标记周期根本没跑完,或者跑完没有合格的Old Region可以收。

Mixed GC执行到第几轮就停,取决于G1对回收收益的判断。如果后续几轮的利润太低,G1会提前结束本周期的Mixed GC。这会导致老年代占用率没有降到理想水平,但这其实是G1在保护你的停顿时间,是可接受的。

3.4 Full GC:G1的最后手段与退化陷阱

Full GC是G1最不想走的路,因为它意味着G1之前的所有努力全部白费。G1的Full GC是单线程串行回收整个堆,停顿时间动辄几秒到几十秒,这对于线上服务基本等同于故障。

导致G1 Full GC的原因主要有四类。第一类是并发标记失败(Concurrent Mode Failure),并发标记周期还没结束,老年代就被填满了,没空间分配新对象。第二类是晋升失败(Promotion Failure),Young GC时Survivor和Old区都放不下要晋升的对象,触发Full GC。第三类是巨型对象(Humongous)分配失败,大对象直接进入老年代并占用连续多个Region,分配时找不到足够的连续Region。第四类是元空间(Metaspace)扩容触发Full GC,这个属于全局性原因,不能全怪G1。

判断Full GC的原因,最简单的办法是在JVM参数里加-XX:+PrintGCDetails,或者用-Xlog:gc*=debug看日志。日志里Full GC (Allocation Failure)说明是分配失败,Full GC (Metadata GC Threshold)说明是元空间问题。不同的原因,解决方案天差地别,千万别看到一个Full GC就盲目调堆大小。

我见过不少团队,一遇到Full GC就把堆调大,结果问题更严重。因为堆越大,G1的并发标记周期越长,对象在Region间拷贝的成本也越高。调优的思路应该是:先确认为什么老年代会满,是存活对象确实多,还是浮动垃圾太多,还是晋升速率异常,然后对症下药。

4. 关键参数与调优实操

4.1 参数速查与匹配场景

参数不在多,在于知道每个参数在什么场景下动它才有意义。下面按作用维度列一个速查表。

参数默认值作用什么时候调
-XX:+UseG1GCJDK9+默认开启启用G1基本不用手动加,但如果还在用JDK8要显式开启
-XX:MaxGCPauseMillis200msG1的目标停顿时间业务对延迟敏感时下调到100或50,但别设太低否则会频繁YGC
-XX:G1HeapRegionSize自动计算Region大小极少手动调,巨型对象场景可能按需调整
-XX:InitiatingHeapOccupancyPercent45触发并发标记的老年代占用率老年代回收不及时导致Full GC时下调,反之可上调
-XX:G1MixedGCCountTarget8一个标记周期内Mixed GC次数Mixed GC耗时太长可增大数值分摊
-XX:G1MixedGCLiveThresholdPercent85Old Region存活率高于此值不回收老年代有大量低存活率Region时可下调
-XX:ParallelGCThreads按CPU核数计算STW阶段的并行线程数容器环境核数识别不准时需要手动指定
-XX:ConcGCThreadsParallelGCThreads的1/4并发阶段线程数并发标记耗时长时适当调大

关于ParallelGCThreads,有个很容易踩的坑。在容器环境里,JVM默认获取的是物理机的核数而不是容器分配的CPU配额。如果你的容器只分到2核,但物理机有32核,JVM按32核来计算并行线程数,结果就是STW时创建大量线程,上下文切换开销巨大。JDK 8u191之前必须手动指定-XX:ParallelGCThreads,之后的版本开始支持容器感知,但建议在高规格容器环境中还是确认一下实际生效值。

4.2 MaxGCPauseMillis该怎么设

很多人的第一反应是把目标停顿时间设得越小越好,比如20ms、50ms。但实际上这个参数是G1的"软目标",不是硬性承诺,而且设得太低会导致一个很尴尬的局面:G1为了保证停顿时间达标,会不断压缩年轻代大小,年轻代变小了,Eden区很快就满,Young GC触发频率大幅上升。

频率上升带来的影响是:GC总开销反而增加的,而且每次GC的promotion也会变多,因为对象年龄没到就不得不晋升了,晋升到老年代的对象增多,老年代压力变大,触发Full GC的风险反而升高。

从我自己的实践经验来看,比较合理的做法是:先观察当前GC的真实停顿时间,再结合业务的延迟要求来设置。如果线上P99延迟要求在100ms以内,可以把MaxGCPauseMillis设在80~100ms;如果业务本身就是离线任务,对停顿不敏感,没必要设这个参数,默认200ms就挺好。

另外,设置这个参数之后,必须用GC日志持续观察一段时间,看G1是否真的把停顿时间压在了目标内。如果你发现日志里GC pause实际停顿经常超过200ms,那问题不在这个参数,而在其他环节,比如RSet扫描时间过长、根扫描太慢,这些和CPU核数、对象引用密度有关,单纯调低目标值解决不了问题。

4.3 IHOP参数的正确调整姿势

-XX:InitiatingHeapOccupancyPercent控制的是老年代达到多少占比时触发并发标记。默认45%对很多应用来说可能偏保守,也可能偏激进,取决于你的业务对象存活率。

先说偏保守的场景:如果老年代长期在10%到20%之间徘徊,45%的阈值就太低了,导致G1频繁启动并发标记周期,但每次标记完发现没多少垃圾可以收,白白消耗了并发标记的CPU开销和SATB快照带来的浮动垃圾,得不偿失。这种情况应该上调IHOP,比如到60%甚至70%。

再说偏激进的场景:如果老年代增长很快,经常冲到60%、70%以上,还没来得及等到45%触发标记周期完成,老年代就满了,引发Concurrent Mode Failure。这种时候单纯调低IHOP不一定是最优解,因为混合回收的进度可能跟不上老年代的增速。核心还是要看老年代的增长速率和回收速率是否匹配。

有一个比较容易忽略的知识点:-XX:UseDynamicNumberOfGCThreads和自适应IHOP(Adaptive IHOP)其实是G1的默认行为。G1会根据历史数据分析老年代的增长趋势,在达到IHOP阈值前就提前启动并发标记。所以你在日志里看到并发标记的启动时机偶尔早于IHOP阈值,不用慌,这是自适应逻辑在起作用。

4.4 调优顺序与日志解读

新手经常犯的一个错误是,一上来就东改一个参数西改一个参数,改完看效果不好又回滚,来回折腾。正确的调优顺序应该是:先看日志、再定位瓶颈、最后才动参数。日志是G1给你的一切真相,不看日志调参等于闭眼开车。

开启GC日志的方式,JDK8用-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/gc.log,JDK9+用-Xlog:gc*:file=/path/gc.log:time,uptime,level,tags。线上环境建议始终带着GC日志,而且要做好日志轮转,不然文件能涨到好几个G。

日志解读的核心关注点依次是:Full GC是否出现、Mixed GC的频率和回收量、Concurrent Mark的耗时、Young GC的停顿时间分布、Eden/Survivor/Old三个区域的大小变化趋势。我在排查时还会特别关注To-space exhausted这类关键字,说明分配速率远高于回收速率,这是典型的容量规划和分代比例问题。

5. 常见问题与排查技巧实录

5.1 Concurrent Mode Failure:并发标记赶不上分配

这个错误是我在实际工作中遇到最多的G1高危故障。触发条件是:并发标记周期还没跑完,老年代就已经满了,JVM无奈之下只能直接进入Full GC。日志通常会显示Concurrent Mode Failure外加一个停顿时间特别长的Full GC记录。

排查时先搞清楚你设置的最大堆大小是否合理。如果老年代本身就很大且存活率高,并发标记的时间会非常长。此时要检查几个数据:老年代Region的平均存活率、分配到老年代的速率、并发标记各阶段的实际耗时。

解决方案通常有几个方向。第一,检查是否有大对象频繁分配,如果Humongous对象过多,考虑调大Region大小,让单个大对象能放进一个Region而不是占满多个连续Region。第二,优化业务的分配速率,比如减少不必要的缓存对象、复用已有对象。第三,适当调低IHOP,让并发标记早点开始,给标记周期留出充足的时间窗口。其实这个优先级是有讲究的,先看代码分配,再看参数,别动不动就堆内存。

5.2 晋升失败与Survivor空间不足

晋升失败是另一种高频问题。它的本质是:Young GC时,存活对象要晋升到Survivor或者Old区,但这两个区域都没有足够的空间容纳晋升的对象。日志表现为GC pause (young) (promotion failed),随后通常会跟一个Full GC做兜底。

我见过一个比较典型的案例:一个高吞吐的订单处理服务,对象创建速率极高,Young GC频率快,但Survivor区因为年轻代被MaxGCPauseMillis压得很小,只有几百MB。每次YGC存活对象都塞不下Survivor区,被迫提前晋升到Old区,Old区增长迅猛,最后触发Full GC。

这种场景的根因是"GC频率和晋升速率互相踩踏"。解决的思路不是加大Survivor区,而是重新审视MaxGCPauseMillis的设置。目标停顿时间设得太低导致年轻代被压缩,这反而是最需要警惕的自作孽。把这个参数放宽一些,年轻代会变大,Survivor的容量随之变大,晋升压力自然缓解。

5.3 巨型对象导致的连锁反应

G1对巨型对象(Humongous)的处理非常特殊。当对象大小超过Region大小的50%(JDK 8)或者超过Region大小(JDK 11+对数组的特殊处理),这个对象会直接进入老年代,占用若干个连续的Region。这些Region不会被Mixed GC列为优先回收对象,因为它们可能存活率高。

巨型对象最大的问题不是占空间本身,而是它在分配时需要连续的Region序列。如果老年代碎片化严重,即使总空闲空间足够,也可能分配失败,触发Full GC。此外,频繁创建巨型对象会导致老年代Region被"撑爆",但实际存活率很低,回收效率极差。

排查时用jmap或者看GC日志里的Humongous统计就能发现。解决思路是:业务层面尽量避免大数组、大对象缓存,能拆分的拆分;如果实在避免不了,比如要缓存一整个大文件对象,就需要评估Region大小,让对象落到单个Region内而不是跨越多个Region。

5.4 频繁Mixed GC但老年代降不下来

有一种很让人头大的情况:Mixed GC一直在跑,但每次回收量很小,老年代占用率居高不下,应用整体的GC开销却很高。我排查过一个数据分析平台的服务,GC日志里Mixed GC频繁出现,每次回收的Region只有几十个,回收后老年代从60%降到58%,下次周期又涨回去。

这个问题的本质是:老年代里全是存活率高的对象,G1挑不出"垃圾多、代价小"的Region来回收。-XX:G1MixedGCLiveThresholdPercent默认85%,如果绝大多数Region的存活率都在90%以上,G1能选的Region就非常有限,Mixed GC自然回收不了多少。

但这里要问一个更根本的问题:为什么老年代存活率这么高?有一种常见原因是最初的IHOP设置太高,导致并发标记开始得太晚,大量对象已经晋升到老年代并长期存活。这时候单纯调低MixedGCLiveThresholdPercent效果有限,更有效的做法是复盘晋升路径,看是否有缓存对象被频繁误晋升到老年代,或者某些长生命周期业务对象在早期被错误分配到了老年代。另外还要检查-XX:MaxTenuringThreshold和动态年龄判定是否有异常。

5.5 一个相对完整的线上排查流程

最后梳理一套我自己的排查流程,遇到G1相关的GC问题可以直接照这个顺序走。

第一步,定位现象。搞清楚是Full GC频繁、Young GC停顿时间长,还是CPU消耗异常高。不同的现象指向的根因差别很大,别混为一谈。

第二步,拉日志。至少准备最近几小时的GC日志,重点关注Full GC前后的Region变化、并发标记各阶段耗时、Mixed GC的回收量。

第三步,确认堆配置。记录-XmxMaxGCPauseMillisIHOP等关键参数,确认并行线程数是否符合容器配置。

第四步,分析分配速率和晋升速率。通过jstat -gcutil观察Eden、Survivor、Old的变化速率,重点看每秒钟有多少字节被晋升到老年代,这个数值是判断老年代压力最直接的指标。

第五步,确认代码侧问题。用jmap -histo看大对象、用jstack看线程分配热点,必要时用async-profiler抓内存分配采样,找出什么对象在什么地方被大量分配。

第六步,把问题和参数对上号,然后小步调整,每次只改一个参数,观察至少一个完整的并发标记周期再下结论。调优切忌一次改多个参数,否则你根本不知道是哪个参数起的作用。

这套流程走下来,大部分G1问题都能定位到根因。真正快速定位问题的人,不是背了一堆参数,而是能串起"现象、日志、分配行为、代码"这条链,理解它们之间的因果。


最后一个实际工作里的小建议:G1线上调优不要追求极致参数,目标是"不失控"。让G1在你设定的停顿目标下稳定运行,偶尔出现一次超过预设的停顿不必惊慌,先看是不是有System.gc、元空间扩容或者操作系统层面的抖动。GC日志里的时间单位、关键字段能在关键时刻帮你省下大把排查时间,把GC日志完整打开,比背任何调优口诀都有用。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/9 10:05:53

单元测试实战指南:从Spring Boot到Vue与嵌入式全覆盖

干这一行十多年,我见过太多项目死在“单元测试”这四个字上。不是没人写,是写不明白,写出来的测试要么跑一次要五分钟、要么随便改个字段就崩、要么覆盖率报表好看但代码上线照样出事故。我自己也踩过这些坑,所以今天想把这些年做…

作者头像 李华
网站建设 2026/9/9 10:04:23

Matlab绘制振动噪声瀑布图:从数据预处理到阶次分析实战

做旋转机械NVH分析的朋友,十有八九都跟瀑布图打过交道。所谓瀑布图,就是把转速轴、频率轴和幅值轴三组数据叠在同一个三维坐标系里,用一张图把机器从低速到高速的全部振动/噪声特征展示出来。Matlab做这件事最方便,因为Workbench、…

作者头像 李华
网站建设 2026/9/9 10:03:14

2026年AI论文写作工具实测:14款主流工具横向对比与选型指南

写论文这件事,不同人痛苦的点不太一样:有人卡在选题架构,有人卡在文献梳理,有人卡在表达太口语被导师批“没有学术腔”,还有人卡在查重和降重反复折腾。市面上的“智能写作 AI 论文软件”这两年多到爆炸,但…

作者头像 李华
网站建设 2026/9/9 10:02:44

Android崩溃日志捕获:手写CrashHandler实现本地异常记录

做Android开发这几年,我最怕听到的一句话就是:“我手机上一个按钮点了就闪退,你那边有日志吗?”问题是,用户不会帮你抓logcat,也不会adb pull,遇到崩溃你能拿到的往往只有一句抱怨。要快速定位这…

作者头像 李华
网站建设 2026/9/9 10:02:24

hermes-agent:轻量级生产级智能体调度中枢

1. 项目概述:一个被严重低估的轻量级智能体调度中枢 最近在几个技术社区和开源项目讨论区里,反复看到 hermes-agent 这个名字——不是作为某个大模型应用的附属插件,也不是某家AI公司的商业产品代号,而是一个独立、低调、但架构…

作者头像 李华
网站建设 2026/9/9 10:01:41

边缘AI芯片方案下的IMU能力边界:标定、时间同步与融合实践

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华