不同的垃圾收集器有不同的特性,适用于不同的场景,往常JVM的垃圾收集器是配合使用的,但是现代垃圾收集器是比较强大的、独立的。
年轻代和老年代的垃圾收集器并不可以随意搭配,如CMS就不能和ParallelScavenge搭配;
Serial/SerialOld
单线程串行收集器,简单高效
- Serial:复制算法收集年轻代;
- Serial Old:标记-整理收集老年代;
行为:
1、必须暂停其他所有的工作线程,直到它收集结束(Stop The World)
2、Client模式下,新生代的默认收集器。
在用户场景下,JVM管理的内存不会很大,Serial收集器在整理200M以内的内存,可以控制在100ms以内,可以接受。
ParallelNew/ParallelOld
多线程收集器
- ParNew:复制算法收集新生代;
- ParallelOld:标记-整理收集老年代;
默认开启的线程数量与CPU核数相同,可以使用
ParallelScavenge
针对年轻代的多线程收集器,使用复制算法;
目的:与ParallelNew关注点不同,尽可能缩短垃圾收集时用户线程的停顿时间;
吞吐量优先:CPU用于运行用户代码的时间占总时间的比值。
控制最大垃圾收集停顿时间:
直接设置吞吐量大小:
CMS
JDK9标记弃用,JDK14正式弃用; Concurrent Mark Sweep:并发标记—清除算法:并发收集,低停顿;
- CMS仅是老年代垃圾收集器;
- 年轻代会搭配:Parallel New收集器 / Serial收集器;
工作过程
1、初始标记:STW;仅仅标记 GC Roots 能直接关联到的对象,速度很快; 2、并发标记:进行 GC Roots Tracing 的过程,它在整个回收过程中耗时较长,不需要停顿;
- 进行[三色标记]
- 并发标记期间,新晋老年代对象、引用发生变化的对象,都会被标记为Dirty; 3、重新标记:STW;修正并发标记期间用户程序又产生的新的引用、新的对象(即上一步标记的Dirty对象);
- 只关注Dirty对象,继续使用三色标记进行标记
- 完成之后,清除Dirty标记; 4、并发清除:不需要停顿,耗时略长,直接清理所有的白色标记对象;
缺点
1、CMS垃圾收集器在垃圾收集过程中的CPU使用率高 2、会以抢占的方式执行GC线程,抢占用户线程资源;
- 吞吐量低:低停顿时间是以牺牲吞吐量为代价的,导致CPU利用率不够高;
- 在并发清除时,用户线程仍会产生垃圾,这些CMS无法处理;
- 收集算法导致:会产生内存碎片
G1
一些参考:
- 视频参考:G1动画
- RedHat-G1介绍:introduction-g1-garbage-collector
- 一个完整的根据日志跟踪G1垃圾回收的过程:collecting-and-reading-g1-garbage-collector-logs
内存模型
G1:Garbage First(JDK9默认收集器)
- Region间采用复制算法;
- 整体采用标记清除算法;
Region
在G1之前的垃圾回收算法中,每个区域的内存都是连续的,G1中每个区域物理内存不再连续,而是分块:
- 内存依然分区,但是会进一步分为:Region块;并对不同类型的块,进行逻辑上的分代标记:
- E:Eden
- S:Survivor(From/To)
- O:Old
- H:特殊Region,当单个对象大于Region的一半,则单独存储在Humongous
- Region物理上不连续;且Region也可以转变类型,如Eden变成Old;以此调整分区间比例大小
- Region都是2的整数次幂:1MB、2MB、4MB...(-XX:G1HeapRegionSize=1m指定)
- Region是动态分配的,如当Eden不够用时,分配一个,再将新对象放进去;或者老年代不够,动态分配一个老年代Region;
- 每个代的Region个数,也是动态的;当收集Eden时间不能达到预期时,下次Eden的总Region就会相应减少,以达到预期收集时间;
Card/Card Table(CT)
Card:每个Region被分割为一个个Card;
Card Table:一个字节数组,存储了Region中Card的内存地址;可以对每个Card进行索引;
- 每个Card:512Byte;
- 对象可以占用一个或多个Card;
- Card Table中存储Card的内存地址;可以进行随机查找;
Remember Set
每个Region都维护一个RSet,标记着当前的Region引用了其他哪些Region里的对象;本质是一个哈希表;
最终目的是为了在真正触发回收动作时,确定哪些Region是回收目标Region(处于CSet中的Region);
即通过RSet引用关系,确定CSet回收范围;
- 通过Card Table数组 + 哈希表进行跨Region的引用记录;
- Region中的每个对象,都以Card为单位存储,可以占用一个或多个;并由Card Table数组索引,可以做到随机访问;
- RSet可以通过哈希表,O(1)的复杂度获取引用对象所在位置;再通过Table Card,锁定对象所在Card;
因此GC Roots对象根据RSet + Card Table可以快速进行Region间的可达性算法分析;
RSet需要记录的引用类型
- 老年代引用新生代:需要记录到RSet;如图所示,新生代被引用的对象不应该被回收;
- 老年代引用老年代:需要记录到RSet;因为老年代回收时不一定全部扫描;(如图中B一定是老年代Region)
RSet不需要记录的引用类型
- Region内部对象间引用:不需要记录到RSet;
- 新生代Region间的对象引用,不需要记录到RSet,因为GC时会包含整个新生代,可达性分析中可以关联到所有新生代间的引用,不会导致存在引用的对象被回收;
- 新生代引用老年代:不需要记录到RSet,新生代对象被回收,不影响老年代;
写屏障
RSet在程序运行期间,通过写屏障,不断地进行更新;
每当用户线程分配新的对象到Region中,都会触发写屏障,会将新分配的对象所在Card,标记为Dirty Card;
Collection Set
CSet:记录了可被回收的Region的候选;可以来自所有分代的Region;
- 年轻代的Region会一直在CSet中,也就是任何GC都会对年轻代进行回收;
- 老年代对年轻代的引用,会通过记录在RSet中,再标记过程中,被添加到CSet中;
当GC结束,CSet中存活的数据,会被移动到别的可用分区;然后将这些Region清空;
G1如何控制停顿时间
1、Young GC:通过控制年轻代Region个数的分配,控制Young GC的停顿; 2、Mixed GC:通过优先列表,GC时优先选择收益高的Region;
收集过程
- G1没有严格的Full GC;通常是Young GC和Mixed GC(同时收集年轻代和老年代)
- 标记算法同样采用[三色标记]
G1并发标记(SATB算法)
并发标记是GC触发的前提,标记完成后,根据内存情况,触发Young GC或Mixed GC;
-
初始标记:STW(这阶段需要STW,但耗时很短;)
- 标记GC Roots能直接关联到的对象;
- 同时生成快照图,标记此时的存活对象,并通过一个指针(NextTams)锁定每个Region中内存的最大位置;
- 并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建新对象
-
并发标记:(与用户程序并发进行可达性分析)
- 期间的新对象,会在NextTams指针后进行分配,并且会直接标记为存活对象;(以此来解决三色标记中可能漏标的问题)
- 期间产生的新对象,同时会通过写屏障,写入本地队列中;
- 期间会进行RSet的更新;
- SATB算法可能误判,但是追求速度快;
-
最终标记:STW
- 并发标记期间因用户程序继续运作,可能产生新的引用、垃圾;在这个阶段进行RSet记录修复;
- 直到三色标记完成;
-
筛选回收:
- 首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是可控制的,而且停顿用户线程将大幅提高收集效率。
Young Collection
年轻代大小从5%到60%动态调整;
YoungGC触发:Eden区达到阈值,达到可以在
- 根据最大停顿时间(MaxGCPauseMillis),选择最佳回收策略;
- 根据GC Root可达算法分析和RSet的记录,对选中的Region进行标记;
- 将可达的对象复制到新的Region(不够年龄的对象进Survivor,达到年龄的对象,进入老年代);
- 将这些Region清空,完成垃圾回收;
1、GC Roots扫描,同时进行三色标记,标记可达的对象;(使用SATB算法与用户线程并发标记) 2、触发Evacuation Young GC:开启并发GC线程,进行复制 + 清除;
- 将Eden存活对象拷贝至Survivor区
- 将Survivor存活对象拷贝至另一个Survivor;
- 满足年龄要求,或超大对象,直接进入老年代;
- 清除CSet中的Region内存、RSet、Card Table等;
Mixed GC
通过SATB算法进行并发标记后,如果整个堆内存已使用的大小,达到了阈值45%(
G1根据设置的停顿时间
Full GC
G1的目标就是尽可能避免Full GC,但是仍然有可能触发Full GC;
通常触发Full GC的原因:Allocation Failure;通过日志可以查看具体原因和回收情况:
- Full GC会是一个单线程的STW;会停顿较长时间,进行完整的内存回收;
- 如果是几天触发以此FullGC,可能并无大碍;
- 如果是几个小时就触发,则可能存在问题;
收集特点
1、低停顿、没有内存碎片(Region间复制算法);
2、可预测停顿 因为Region的分区,G1可以进行部分区域的回收,可以缩小回收范围;
G1会跟踪每个Region的价值大小(回收获得的空间/回收需要的时间),维护一个优先列表,根据允许的停顿时间,指定回收计划,优先回收价值最大的Region,有限时间内获得更高的收集效率;
3、适用于大内存环境,堆内存6-8G以上;
- G1收集器为了收集算法的时间,用了很多空间换时间的操作;需要更多的内存;
- 如:Card Table、RSet等等;
G1参数配置
参数 | 作用 |
---|---|
-XX:+UseG1GC | 开启G1垃圾收集器 |
-XX:G1HeapRegionSize=1m | G1中Region大小,只会是:1,2,4,8...32(最大32M) |
-XX:G1NewSizePercent | G1初始时新生代占用总内存大小比例(5%) |
-XX:G1MaxNewSizePercent | G1会动态调控年轻代大小,最大不超过此比例(60%) |
-XX:MaxGCPauseMillis=200ms | GC触发的最大停顿时间(默认200ms,不建议改,不好确定合适的值) |
-XX:SurvivorRation=8 | Eden占新生代的8/10,剩余2/10,From/To 平分 |
-XX:InitiatingHeapOccupancyPercent | 触发 MixedGC 的内存阈值(默认老年代内存达到 45%触发) |
停顿时间能否设置过小
当最大停顿时间过小,容易造成,G1判定无法达成目标,就会不进行回收,直到内存无法正常工作,触发Full GC;
为什么需要大内存
1、G1收集器会对内存进行分区,分为多个Region,如果内存过小,Region的个数就会变少,G1的灵活度就会降低,会增加扫描、标记、收集的时间,最好是3072个Region以上(),Region 2MB以上;就最少需要6GB了;
2、G1收集器为了收集算法的时间,用了很多空间换时间的操作;需要更多的内存;如:Card Table、RSet都需要额外的空间;