G1杂谈

在之前的文章中提到了四种垃圾回收器:SerialGC、ParallelGC、CMS和G1。对于这四种垃圾回收器垃圾在新生代和老年代在不足时触发的垃圾回收却有所不同。

  • SerialGC

 

新生代内存不足发生的垃圾回收:Minor GC。

老年代内存不足发生的垃圾回收:Full GC。

  • ParallelGC

 

新生代内存不足发生的垃圾回收:Minor GC。

老年代内存不足发生的垃圾回收:Full GC。

  • CMS

新生代内存不足发生的垃圾回收:Minor GC。

老年代内存不足是垃圾回收分为两种情况:回收速度高于新用户线程产生垃圾的速度时,也就是回收速度比新产生垃圾的速度快,来得及打扫,这个时候还是处于并发执行垃圾收集的阶段,虽然也有暂停,但是时间很短,还称不上FullGC。只有新产生垃圾速度大于回收的速度时,这个时候并发收集就失败了,然后会蜕化为一个串行收集,这个时候才叫FullGC。

  • CMS

新生代内存不足发生的垃圾回收:Minor GC。

老年代内存不足时和CMS的情况一样。

Young Collection跨代调用

新生代垃圾回收的过程:首先要找到根对象,然后对根对象采用可达性分析再找到存活对象,存活对象进行一个复制,复制到一个幸存区。这里存在一个问题,我们要找新生代的根对象,根对象有一部分是来自于老年代的,通常老年代存活的对象非常的多,如果我们要去遍历整个老年代,显示效率非常低,因此采用了一种卡表的技术,把老年代的区域再次进行细分,分成一个个Card,每个Card的大小大约是512K,如果老年代中有一个对象引用了新生代中的对象,那么对应的Card就标记为脏Card。这样的话将来就不用去找整个老年代,而是只需要去关心那些脏Card的区域,主要是为了提高检索效率。

上图中的粉红色区域都是脏Card,都引用了新生代Region中的对象。新生代这边会有一个Remembered Set记录从外部对我的引用也就是记录有哪些脏Card,当之后对新生代Region做垃圾回收的时候,就可以通过RememberedSet知道有哪些脏Card,然后再到脏Card中去找GC root,增加了效率。此时会有一个问题,我们需要去标记脏Card,是通过post-write-barrier去更新脏Card,这个过程是一个异步操作,更新的指令会放在一个dirty card queue中,将来会有一个线程来执行脏Card的更新操作。

Remark

 之前在CMS和G1这些并发垃圾回收器时都两个阶段,一个阶段叫并发标记阶段,另外一个阶段叫重新标记阶段。哪在重新标记阶段又经历了哪些?

上图,黑色的部分表示已经处理完成,而且表示有引用在引用他们,因此在结束时会被保留下来;灰色表示正在处理的过程中,而白色的则是未处理的部分。当然这个是一个中间状态,处理结束后,下面那个白色和灰色都会变成黑色,因为有对象引用,而上面的那个白色则没有对象引用,所以还是白色,所以在最后会被当成垃圾回收掉。这个过程都是发生在并发标记的过程。

可是在并发标记的过程中可能会出现,已经标记为白色的对象,又有新的对象再引用他,如果不进行操作,那么后续操作会直接将这个对象回收掉,后果就会很严重。所以此时需要在有新对象引用时,有一个pre_write_barrier的指令会执行,就会把这个对象加入到一个satb_mark_queue队列中,并且把这个对象变成灰色,等到并发标记结束时,进入重新标记,重新标记会让其他用户线程进行暂停,这个时候重新标记会把satb_mark_queue队列的对象一个一个的取出来再做一次检查,发现实灰色,会再一次进行处理,这个时候发现有对象在引用它,就会标记为黑色,避免被回收掉。

G1的优化

1.JDK 8U20版本中的字符串去重功能

先来看下面的一段代码:

String s1=new String("Hello"); // char[]{'H','e','l','l','0'}
String s2=new String("Hello");// char[]{'H','e','l','l','0'}

 在JDK8中字符串底层是使用了Char数组来存储每个字符,上面的一段代码执行后就会有两个char数组,显示如果有大量创建字符串的操作,那么内存就会被大量使用。为了解决这种方法,可以用之前用String的internal方法。也可以采用G1的一个优化。

在G1中会把所有的新分配的字符串分配到队列中,在新生代的垃圾回收时,会并发检查是否有字符串重复,如果他们的值一样,会让他们引用一个相同的Char数组,也就是上面的代码在经过垃圾回收后都就只有一个char数组。

开启这个功能使用命令:-XX:+UseStringDeduplication。它的优点是可以节省大量内存空间,缺点就是略微多占用CPU空间,新生代回收时间略微增加。

2.JDK 8U40并发标记类卸载

在之前的JDK8.0之前的版本中,类是没法卸载,就会一直占用内存,其实有时候很多自定义类加载器创建和加载类,使用一段时间后就没在使用,这个时候如果还占用内存,对垃圾回收就不利。在JDK8U40之后,所有对象经过并发标记之后,就能知道哪些类不再被使用,这个时候会尝试卸载的操作,只是卸载的条件有些苛刻:这些类的实例都被回收且类加载器的所有类都不再使用,这个时候会把它所加载的类卸载掉。只是针对于自定义类加载器。

通过参数:-XX:+ClassUnloadingWithConcurrentMark,默认为启用。

3.JDK 8u60 回收巨型对象

所谓巨型对象是一个对象大于一个Region的一半。G1对巨型对象不会进行拷贝,回收时会优先处理。G1会跟踪所有老年代的incoming引用,这样老年代的incoming引用为0的巨型对象就可以在新生代垃圾回收时处理掉。

4.JDK9中并发标记的起始时间的调整

开发标记必须在堆空间占满前完成,否则退化为FullGC。JDK9之前需要使用-XX:InitiatingHeapOccupyPercent。而在JDK9中可以动态的调整,进行数据采样并动态的调整。总会添加一个安全的空档文件。

 

 

猜你喜欢

转载自blog.csdn.net/qq_35363507/article/details/105103106
G1