GC垃圾回收器
G1收集器
Garbage First,是一款面向服务端应用的垃圾收集器。G1算法JDK1.9之后默认回收算法,特点是保持高回收率的同时,减少停顿。
特点:
1、并行于并发:G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让java程序继续执行。
2、分代收集:分代概念在G1中依然得以保留。虽然G1可以不需要其它收集器配合就能独立管理整个GC堆,但它能够采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。也就是说G1可以自己管理新生代和老年代了。
3、空间整合:由于G1使用了独立区域(Region)概念,G1从整体来看是基于“标记-整理”算法实现收集,从局部(两个Region)上来看是基于“复制”算法实现的,但无论如何,这两种算法都意味着G1运作期间不会产生内存空间碎片。
在最后筛选回收阶段,对每个Region里的回收对象价值(回收该区域的时间消耗和能得到的内存比值)最后排序,用户可以自定义停顿时间 ,那么g1就可以对部分region进行回收,这使得停顿时间是用户可以自己控制的。
但是每个region之间是存在相关引用关系的,这将导致minorGc时,会同时对老年代进行扫描,甚至全堆扫描,造成minorGc交率低下,时间变的很长。
4、可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用这明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒
如果不计算维护Remembered Set的操作,G1收集器的运作大致可划分为以下几个步骤:
1、初始标记(Initial Making)
2、并发标记(Concurrent Marking)
3、最终标记(Final Marking)
4、筛选回收(Live Data Counting and Evacuation)
看上去跟CMS收集器的运作过程有几分相似,不过确实也这样。初始阶段仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以用的Region中创建新对象,这个阶段需要停顿线程,但耗时很短。并发标记阶段是从GC Roots开始对堆中对象进行可达性分析,找出存活对象,这一阶段耗时较长但能与用户线程并发运行。而最终标记阶段需要吧Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但可并行执行。最后筛选回收阶段首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,这一过程同样是需要停顿线程的,但Sun公司透露这个阶段其实也可以做到并发,但考虑到停顿线程将大幅度提高收集效率,所以选择停顿。
在这里插入图片描述
其它收集器相比,G1变化较大的是它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留了新生代和老年代的概念,但新生代和老年代不再是物理隔离的了它们都是一部分Region(不需要连续)的集合。同时,为了避免全堆扫描,G1使用了Remembered Set来管理相关的对象引用信息。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏了。
ZGC收集器
Z Garbage Collector 垃圾收回器,也被称为 ZGC, 是一种可伸缩的低延迟垃圾收集器。Java 11包含一个全新的垃圾收集器--ZGC,它由Oracle开发。
目标
垃圾回收停顿时间不超过10ms
无论是相对小的堆(几百MB)还是大堆(TB级)都能应对自如
与G1相比,吞吐量下降不超过15%在这里插入图片描述
方便日后在此基础上实现新的gc特性、利用colored pointers(译者注:暂时翻译为彩色指针)和读屏障进一步优化收集器
在这里插入图片描述
ZGC描述
大体上来说,ZGC是一种并发的、不分代的、基于Region且支持NUMA的压缩收集器。因为只会在枚举根节点的阶段STW, 因此停顿时间不会随着堆大小或存活对象的多少而增加。
ZGC的一个核心设计就是读屏障与彩色指针(colored object pointers, 缩写, colored oops)组合起来使用总体来说是一种利用64位指针中未使用的bit来保存元数据的指针)。这是ZGC可以与用户线程并发执行的原因。从Java的线程角度来看,读取Java对应中的引用变量的操作属于一种读屏障。与单纯的取对象内存地址相比,使用读屏障可以利用彩色指针中包含的信息来决定在允许Java线程读取指针的地址值之前是否需要执行一些操作。例如,对象可能被垃圾收集器移动过了,这时读屏障就可以感知到这种情况并执行一些必要的行为。
跟其它的可选方案相比,我们认为使用彩色指针模式有一些非常吸引人的优势,比如:
– 这允许我们在移动对象/整理内存阶段,在指向可回收/重用区域的指针确定之前回收/重用这部分内存。(原文: It allows us to reclaim and reuse memory during the relocation/compaction phase, before pointers pointing into the reclaimed/reused regions have been fixed.)。这有利于降低堆的开销。这同时也意味着我们不需要再实现一个单独的标记-整理算法用于处理Full GC。
– 这允许我们使用相对来说更少量、更简单的GC屏障。这可以降低JVM运行时的性能开销。同时也可以让JVM字节码解释器和JIT编译器中的GC代码更加容易实现和优化。
– 我们目前会在彩色指针中保存与标记和重定位相关的数据。不过,只要彩色指针中还有足够的未使用的bit, 我们还可以在里面存储更多对读屏障有用的信息。我们认为这为未来实现更多的特性奠定了良好的基础。比如,在复杂多变的内存环境下,我们可以在彩色指针中存储一些追踪信息来让垃圾回收器在移动对象时能将低频次使用的对象移动到不常访问的内存区域。
RSet、Region
堆内存
在G1的垃圾回收算法中,堆内存采用了另外一种完全不同的方式进行组织,被划分为多个(默认2000多个)大小相同的内存块(Region),每个Region是逻辑连续的一段内存,在被使用时都充当一种角色,如下图:
Region
每个Region被标记了E、S、O和H,其中H是以往算法中没有的,它代表Humongous,表示这些Region存储的是巨型对象(humongous object,H-obj),当新建对象大小超过Region大小一半时,直接在新的一个或多个连续Region中分配,并标记为H。
Region的相关实现位于heapRegion.cpp类中,当堆内存初始化时,G1CollectorPolicy调用HeapRegion::setup_heap_region_size方法根据最小堆设置每个Region大小。
PHP
heap
Region的大小可以通过-XX:G1HeapRegionSize参数指定,如果没有显示设置,则根据如下逻辑计算出一个合理的大小。
Region的大小只能是1M、2M、4M、8M、16M或32M,比如-Xmx16g -Xms16g,G1就会采用16G / 2048 = 8M 的Region.
RSet
每个Region初始化时,会初始化一个remembered set(已记忆集合),这个翻译有点拗口,以下简称RSet,该集合用来记录并跟踪其它Region指向该Region中对象的引用,每个Region默认按照512Kb划分成多个Card,所以RSet需要记录的东西应该是 xx Region的 xx Card。
Region1和Region3中有对象引用了Region2的对象,则在Region2的Rset中记录了这些引用。
RSet实现过程
为了维护这些RSet,如果每次给引用类型的字段赋值都要更新RSet,这带来的额外开销实在太大,G1中采用post-write barrier和concurrent refinement threads实现了RSet的更新。
java层面给old对象的p字段赋值young对象之后,jvm底层会执行oop_store方法,实现位于oop.inline.hpp类中。
在赋值动作的前后,JVM插入一个pre-write barrier和post-write barrier,其中post-write barrier的最终动作如下:
1.找到该字段所在的位置(Card),并设置为dirty_card
2.如果当前是应用线程,每个Java线程有一个dirty card queue,把该card插入队列
3.除了每个线程自带的dirty card queue,还有一个全局共享的queue
赋值动作到此结束,接下来的RSet更新操作交由多个ConcurrentG1RefineThread并发完成,每当全局队列集合超过一定阈值后,ConcurrentG1RefineThread会取出若干个队列,遍历每个队列中记录的card,并进行处理,位于G1RemSet::refine_card方法,大概实现逻辑如下:
1、根据card的地址,计算出card所在的Region
2、如果Region不存在,或者Region是Young区,或者该Region在回收集合中,则不进行处理
3、最终使用闭合函数G1UpdateRSOrPushRefOopClosure::do_oop_nv()的处理该card中的对象
其中_from是持有引用的对象所在的Region,to是引用对象所在的Region,通过add_reference方法加入到RSet中,更细节的实现在OtherRegionsTable::add_reference方法中,有兴趣的同学可以继续研究,比如RSet的存储结构。
RSet有什么好处?
进行垃圾回收时,如果Region1有根对象A引用了Region2的对象B,显然对象B是活的,如果没有Rset,就需要扫描整个Region1或者其它Region,才能确定对象B是活跃的,有了Rset可以避免对整个堆进行扫描。
RSet有什么风险?
通过对RSet实现过程的研究,我们得知应用线程只负责把更新字段所在的Card插入到dirty card queue中,然后由后台线程refinement threads负责RSet的更新操作,如果应用线程插入速度过快,refinement threads来不及处理,那么应用线程将接管RSet更新的任务,这是必须要避免的。
refinement threads线程数量可以通过-XX:G1ConcRefinementThreads或-XX:ParallelGCThreads参数设置
JVM中的STW(Stop The World)
Stop一the一World,简称STW,指的是Gc事件发生过程中,会产生应用程序的停顿。停顿产生时整个应用程序线程都会被暂停,没有任何响应,有点像卡死的感觉,这个停顿称为STW。
举例:
可达性分析算法中枚举根节点(GC Roots)会导致所有Java执行线程停顿。
停顿的原因
分析工作必须在一个能确保一致性的快照中进行
一致性指整个分析期间整个执行系统看起来像被冻结在某个时间点上
如果出现分析过程中对象引用关系还在不断变化,则分析结果的准确性无法保证
示例代码:
被STW中断的应用程序线程会在完成GC之后恢复,频繁的中断会让用户感觉像是网速不快造成的电影卡顿一样,所以我们要减少STW的发生
STW事件和采用哪款GC无关,所有的GC都有这个事件。
哪怕是G1也不能完全避免Stop一the一world情况发生,只能说垃圾回收器越来越优秀,回收效率越来越高,尽可能地缩短了暂停时间。
STW是JVM在后台自动发起和自动完成的。在用户不可见的情况下,把用户正常的工作线程全部停掉。
开发中采用System.gc();会导致Stop一the一world的发生。
Java
public class StopTheWorldDemo {
public static class WorkThread extends Thread {
List<byte[]> list = new ArrayList<byte[]>();
public void run() {
try {
while (true) {
for(int i = 0;i < 1000;i++){
byte[] buffer = new byte[1024];
list.add(buffer);
}
if(list.size() > 10000){
list.clear();
System.gc();//会触发full gc,进而会出现STW事件
}
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
public static class PrintThread extends Thread {
public final long startTime = System.currentTimeMillis();
public void run() {
try {
while (true) {
// 每秒打印时间信息
long t = System.currentTimeMillis() - startTime;
System.out.println(t / 1000 + "." + t % 1000);
Thread.sleep(1000);
}
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
public static void main(String[] args) {
WorkThread w = new WorkThread();
PrintThread p = new PrintThread();
w.start();
p.start();
}
}
W线程当中的GC触发了STW,进而干扰了P线程有规律性打印。打印变得杂乱无章
打印输出:
JVM-三色标记算法
三色标记算法是一种垃圾回收的标记算法。它可以让JVM不发生或仅短时间发生STW(Stop The World),从而达到清除JVM内存垃圾的目的。JVM中的CMS、G1垃圾回收器 所使用垃圾回收算法即为三色标记法。
三色标记过程:
黑色:代表该对象以及该对象下的属性全部被标记过了。(程序需要用到的对象,不应该被回收)
灰色:对象被标记了,但是该对象下的属性未被完全标记。(需要在该对象中寻找垃圾)
白色:对象未被标记(需要被清除的垃圾)
三色标记存在的问题:
对象漏标:如果已经被C已经被标记为黑色了,因为是并发标记,此时可能会有线程在C中引用D。此时由于C已经被标记为黑色,不会再扫描D。D会被认为需要回收,此问题会导致系统出问题。
CMS 和 C1采用了两种方式来解决上面的问题
CMS解决漏标:
以获取最短回收停顿时间为目标的收集器。基于标记-清除算法实现。
运行过程:
在应对漏标问题时,CMS使用了增量更新的方法(Increment Update)
当未被标记的对象被重新引用后,引用它的对象如果是黑色的话,那么会将颜色置为灰色,在二次标记的时候让GC线程继续标记它的属性对象
G1解决漏标:
在应对漏标时,采用了SATB (snapshot at the beginning)
在标记开始的时候成成一个快照图标记存活对象
在一个引用断开后,要将此引用推到GC的堆栈中,保证对象还能被GC线程扫描到(通过在 wirte barrier 里把所有旧的引用所指向的对象都变成非白的)
配合Rset,去扫描哪些Region引用到当前的白色对象,若没有引用到当前对象,则回收
ZGC的染色指针
在64位系统中,如果没有被压缩的话,一个指向对象的指针(即地址值)是占64bit的,我们拿出4个bit,来记录一些信息.
如果这个指针原来指向了一个对象,在并发标记的过程之中,指向的对象有所改变,我们就用这4个bit来记录下这个变化
下一次重新扫描的时候,就扫描这些变化过的对象(因为是地址值,而且是约定的值,比如规定第一位0是已经变化过的,那么下次直接扫描第一位是0的就好了),只不过这样的话,zgc只能支持4tb的内存.但是如果将来前18位也可以被开发出来使用的话,这4个bit直接挪到前面,那么zgc的支持内存立刻就得以扩展.
ZGC收集器有一个标志性的设计是它采用的染色指针技术(Colored Pointer,其他类似的技术中可能将它称为Tag Pointer或者Version Pointer)。从前,如果我们要在对象上存储一些额外的、只供收集器或者虚拟机本身使用的数据,通常会在对象头中增加额外的存储字段(详见2.3.2节的内容),如对象的哈希码、分代年龄、锁记录等就是这样存储的。这种记录方式在有对象访问的场景下是很自然流畅的,不会有什么额外负担。但如果对象存在被移动过的可能性,即不能保证对象访问能够成功呢?又或者有一些根本就不会去访问对象,但又希望得知该对象的某些信息的应用场景呢?能不能从指针或者与对象内存无关的地方得到这些信息,譬如是否能够看出来对象被移动过?这样的要求并非不合理的刁难,例如对象标记的过程中需要给对象打上三色标记(见3.4.6节),这些标记本质上就只和对象的引用有关,而与对象本身无关——某个对象只有它的引用关系能决定它存活与否,对象上其他所有的属性都不能够影响它的存活判定结果。HotSpot虚拟机的几种收集器有不同的标记实现方案,有的把标记直接记录在对象头上(如Serial收集器),有的把标记记录在与对象相互独立的数据结构上(如G1、Shenandoah使用了一种相当于堆内存的1/64大小的,称为BitMap的结构来记录标记信息),而ZGC的染色指针是最直接的、最纯粹的,它直接把标记信息记在引用对象的指针上,这时,与其说可达性分析是遍历对象图来标记对象,还不如说是遍历“引用图”来标记“引用”了。
染色指针是一种直接将少量额外的信息存储在指针上的技术,可是为什么指针本身也可以存储额外信息呢?在64位系统中,理论可以访问的内存高达16EB(2的64次幂)字节[3]。实际上,基于需求(用不到那么多内存)、性能(地址越宽在做地址转换时需要的页表级数越多)和成本(消耗更多晶体管)的考虑,在AMD64架构[4]中只支持到52位(4PB)的地址总线和48位(256TB)的虚拟地址空间,所以目前64位的硬件实际能够支持的最大内存只有256TB。此外,操作系统一侧也还会施加自己
的约束,64位的Linux则分别支持47位(128TB)的进程虚拟地址空间和46位(64TB)的物理地址空间,64位的Windows系统甚至只支持44位(16TB)的物理地址空间。
G1的适合场景
1、50%以上的堆被存活对象占用:当大多数对象都存活的时候,说明老年代被占用的比例也会很大,这个时候就会触发full gc,full gc是很慢的,如果我们使用G1,那么G1就会触发mixed gc,而且mixed gc的GC最大停顿时间还是可控的。
2、对象分配和晋升的速度变化非常大:说明了对象往老年代挪动的频率很频繁,一样的,可以减少full gc的发生。
3、垃圾回收时间特别长,超过1秒:可以设置停顿时间,提升用户体验。
4、8GB以上的堆内存(建议值):内存如果在8G以下,收集的垃圾不是很多,而G1的算法相对于CMS较为复杂,还很有可能效率不如CMS,但是对于大内存,STW时间比较长,所以,在可控停顿时间这里,G1比较合适。
5、停顿时间是500ms以内:停顿时间可由用户控制。
ZGC目标
支持TB级别:根据官方文档来看,在Jdk11时ZGC可支持的最大内存为4TB,在jdk13可以支持16TB。
最大停顿时间不超过10ms:之所以能控制在10ms以下,是因为它的停顿时间主要跟Root扫描有关,而跟root数量和堆的大小没有关系。
奠定未来GC特性的基础。
最坏的情况下吞吐量会下降15%。
如何选择垃圾收集器
JDK 1.8默认使用 Parallel(年轻代和老年代都是)
JDK 1.9默认使用 G1
优先调整堆的大小让服务器自己来选择。
如果内存小于100M,使用串行收集器。
如果是单核,并且没有停顿时间的要求,串行或JVM自己选择。
如果允许停顿时间超过1秒,选择并行或者JVM自己选。
如果响应时间最重要,并且不能超过1秒,使用并发收集器。
4G以下可以用parallel,4-8G可以用ParNew+CMS,8G以上可以用G1,几百G以上用ZGC。