分代收集理论当前JVM垃圾收集器基本上都采用分代收集算法根据对象存活周期的不同将java堆分为新生代与老年代新生代中对象存活率低每次垃圾收集时都会有大量(近99%)对象死去。可以使用复制算法只需要复制少量的对象就可以完成新生代的垃圾收集。老年代中对象存活率高没有额外空间对老年代对象进行担保所以必须选择标记清除或者标记整理算法进行垃圾收集。标记清除或标记整理比复制算法慢10倍以上。简述为生代存活率低适合复制算法快老年代存活率高只能用标记-清除/整理。垃圾收集算法标记-复制算法复制算法将内存划分为大小相等的两块区域每次只使用其中一块。当已使用的这块区域内存耗尽时触发GC将存活的对象依次复制到另一块空闲区域中然后一次性清空原先的整块区域。优点内存规整无碎片实现简单运行高效只需复制少量存活对象无需处理死亡对象采用指针碰撞方式分配内存效率高。缺点内存利用率低任何时候都有一半内存处于闲置状态可用内存仅为总容量的一半标记-清除算法算法分为“标记”和“清除”阶段。首先在内存中标记存活的对象然后统一清除未标记的对象或者先标记需要回收的对象在统一清除标记的对象。优点不需要额外内存空间实现简单逻辑清晰缺点效率低标记和清除都需要遍历所有对象耗时较长内存碎片清除后产生大量不连续的内存碎片导致后续无法为大对象分配连续空间可能提前触发另一次GC标记-整理算法标记内存中存活的对象将所有存活对象向内存空间的一端移动使其紧密排列然后直接清理掉边界以外的所有内存。优点内存规整消除内存碎片为大对象分配连续空间提供保证分配简单采用指针碰撞方式分配内存效率高缺点效率低比标记-清除多了一个“移动/整理”对象的步骤需要更新所有引用关系停顿时间长整理阶段需要暂停所有用户线程Stop-The-World划分内存的方式指针碰撞堆内存被一块“分界指针”一分为二一侧是已使用的内存存活对象另一侧是空闲内存可用空间。分配新对象时只需将分界指针向空闲方向移动一段与对象大小相等的距离即可空闲列表JVM 维护一个列表记录哪些内存块是空闲的。分配对象时需要从列表中找到一个足够大的空闲块分配给对象并更新列表记录。垃圾收集器1、Serial收集器 -XX:UseSerialGC -XX:UseSerialOldGCSerial收集器是JVM中历史最悠久的垃圾收集器采用单线程工作使用复制算法工作模式垃圾收集时仅使用一个线程执行GC在此期间必须暂停所有用户线程Stop-The-WorldSTW直到垃圾收集结束.优点:简单高效单线程避免了多线程的同步开销。内存占用低不需要维护复杂的数据结构。单核CPU性能最优在单核或少量核心环境中无上下文切换开销吞吐量反而最高。缺点STW时间长随着堆内存增大停顿时间线性增长无法利用多核CPU在多核服务器上浪费硬件资源2、Serial Old收集器Serial Old收集器是Serial收集器的老年代版本同样采用单线程工作使用标记-整理算法。用途1、在JDK1.5以及以前的版本中与Parallel Scavenge收集器搭配使用2、是作为CMS收集器的后备方案虽然慢但能保证程序不因内存耗尽而崩溃。3、Parallel Scavenge收集器 -XX:UseParallelGC,-XX:UseParallelOldGCParallel 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模式下实现低延迟ParNewCMS是标配组合。当然JDK 9后CMS和ParNew都被废弃官方推荐用G1替代。Parallel Scavenge和ParNew有什么区别两者都是多线程新生代收集器都使用复制算法但关注点不同ParNew是为配合CMS设计的追求低停顿Parallel Scavenge追求高吞吐量不能与CMS搭配只能与Parallel Old组合。6、CMS收集器(-XX:UseConcMarkSweepGC)CMSConcurrent 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 GCSTW 单线程整理 → 超长停顿通过-XX:CMSInitiatingOccupancyFraction参数降低老年代使用率阈值如75%尽早触发CMS GC避免回收不及时.什么是Concurrent Mode FailureCMS并发回收时老年代垃圾产生速度超过回收速度导致老年代被填满此时CMS会降级为Serial Old进行单线程Full GC导致超长STW停顿。避免并发模式失败方法降低老年代使用率阈值更早触发FullGC增加GC频率。增大老年代空间(堆内存) 降低GC频率。增加ConcGCThreads 加快并发回收速度但会占用更多CPU。减少重新标记停顿间接帮助但会增加一次Minor G。启用碎片整理避免大对象晋升失败但会增加Full GC停顿。相关核心参数核心开关参数-XX:UseConcMarkSweepGC启用cms会自动启用ParNew作为新生代收集器-XX:ConcGCThreads设置并发GC线程数默认值取决于CPU核数通常为(ParallelGCThreads3)/4碎片整理参数3.-XX:UseCMSCompactAtFullCollectionFullGC之后做压缩整理减少内存碎片但会增加停顿时间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:CMSScavengeBeforeRemarkCMS重新标记前执行一次Minor GC降低标记阶段开销CMS 80%的GC耗时在标记阶段8. -XX:CMSParallellnitialMarkEnabled表示在初始标记的时候多线程执行缩短STW9. -XX:CMSParallelRemarkEnabled在重新标记的时候多线程执行缩短STW三色标记在并发标记期间用户线程还在运行对象引用可能随时变化会导致多标与漏标问题。**多标**原本应回收的对象被标记为存活浮动垃圾。**漏标**原本存活的对象未被标记。三色标记算法本身并不解决漏标问题它只是描述了对象的状态。解决漏标需要配合写屏障 增量更新CMS或SATBG1。三色标记算法在GC Roots可达性分析遍历对象过程中按照“是否已访问过”将对象标记为三种颜色黑色对象已被GC访问且所有引用都已扫描他是安全存活黑色对象不可能直接指向白色对象必须经过灰色。灰色对象已被GC访问但至少还有一个引用未扫描处于“正在处理”状态是扫描的中间节点。白色对象尚未被GC访问初始全是白色分析结束后仍为白色则是不可达并可以回收。多标-浮动垃圾**浮动垃圾**在并发标记并发清理过程中产生的本应回收但本轮GC未回收的内存对象。主要来源1、GC Root失效导致的浮动垃圾2、并发期间新产生的对象漏标-读写屏障漏标会导致被引用的对象被当成垃圾误删除这是严重bug必须解决解决方式1 增量更新Incremental Update黑色对象一旦新引用了白色对象它就变回灰色对象需要重新扫描灰色对象所有引用重新标记 STW 较长2原始快照Snapshot At The BeginningSATB灰色对象即使删除了对白色对象的引用GC 仍基于开始时的快照认为这个引用存在白色对象不会被误删。注意白色对象中可能包含本该回收的垃圾成为浮动垃圾留到下一轮 GC 再清理。以上无论是对引用关系记录的插入还是删除 虚拟机的记录操作都是通过写屏障实现的。写屏障定义JVM在对象引用赋值操作前后插入的一段额外代码用于维护GC所需的信息。写屏障实现SATBG1使用核心思想记录被删除的引用保证GC开始时所有存活对象在本轮都能被标记到。写屏障实现增量更新cms使用核心思想记录新增的引用保证并发标记期间新建立的引用关系不会导致漏标。SATB和增量更新有什么区别两者都是解决并发标记漏标问题的写屏障实现。SATBG1使用 记录灰色对象删除的引用以快照保证存活但会产生浮动垃圾增量更新CMS使用 记录黑色对象新增的引用让黑色对象变灰重新扫描无浮动垃圾但重新标记STW较长。SATB换来了更短的重新标记停顿代价是浮动垃圾。为什么G1用SATBCMS用增量更新记忆集与卡表为什么需要记忆集在新生代GCMinor GC做GC Roots可达性扫描时可能会碰到老年代对象引用新生代对象的情况跨代引用。如果为了找出这些引用而扫描整个老年代效率极低记录集Remember Set一种抽象数据结构用于记录从非收集区域指向收集区域的指针集合。避免全堆扫描只扫描有跨代引用的区域。**卡表Card Table**记忆集的一种具体实现将内存划分为固定大小的卡页Card Page用字节数组记录每个卡页是否存在跨代引用。标记哪些内存块包含跨代引用实现精细化管理什么是卡表卡表是HotSpot中实现记忆集的一种方式用一个字节数组记录跨代引用信息。每个字节对应512字节的卡页如果卡页内存在老年代指向新生代的引用则标记为“脏”。GC时只需扫描脏卡页中的对象避免扫描整个老年代。卡表通过写屏障维护——发生跨代引用赋值时写屏障将对应卡页标记为脏。所有涉及部分区域收集Partial GC的垃圾收集器如G1、ZGC都会面临相同的问题。6、G1收集器(-XX:UseG1GC)G1Garbage-First 是一款面向服务器的垃圾收集器主要针对配备多颗处理器及大容量内存的机器。以极高概率满足GC停顿时间要求可预测停顿用可预测的停顿通过设定MaxGCPauseMillis替代CMS的“尽可能短但不可控”的停顿。同时具备高吞吐量性能特征平衡低延迟与高吞吐。G1 内存布局核心特征1. Region 划分Java 堆被划分为多个大小相等的独立区域RegionJVM 最多支持 2048 个 RegionRegion 大小 堆大小 / 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 个 RegionEden 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 GC3、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 个 Region2、停顿控制与年轻代调优-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)将一次回收拆分为多次避免单次停顿过长。比如筛选回收阶段可以回收一会然后暂停回收恢复系统运行一会再开始回收这样可以让系统不至于单次停顿时间过长。