分代收集理论
当前JVM垃圾收集器基本上都采用分代收集算法,根据对象存活周期的不同将java堆分为新生代与老年代
新生代中对象存活率低每次垃圾收集时都会有大量(近99%)对象死去。可以使用复制算法,只需要复制少量的对象就可以完成新生代的垃圾收集。
老年代中对象存活率高,没有额外空间对老年代对象进行担保,所以必须选择标记清除或者标记整理算法进行垃圾收集。
标记清除或标记整理比复制算法慢10倍以上。
简述为:生代存活率低,适合复制算法(快);老年代存活率高,只能用标记-清除/整理。
垃圾收集算法
标记-复制算法
复制算法将内存划分为大小相等的两块区域,每次只使用其中一块。当已使用的这块区域内存耗尽时,触发GC:将存活的对象依次复制到另一块空闲区域中,然后一次性清空原先的整块区域。
优点:内存规整,无碎片;实现简单,运行高效(只需复制少量存活对象,无需处理死亡对象)采用指针碰撞方式分配内存,效率高。
缺点:内存利用率低,任何时候都有一半内存处于闲置状态,可用内存仅为总容量的一半
标记-清除算法
算法分为“标记”和“清除”阶段。首先在内存中标记存活的对象,然后统一清除未标记的对象,或者先标记需要回收的对象,在统一清除标记的对象。
优点:不需要额外内存空间,实现简单,逻辑清晰
缺点:
效率低:标记和清除都需要遍历所有对象,耗时较长
内存碎片:清除后产生大量不连续的内存碎片,导致后续无法为大对象分配连续空间,可能提前触发另一次GC
标记-整理算法
标记内存中存活的对象,将所有存活对象向内存空间的一端移动,使其紧密排列,然后直接清理掉边界以外的所有内存。
优点:
内存规整:消除内存碎片,为大对象分配连续空间提供保证
分配简单:采用指针碰撞方式分配内存,效率高
缺点:
效率低:比标记-清除多了一个“移动/整理”对象的步骤,需要更新所有引用关系
停顿时间长:整理阶段需要暂停所有用户线程(Stop-The-World)
划分内存的方式:
指针碰撞:堆内存被一块“分界指针”一分为二:一侧是已使用的内存(存活对象),另一侧是空闲内存(可用空间)。分配新对象时,只需将分界指针向空闲方向移动一段与对象大小相等的距离即可
空闲列表:JVM 维护一个列表,记录哪些内存块是空闲的。分配对象时,需要从列表中找到一个足够大的空闲块分配给对象,并更新列表记录。
垃圾收集器
1、Serial收集器 (-XX:+UseSerialGC -XX:+UseSerialOldGC)
Serial收集器是JVM中历史最悠久的垃圾收集器,采用单线程工作,使用复制算法
工作模式:垃圾收集时,仅使用一个线程执行GC在此期间,必须暂停所有用户线程(Stop-The-World,STW),直到垃圾收集结束.
优点:
简单高效:单线程避免了多线程的同步开销。
内存占用低:不需要维护复杂的数据结构。
单核CPU性能最优:在单核或少量核心环境中,无上下文切换开销,吞吐量反而最高。
缺点:
STW时间长:随着堆内存增大,停顿时间线性增长
无法利用多核CPU:在多核服务器上浪费硬件资源
2、Serial Old收集器
Serial Old收集器是Serial收集器的老年代版本,同样采用单线程工作,使用标记-整理算法。
用途:
1、在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用
2、是作为CMS收集器的后备方案,虽然慢,但能保证程序不因内存耗尽而崩溃。
3、Parallel Scavenge收集器 (-XX:+UseParallelGC,-XX:+UseParallelOldGC)
Parallel Scavenge收集器采用多线程进行垃圾回收。默认收集线程数与cpu核数相同。采用复制算法。
特点:Parallel Scavenge收集器的核心关注点是吞吐量(高效率利用CPU),而CMS等收集器更关注用户线程停顿时间(提升用户体验),Parallel Scavenge追求的是整体效率最高,而不是单次停顿最短。
优点:
高吞吐量:核心优势,最大化CPU用于用户代码的时间
多线程并行:充分利用多核CPU,减少GC总耗时
内存规整:采用复制算法,无碎片问题
缺点:
停顿时间长:每次GC必须STW,且单次停顿时间较长
响应延迟不可控:关注吞吐量而非延迟,不适合交互式应用
不能与CMS搭配:设计目标冲突,无法用于低延迟组合
4、Parallel Old收集器
Parallel Old收集器是Parallel Scavenge收集器的老年代版本。使用多线程进行垃圾收集,采用标记-整理算法。
JDK8默认的新生代和老年代收集器:Parallel Scavenge收集器 + Parallel Old收集器
5、ParNew收集器 (-XX:+UseParNewGC)
ParNew收集器其实跟Parallel收集器很类似,都是多线程新生代收集器,采用复制算法,默认线程数都与CPU核数相关。
ParNew是唯一能与CMS配合的多线程新生代收集器。在JDK 8及以前,如果需要在Server模式下实现低延迟,ParNew+CMS是标配组合。当然,JDK 9后CMS和ParNew都被废弃,官方推荐用G1替代。
Parallel Scavenge和ParNew有什么区别?
两者都是多线程新生代收集器,都使用复制算法,但关注点不同:
ParNew是为配合CMS设计的,追求低停顿;
Parallel Scavenge追求高吞吐量,不能与CMS搭配,只能与Parallel Old组合。
6、CMS收集器(-XX:+UseConcMarkSweepGC)
CMS(Concurrent Mark Sweep)收集器是HotSpot虚拟机中第一款真正意义上的并发收集器,核心目标是获取最短回收停顿时间。第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。
特点:
1、垃圾收集线程与用户线程(基本上)同时工作
2、非常适合注重用户体验的应用(Web服务、GUI程序等)
3、使用标记-清除算法实现
垃圾收集过程:
1、初始标记(STW):暂停所有的其他线程(STW),并记录下gc roots直接引用对象,速度很快。
2、并发标记:从gc roots直接关联对象遍历整个对象图,过程耗时较长,可以与用户线程同时运行,但可能导致已标记对象状态发生改变(增量更新解决)。
3、重新标记(STW):修正并发标记用户线程运行时导致已标记对象发生改变的对象(主要处理漏标问题),停顿时间比初始标记稍长,但比并发标记短。主要使用三色标记增量更新算法作重新标记。
4、并发清理:GC线程与用户线程同时运行,GC线程开始对未标记的区域清理,此阶段如果有新增对象会被标记为黑色不做任何处理
5、并发重置:重置本次GC过程中的标记数据,为下一次GC做准备。
CMS 的默认晋升阈值是 6
优点:
1、并发收集:GC线程与用户线程大部分时间同时工作,最耗时的并发标记和并发清理阶段都不需要STW。
2、低停顿:只有初始标记和重新标记两个短暂STW阶段,用户体验基本无感知卡顿。
缺点:
1、对CPU资源敏感:并发阶段GC线程与用户线程争抢CPU,导致应用吞吐量下降
2、无法处理浮动垃圾:在并发标记和并发清理阶段用户线程持续产生新垃圾,这些浮动垃圾本次GC无法处理,只能等到下一次GC再清理
3、大量空间碎片:使用“标记-清除”算法会产生大量空间碎片(通过 -XX:+UseCMSCompactAtFullCollection 让JVM在标记清除后做整理(但会增加停顿))
4、并发模式失败:执行过程存在不确定性,上一次GC还没执行完,新GC又被触发(垃圾回收速度跟不上垃圾产生速度),此时降级为 Serial Old 进行Full GC(STW + 单线程整理 → 超长停顿),通过-XX:CMSInitiatingOccupancyFraction参数降低老年代使用率阈值(如75%),尽早触发CMS GC,避免回收不及时.
什么是Concurrent Mode Failure
CMS并发回收时,老年代垃圾产生速度超过回收速度,导致老年代被填满,此时CMS会降级为Serial Old进行单线程Full GC,导致超长STW停顿。
避免并发模式失败方法
降低老年代使用率阈值更早触发FullGC增加GC频率。
增大老年代空间(堆内存) 降低GC频率。
增加ConcGCThreads 加快并发回收速度但会占用更多CPU。
减少重新标记停顿,间接帮助但会增加一次Minor G。
启用碎片整理,避免大对象晋升失败,但会增加Full GC停顿。
相关核心参数
核心开关参数
- -XX:+UseConcMarkSweepGC:启用cms,会自动启用ParNew作为新生代收集器
- -XX:ConcGCThreads:设置并发GC线程数,默认值取决于CPU核数,通常为(ParallelGCThreads+3)/4
碎片整理参数:
3.-XX:+UseCMSCompactAtFullCollection:FullGC之后做压缩整理(减少内存碎片,但会增加停顿时间)
4. -XX:CMSFullGCsBeforeCompaction:多少次FullGC之后压缩一次,默认0(每次Full GC后都压缩),设为n表示每n次Full GC后压缩1次
触发时机参数
5. -XX:CMSInitiatingOccupancyFraction:当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比)
6. -XX:+UseCMSInitiatingOccupancyOnly:不指定时JVM会动态调整;指定后固定使用CMSInitiatingOccupancyFraction的值
STW优化参数
7. -XX:+CMSScavengeBeforeRemark:CMS重新标记前执行一次Minor GC,降低标记阶段开销(CMS 80%的GC耗时在标记阶段)
8. -XX:+CMSParallellnitialMarkEnabled:表示在初始标记的时候多线程执行,缩短STW
9. -XX:+CMSParallelRemarkEnabled:在重新标记的时候多线程执行,缩短STW
三色标记
在并发标记期间用户线程还在运行,对象引用可能随时变化,会导致多标与漏标问题。
**多标:**原本应回收的对象被标记为存活(浮动垃圾)。
**漏标:**原本存活的对象未被标记。
三色标记算法本身并不解决漏标问题,它只是描述了对象的状态。解决漏标需要配合写屏障 + 增量更新(CMS)或SATB(G1)。
三色标记算法在GC Roots可达性分析遍历对象过程中,按照“是否已访问过”将对象标记为三种颜色:
黑色:对象已被GC访问,且所有引用都已扫描,他是安全存活,黑色对象不可能直接指向白色对象(必须经过灰色)。
灰色:对象已被GC访问,但至少还有一个引用未扫描,处于“正在处理”状态,是扫描的中间节点。
白色:对象尚未被GC访问,初始全是白色,分析结束后仍为白色则是不可达并可以回收。
多标-浮动垃圾
**浮动垃圾:**在并发标记(并发清理)过程中产生的,本应回收但本轮GC未回收的内存对象。
主要来源
1、GC Root失效导致的浮动垃圾
2、并发期间新产生的对象
漏标-读写屏障
漏标会导致被引用的对象被当成垃圾误删除,这是严重bug,必须解决
解决方式:
1: 增量更新(Incremental Update)
黑色对象一旦新引用了白色对象,它就变回灰色对象(需要重新扫描灰色对象所有引用,重新标记 STW 较长)
2:原始快照(Snapshot At The Beginning,SATB)
灰色对象即使删除了对白色对象的引用,GC 仍基于开始时的快照认为这个引用存在,白色对象不会被误删。
注意:白色对象中可能包含本该回收的垃圾,成为浮动垃圾,留到下一轮 GC 再清理。
以上无论是对引用关系记录的插入还是删除, 虚拟机的记录操作都是通过写屏障实现的。
写屏障
定义:JVM在对象引用赋值操作前后插入的一段额外代码,用于维护GC所需的信息。
写屏障实现SATB(G1使用)
核心思想:记录被删除的引用,保证GC开始时所有存活对象在本轮都能被标记到。
写屏障实现增量更新(cms使用)
核心思想:记录新增的引用,保证并发标记期间新建立的引用关系不会导致漏标。
SATB和增量更新有什么区别?
两者都是解决并发标记漏标问题的写屏障实现。SATB(G1使用) 记录灰色对象删除的引用,以快照保证存活,但会产生浮动垃圾;增量更新(CMS使用) 记录黑色对象新增的引用,让黑色对象变灰重新扫描,无浮动垃圾但重新标记STW较长。SATB换来了更短的重新标记停顿,代价是浮动垃圾。
为什么G1用SATB?CMS用增量更新?
记忆集与卡表
为什么需要记忆集?
在新生代GC(Minor GC)做GC Roots可达性扫描时,可能会碰到老年代对象引用新生代对象的情况(跨代引用)。如果为了找出这些引用而扫描整个老年代,效率极低
记录集(Remember Set):一种抽象数据结构,用于记录从非收集区域指向收集区域的指针集合。避免全堆扫描,只扫描有跨代引用的区域。
**卡表(Card Table):**记忆集的一种具体实现,将内存划分为固定大小的卡页(Card Page),用字节数组记录每个卡页是否存在跨代引用。标记哪些内存块包含跨代引用,实现精细化管理
什么是卡表?
卡表是HotSpot中实现记忆集的一种方式,用一个字节数组记录跨代引用信息。每个字节对应512字节的卡页,如果卡页内存在老年代指向新生代的引用,则标记为“脏”。GC时只需扫描脏卡页中的对象,避免扫描整个老年代。卡表通过写屏障维护——发生跨代引用赋值时,写屏障将对应卡页标记为脏。
所有涉及部分区域收集(Partial GC)的垃圾收集器(如G1、ZGC)都会面临相同的问题。
6、G1收集器(-XX:+UseG1GC)
G1(Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。
以极高概率满足GC停顿时间要求(可预测停顿),用可预测的停顿(通过设定MaxGCPauseMillis)替代CMS的“尽可能短但不可控”的停顿。
同时具备高吞吐量性能特征(平衡低延迟与高吞吐。
G1 内存布局核心特征
1. Region 划分
- Java 堆被划分为多个大小相等的独立区域(Region)
- JVM 最多支持 2048 个 Region(Region 大小 = 堆大小 / 2048(如 4GB 堆 → 2MB/Region))
- 可通过 -XX:G1HeapRegionSize 手动指定Region大小,但推荐使用默认计算方式
2. 分代与 Region
- 保留年轻代和老年代的概念,但不再是物理连续的空间
- 年轻代和老年代都是 Region 的集合(可以不连续)
- Region 的角色是可以动态变化的(年轻代 ↔ 老年代),一个 Region 可以从年轻代变为老年代或者老年代变为年轻代(GC 后)。
3. 年轻代大小
- 默认初始占比:5%(如 4GB 堆 → 约 200MB → 约 100 个 Region)可通过 -XX:G1NewSizePercent 调整初始占比
- 运行时 JVM 会动态增加年轻代 Region 数量,但是最大对堆内存占比:不超过 60%(可通过 -XX:G1MaxNewSizePercent 调整)
- 年轻代内部仍遵循Eden : Survivor = 8 : 1 : 1,假设年轻代有 1000 个 Region:Eden 800 个,S0 100 个,S1 100 个
4.G1中大对象的处理(Humongous Region)
- 阈值:对象大小 > 50%单个Region,例如 :Region大小为2MB ,则对象 > 1MB 即为大对象。
- 大对象不进入老年代Region,而是放入 Humongous区,如果一个对象非常大> 100% Region,可能会横跨多个连续的Humongous Region。
- Humongous区专门存放短期巨型对象,不用直接进老年代,节约老年代空间,避免因老年代空间不够而导致的GC开销。
- Full GC 时,除了收集年轻代和老年代,也会将Humongous区一并回收。
垃圾收集过程:
- 初始标记(STW):暂停所有用户线程,并记录下gc roots直接引用对象,速度很快。(同cms)
- 并发标记:从gc roots直接关联对象遍历整个对象图,过程耗时较长,可以与用户线程同时运行,但可能导致已标记对象状态发生改变。
- 最终标记(STW):修正并发标记用户线程运行时导致已标记对象发生改变的对象(主要处理漏标问题),停顿时间比初始标记稍长,但比并发标记短。主要使用三色标记原始快照算法作重新标记。
- 筛选回收(STW):对各个 Region 的回收价值和回收成本进行排序,根据用户期望的 GC 停顿时间(-XX:MaxGCPauseMillis)制定回收计划。
筛选回收阶段
- 回收示例:假设老年代有 1000 个 Region 满了,预期停顿时间200ms,通过历史成本计算回收 800 个 Region 刚好需要 200ms,所以本次只=回收这 800 个 Region(放入 Collection Set)。
- 本阶段会 STW(停顿用户线程),虽然理论上可以并发,但 G1 选择了 STW 以大幅提高收集效率,时间可控(用户可指定),停顿影响有限。
- 使用复制算法,将 Region 中的存活对象复制到另一个 Region,基本无内存碎片。
优先列表(Garbage-First )
G1 会维护一个优先列表,按回收价值与成本比排序,每次根据用户指定的回收时间优先选择回收价值最大的 Region回收。
筛选回收阶段总结:
G1 会按回收价值/成本比对 Region 排序,然后根据用户设定的GC 停顿时间选择本次回收的 Region 集合。例如,目标停顿 200ms,通过历史数据预测回收 800 个 Region 刚好满足,就只回收这 800 个。这个阶段虽然会 STW,但时间可控(几十到几百毫秒)。回收算法采用复制算法,将存活对象复制到其他 Region,因此无内存碎片,不像 CMS 需要额外整理。
G1 垃圾收集分类
1、Young GC(年轻代收集)
触发机制:计算回收时间接近设定的停顿目标时才触发 Young GC。
G1会计算当前Eden区回收大概需要多少时间,如果回收时间远小于用户期望的 GC 停顿时间,继续增加年轻代 Region 数量存放新对象,不马上做 Young GC。
2、Mixed GC(混合收集)不是 Full GC
触发条件:老年代堆占用率达到 -XX:InitiatingHeapOccupancyPercent(默认 45%)
回收范围:所有年轻代 Region、部分老年代 Region(根据停顿时间确定优先级)、大对象区(Humongous)
算法:主要使用复制算法,将存活对象拷贝到其他 Region
失败降级:拷贝过程中如果没有足够的空 Region 承载存活对象会 触发 Full GC
3、Full GC(全局收集)
触发条件:Mixed GC 复制失败 / 分配失败。
工作方式停止所有用户程序(STW),单线程进行标记、清理和压缩整理。整理出空闲 Region 供后续 Mixed GC 使用,这个过程非常耗时(秒级甚至分钟级)。
G1收集器参数设置
1、核心开关与基础配置
- -XX:+UseG1GC : 使用G1收集器,JDK 9+ 默认开启
- -XX:ParallelGCThreads : 指定GC工作的线程数量,默认值取决于 CPU 核数
- -XX:G1HeapRegionSize : 指定Region分区大小,范围 1MB~32MB,必须是 2 的 N 次幂,默认将整堆划分为 2048 个 Region
2、停顿控制与年轻代调优
- -XX:MaxGCPauseMillis : 目标暂停时间(默认200ms)
- -XX:G1NewSizePercent : 新生代内存初始空间(默认整堆5%)
6. -XX:G1MaxNewSizePercent:新生代内存最大空间(默认整堆60%),会动态调整的上限
3、对象晋升与年龄控制
7. -XX:TargetSurvivorRatio :Survivor区填充容量(默认50%),Survivor区域里的一批对象(年龄1+年龄2+年龄n的多个年龄对象)总和超过了Survivor区域的50%,此时就会把年龄n(含)以上的对象都放入老年代
8. -XX:MaxTenuringThreshold:最大年龄阈值(默认15)
4、Mixed GC 触发与回收控制
- -XX:InitiatingHeapOccupancyPercent : 老年代占用整堆内存阈值(默认45%),触发 Mixed GC。比如我们之前说的堆默认有2048个region,如果有接近1000个region都是老年代的region,则可能就要触发MixedGC了。
- -XX:G1MixedGCLiveThresholdPercent :Region 存活对象阈值(默认85%) ,Region 中存活对象低于此值才回收;超过则回收意义不大。
- -XX:G1MixedGCCountTarget : 在一次筛选回收中指定做几次筛选回收(默认8),将一次回收拆分为多次,避免单次停顿过长。比如筛选回收阶段可以回收一会,然后暂停回收,恢复系统运行,一会再开始回收,这样可以让系统不至于单次停顿时间过长。