G1收集器(-XX:+UseG1GC)

G1(Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。(jdk9默认收集器)

image.png

1.G1将堆划分为多个大小相等的独立Region,最多2048个;
2.region大小=堆大小/2048。如:堆大小为2G,则每个region为2MB,可用参数(-XX:G1HeapRegionSize)调整(推荐使用默认值)
3.虽保留分代概念,但是逻辑分代,无物理隔阂,都是可以不连续的region集合;
4.年轻代占比5%(默认值),如:2G堆内存,年轻代则占比200MB,可通过参数(-XX:G1NewSizePercent)调整,不停的给年轻代增加更多的region,但年轻代最多占比不会超过60%,可通过(-XX:G1MaxNewSizePercent)调整;
5.年轻代中Eden和Suvivor默认值依然是8:1:1;
6.region区域的“角色”非固定,进行垃圾回收后,可能会从年轻代转变为老年代;
7.G1中的对象什么时候挪到老年代和以前原则一样,不同的是对大对象的处理
8.大对象主要分配在Humongous区域,判断规则:如果对象超过region大小50%,就会被放入Humongous。如果对象太大,比如4 5M,会被分配到连续的几个region;
9.Humongous专门存放短暂且巨型对象,不用直接进入老年代(为了节约老年代空间),避免老年代空间不够GC开销;
10.Full GC会回收年轻代、老年代、Humongous中垃圾对象;

注:一开始是没有老年代区域,是边用边改变region区的“角色”,每做一次GCregion“角色”会发生变化,不会有固定的“角色”region。通俗叫就是逻辑分代,没有物理隔阂。
image.png
G1收集器一次GC的运作过程大致分为以下几个步骤:
初始标记(initial mark,STW):暂停所有的其他线程,并记录下gc roots直接能引用的对象,速度很快
并发标记(Concurrent Marking):同CMS的并发标记
最终标记(Remark,STW):同CMS的重新标记
筛选回收(Cleanup,STW):筛选回收阶段首先对各个Region的回收价值和成本进行排序根据用户所期望的GC停顿时间(可以用JVM参数 -XX:MaxGCPauseMillis指定)来制定回收计划,比如说老年代此时有1000个Region都满了,但是因为根据预期停顿时间,本次垃圾回收可能只能停顿200毫秒,那么通过之前回收成本计算得知,可能回收其中800个Region刚好需要200ms,那么就只会回收800个Region(Collection Set,要回收的集合),尽量把GC导致的停顿时间控制在我们指定的范围内。这个阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅提高收集效率。不管是年轻代或是老年代,回收算法主要用的是复制算法将一个region中的存活对象复制到另一个region中,这种不会像CMS那样回收完因为有很多内存碎片还需要整理一次,G1采用复制算法回收几乎不会有太多内存碎片。(注意:CMS回收阶段是跟用户线程一起并发执行的,G1因为内部实现太复杂暂时没实现并发回收,不过到了Shenandoah就实现了并发收集,Shenandoah可以看成是G1的升级版本)

G1垃圾收集分类

YoungGC

YoungGC并不是说现有的Eden区放满了就会马上触发,G1会计算下现在Eden区回收大概要多久时间,如果回收时间远远小于参数 -XX:MaxGCPauseMills 设定的值,那么增加年轻代的region,继续给新对象存放,不会马上做Young GC,直到下一次Eden区放满,G1计算回收时间接近参数 -XX:MaxGCPauseMills 设定的值,那么就会触发Young GC

MixedGC

不是FullGC,老年代的堆占有率达到参数(-XX:InitiatingHeapOccupancyPercent)设定的值则触发,回收所有的Young和部分Old(根据期望的GC停顿时间确定old区垃圾收集的优先顺序)以及大对象区,正常情况G1的垃圾收集是先做MixedGC,主要使用复制算法,需要把各个region中存活的对象拷贝到别的region里去,拷贝过程中如果发现没有足够的空region能够承载拷贝对象就会触发一次Full GC

Full GC

停止系统程序,然后采用单线程进行标记、清理和压缩整理,好空闲出来一批Region来供下一次MixedGC使用,这个过程是非常耗时的。(Shenandoah优化成多线程收集了)

G1垃圾收集器优化建议

假设参数 -XX:MaxGCPauseMills 设置的值很大,导致系统运行很久,年轻代可能都占用了堆内存的60%了,此时才触发年轻代gc。那么存活下来的对象可能就会很多,此时就会导致Survivor区域放不下那么多的对象,就会进入老年代中。或者是你年轻代gc过后,存活下来的对象过多,导致进入Survivor区域后触发了动态年龄判定规则,达到了Survivor区域的50%,也会快速导致一些对象进入老年代中。所以这里核心还是在于调节 -XX:MaxGCPauseMills 这个参数的值,在保证他的年轻代gc别太频繁的同时,还得考虑每次gc过后的存活对象有多少,避免存活对象太多快速进入老年代,频繁触发mixed gc。

什么场景适合使用G1

  1. 50%以上的堆被存活对象占用
    2. 对象分配和晋升的速度变化非常大
    3. 垃圾回收时间特别长,超过1秒
    4. 8GB以上的堆内存(建议值)
    5. 停顿时间是500ms以内

    ZGC收集器(-XX:+UseZGC)

    ZGC是一款JDK 11中新加入的具有实验性质的低延迟垃圾收集器,ZGC可以说源自于是Azul System公司开发的C4(Concurrent Continuously Compacting Collector)收集器。

    不分代(暂时)

    单代,即ZGC「没有分代」。我们知道以前的垃圾回收器之所以分代,是因为源于“「大部分对象朝生夕死」”的假设,事实上大部分系统的对象分配行为也确实符合这个假设。
    那么为什么ZGC就不分代呢?因为分代实现起来麻烦,作者就先实现出一个比较简单可用的单代版本,后续会优化。

    ZGC内存布局

    ZGC收集器是一款基于Region内存布局的, 暂时不设分代的, 使用了读屏障、 颜色指针等技术来实现可并发的标记-整理算法的, 以低延迟为首要目标的一款垃圾收集器。
    ZGC的Region可以具有如图所示的大、 中、 小三类容量:
    小型Region(Small Region) : 容量固定为2MB, 用于放置小于256KB的小对象。
    中型Region(Medium Region) : 容量固定为32MB, 用于放置大于等于256KB但小于4MB的对象。
    大型Region(Large Region) : 容量不固定, 可以动态变化, 但必须为2MB的整数倍, 用于放置4MB或以上的大对象。
    每个大型Region中只会存放一个大对象,这也预示着虽然名字叫作“大型Region”,但它的实际容量完全有可能小于中型Region,最小容量可低至4MB。 大型Region在ZGC的实现中是不会被重分配(重分配是ZGC的一种处理动作,用于复制对象的收集器阶段,稍后会介绍到)的,因为复制一个大对象的代价非常高昂。
    image.png

    ZGC运作过程

    ZGC的运作过程大致可划分为以下四个大的阶段:
    image.png
    并发标记(Concurrent Mark):与G1一样,并发标记是遍历对象图做可达性分析的阶段,它的初始标记 (Mark Start)和最终标记(Mark End)也会出现短暂的停顿,与G1不同的是,ZGC的标记是在指针上而不是在对象上进行的,标记阶段会更新染色指针中的Marked 0、Marked 1标志位。
    并发预备重分配(Concurrent Prepare for Relocate):这个阶段需要根据特定的查询条件统计得出本次收集过程要清理哪些Region,将这些Region组成重分配集(Relocation Set)。ZGC每次回收都会扫描所有的Region,用范围更大的扫描成本换取省去G1中记忆集的维护成本。
    并发重分配(Concurrent Relocate):重分配是ZGC执行过程中的核心阶段,这个过程要把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表(Forward Table),记录从旧对象到新对象的转向关系。ZGC收集器能仅从引用上就明确得知一个对象是否处于重分配集之中,如果用户线程此时并发访问了位于重分配集中的对象,这次访问将会被预置的内存屏障(读屏障)所截获,然后立即根据Region上的转发表记录将访问转发到新复制的对象上,并同时修正更新该引用的值,使其直接指向新对象,ZGC将这种行为称为指针的“自愈”(Self-Healing)能力。
    1.ZGC的颜色指针因为“自愈”(Self‐Healing)能力,所以只有第一次访问旧对象会变慢, 一旦重分配集中某个Region的存活对象都复制完毕后,
    2. 这个Region就可以立即释放用于新对象的分配,但是转发表还得留着不能释放掉, 因为可能还有访问在使用这个转发表。
    并发重映射(Concurrent Remap):重映射所做的就是修正整个堆中指向重分配集中旧对象的所有引用,但是ZGC中对象引用存在“自愈”功能,所以这个重映射操作并不是很迫切。ZGC很巧妙地把并发重映射阶段要做的工作,合并到了下一次垃圾收集循环中的并发标记阶段里去完成,反正它们都是要遍历所有对象的,这样合并就节省了一次遍历对象图的开销。一旦所有指针都被修正之后, 原来记录新旧对象关系的转发表就可以释放掉了。

    如何选择垃圾收集器

  2. 优先调整堆的大小让服务器自己来选择
    2. 如果内存小于100M,使用串行收集器
    3. 如果是单核,并且没有停顿时间的要求,串行或JVM自己选择
    4. 如果允许停顿时间超过1秒,选择并行或者JVM自己选
    5. 如果响应时间最重要,并且不能超过1秒,使用并发收集器
    6. 4G以下可以用parallel,4-8G可以用ParNew+CMS,8G以上可以用G1,几百G以上用ZGC

    问题一:G1相对于CMS以及之前的一些垃圾收集器来说,为什么它对大内存的处理能力更强?

    大内存的堆一次回收耗费的时间可能很长,导致STW时间也会很长,G1提供可预测的停顿功能,这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段(通过参数”-XX:MaxGCPauseMillis”指定)内完成垃圾收集,这样可以在一定程度上控制一次回收的STW时间。

    问题二:什么场景适合使用G1垃圾收集器

  3. 50%以上的堆被存活对象占用
    2. 对象分配和晋升的速度变化非常大
    3. 垃圾回收时间特别长,超过1秒
    4. 8GB以上的堆内存(建议值)
    5. 停顿时间是500ms以内

    问题三:方法局部变量比如 int a=xxx,到底存在什么位置?

    int类型,占4字节
    int a=2,此时的字节码如下图:
    image.png
    iconst_2:将-1到5的常量压入栈中; (2是-1到5的具体数值)

int a=12,此时的字节码如下图:
image.png
bipush:将单字节(byte)的常量值(-128-127)压入栈中;(1个字节)
int a=128,此时的字节码如下图:
image.png
sipush:将一个短整型(short)常量值(-32768-32767)压入栈中;(2个字节)
int a=32768,此时的字节码如下图:
image.png
ldc:将整形(int)常量值(-2147483648-2147483647)压入栈中;(4个字节)

注意:ldc指令是从常量池中获取值的,也就是说在这段范围(-2147483648~2147483647)内的int值是存储在常量池中的。即2个字节以下存在栈中,2个字节以上存在常量池中