从JVM内存模型到JVM问题排查(二)
垃圾收集算法
分代收集理论
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
标记复制算法
为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。可以联想到Survivor区的结构,当触发minorgc时将S0或者S1中存活的对象复制到另一块去。该算法目前使用在新生代的垃圾回收,因为需要空出一半的内存,因此不适合用在老年代的垃圾回收(G1、ZGC这些除外)。
标记清除算法
算法分为“标记”和“清除”阶段:标记存活的对象,统一回收所有未被标记的对象(一般选择这种);也可以反过来,标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
存在的问题
- 效率问题(如果需要标记的对象太多,效率不高
- 空间问题(标记清除后会产生大量不连续的碎片)
标记整理算法
根据老年代的特点特出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
垃圾收集器
Serial收集器
Serial(串行)收集器是最基本、历史最悠久的垃圾收集器了。大家看名字就知道这个收集器是一个单线程收集器了。它的“单线程”的意义不仅仅意味着它只会使用一条垃圾收集线程去完成垃圾收集工作,更重要的是它在进行垃圾收集工作的时候必须暂停其他所有的工作线程(STW),直到它收集结束。新生代采用复制算法,老年代采用标记-整理算法。
参数
1 | -XX:+UseSerialGC -XX:+UseSerialOldGC |
工作流程图
Parallel Scavenge收集器
Parallel收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为(控制参数、收集算法、回收策略等等)和Serial收集器类似。默认的收集线程数跟cpu核数相同,当然也可以用参数(-XX:ParallelGCThreads)指定收集线程数,但是一般不推荐修改。新生代采用复制算法,老年代采用标记-整理算法。
参数
1 | -XX:+UseParallelGC -XX:+UseParallelOldGC |
工作流程图
ParNew收集器
ParNew收集器跟Parallel收集器类似,唯一的区别就是它能和CMS收集器配合使用。
参数
1 | -XX:+UseParNewGC |
工作流程图
CMS收集器
CMS收集器更注重用户体验,旨在让STW的时间最短,因此它的工作流程是应用程序线程和GC线程基本上并行工作。CMS采用的算法是标记清除,并且是用在老年代垃圾回收的收集器。
参数
1 | -XX:+UseConcMarkSweepGC(old) |
工作流程图
- 初始标记:会STW,但是只标记GC Roots直接引用的对象,因此速度非常快。
- 并发标记:不会STW,并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程, 这个过程耗时较长但是不需要停顿用户线程, 可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。
- 重新标记:会STW,重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对
象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三
色标记里的增量更新算法做重新标记。 - 并发清理:不会STW,开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑
色不做任何处理。 - 并发重置:不会STW,重置本次GC过程中的标记数据。
注意事项
- 执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是”concurrentmode failure”,此时会进入stop the world,用serial old垃圾收集器来回收(STW并且使用单线程回收)。
- 使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生,当然通过参数
-XX:+UseCMSCompactAtFullCollection
可以让jvm在执行完标记清除后再做整理 - CMS垃圾收集器有可能会产生浮动垃圾,这些浮动垃圾只能等下一次GC回收
-XX:CMSInitiatingOccupancyFraction
: 当老年代使用达到该比例时会触发FullGC(默认是92,这是百分比),这么做的目的是由于整个过程会产生浮动垃圾,此时又要满足用户线程所需的空间,所以当空间到达92%就触发FullGC,避免发生”concurrentmode failure”,导致STW并且使用单线程回收垃圾。
核心参数图
解决并发标记的策略
写屏障 + 增量更新
G1
主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。
特性
G1将Java堆划分为多个大小相等的独立区域(Region),JVM最多可以有2048个Region。一般Region大小等于堆大小除以2048,比如堆大小为4096M,则Region大小为2M,当然也可以用参数”-XX:G1HeapRegionSize
“手动指定Region大小,但是推荐默认的计算方式。G1保留了年轻代和老年代的概念,但不再是物理隔阂了,它们都是(可以不连续)Region的集合。默认年轻代对堆内存的占比是5%,如果堆大小为4096M,那么年轻代占据200MB左右的内存,对应大概是100个Region,可以通过“-XX:G1NewSizePercent”设置新生代初始占比,在系统运行中,JVM会不停的给年轻代增加更多的Region,但是最多新生代的占比不会超过60%,可以通过“-XX:G1MaxNewSizePercent
”调整。年轻代中的Eden和Survivor对应的region也跟之前一样,默认8:1:1,假设年轻代现在有1000个region,eden区对应800个,s0对应100个,s1对应100个。一个Region可能之前是年轻代,如果Region进行了垃圾回收,之后可能又会变成老年代,也就是说Region的区域功能可能会动态变化。G1垃圾收集器对于对象什么时候会转移到老年代跟之前讲过的原则一样,唯一不同的是对大对象的处理,G1有专门分配大对象的Region叫Humongous区,而不是让大对象直接进入老年代的Region中。在G1中,大对象的判定规则就是一个大对象超过了一个Region大小的50%,比如按照上面算的,每个Region是2M,只要一个大对象超过了1M,就会被放入Humongous中,而且一个大对象如果太大,可能会横跨多个Region来存放。Humongous区专门存放短期巨型对象,不用直接进老年代,可以节约老年代的空间,避免因为老年代空间不够的GC开销。Full GC的时候除了收集年轻代和老年代之外,也会将Humongous区一并回收。
参数
1 | -XX:+UseG1GC |
工作流程图
- 初始标记:会STW,暂停所有的其他线程,并记录下gc roots直接能引用的对象,速度很快。
- 并发标记:不会STW,同CMS的并发标记。
- 最终标记:会STW,同CMS的重新标记。
- 筛选回收:会STW,筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间(可以用JVM参数
-XX:MaxGCPauseMillis
指定)来制定回收计划,比如说老年代此时有1000个Region都满了,但是因为根据预期停顿时间,本次垃圾回收可能只能停顿200毫秒,那么通过之前回收成本计算得知,可能回收其中800个Region刚好需要200ms,那么就只会回收800个Region(Collection Set,要回收的集合),尽量把GC导致的停顿时间控制在我们指定的范围内。这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。不管是年轻代或是老年代,回收算法主要用的是复制算法,将一个region中的存活对象复制到另一个region中,这种不会像CMS那样回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片。
G1垃圾收集分类
Young GC
YoungGC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数-XX:MaxGCPauseMills
设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做YoungGC,直到下一次Eden区放满,G1计算回收时间接近参数 -XX:MaxGCPauseMills
设定的值,那么就会触发Young GC。
Mixed GC
不是FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent
)设定的值则触发,回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC。
Full GC
停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的。(Shenandoah优化成多线程收集了)。
三色标记算法
把Gc Roots可达性分析遍历对象过程中遇到的对象, 按照“是否访问过”这个条件标记成以下三种颜色:
- 黑色:把Gcroots可达性分析遍历对象过程中遇到的对象, 按照“是否访问过”这个条件标记成以下三种颜色表示对象已经被垃圾收集器访问过, 且这个对象的所有引用都已经扫描过。 黑色的对象代表已经扫描过, 它是安全存活的, 如果有其他对象引用指向了黑色对象, 无须重新扫描一遍。 黑色对象不可能直接(不经过灰色对象) 指向某个白色对象。
- 灰色:表示对象已经被垃圾收集器访问过, 但这个对象上至少存在一个引用还没有被扫描过。
- 白色:表示对象尚未被垃圾收集器访问过。 显然在可达性分析刚刚开始的阶段, 所有的对象都是白色的, 若在分析结束的阶段, 仍然是白色的对象, 即代表不可达。
多标
在并发标记过程中,如果由于方法运行结束导致部分局部变量(gcroot)被销毁,这个gcroot引用的对象之前又被扫描过(被标记为非垃圾对象),那么本轮GC不会回收这部分内存。这部分本应该回收但是没有回收到的内存,被称之为“浮动垃圾”。浮动垃圾并不会影响垃圾回收的正确性,只是需要等到下一轮垃圾回收中才被清除。另外,针对并发标记(还有并发清理)开始后产生的新对象,通常的做法是直接全部当成黑色,本轮不会进行清除。这部分对象期间可能也会变为垃圾,这也算是浮动垃圾的一部分。
漏标
在标记过程中原本应该标记为黑色的对象,被遗漏导致对象被回收,这将会导致比较严重的BUG。
解决方法
增量更新(Incremental Update):当黑色对象插入新的指向白色对象的引用关系时, 就将这个新插入的引用记录下来, 等并发扫描结束之后,再将这些记录过的引用关系中的黑色对象为根,重新扫描一次。 这可以简化理解为,黑色对象一旦新插入了指向白色对象的引用之后,它就变回灰色对象了。重新扫描的时候将这些引用重新扫描一遍。
原始快照(Snapshot At The Beginning,SATB):当灰色对象要删除指向白色对象的引用关系时, 就将这个要删除的引用记录下来, 在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色(目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾)以上无论是对引用关系记录的插入还是删除, 虚拟机的记录操作都是通过写屏障实现的。重新扫码的时候将对象D直接标记为黑色,那么对象D就能在本轮GC中存活下来,但是对象D也有可能是浮动垃圾。