现象
JVM在触发GC如果发现有线程正处在JNI临界区的时候,会暂停GC动作,直到在离开JNI临界区的时候会再触发GC动作
源码:
JNI临界区(JNI critical regions)
JNI就是Java Native Interface,用于Java与C/C++语言的相互操作/调用,JNI定义了一些函数,用于获取或者释放指向Java对象的直接指针。这些方法必须成双成对使用,例如 先获取指向对象的指针,再释放指向对象的指针。在此类函数的代码被视为在临界区中运行,在此期间可使用的Java对象被称为临界对象。
会导致的问题
直观来看,会因为JNI临界区的问题导致GC停滞,影响了GC效率,也同样影响了吞吐量,严重时会导致不必要的内存不足的情况(GC停滞,新对象又持续被创建)。再往深层次看,如果持续观察GC收集情况,会发现GC是不稳定的,可能时不时的GC效率就低了,这种隐式的问题是更可怕的,因为大家一定是期望程序是稳定的,这表明程序是健康的,当他出现波动,但是又没规律时,最让开发人员和运维人员头疼。以至于有一些 Java 库和框架的维护者默认不使用JNI临界区,比如Netty完全不再使用JNI临界区。
优化方案
相信了解过G1的同学应该知道G1分成不同的region区域用来存储Java对象,通过移动存活的对象,回收没有存活对象的Region区域用来达到快速进行GC的目的。
当G1在进行Young GC的时候,如果发现没有空余的region区域用来移动存活的对象,那么就会将当前region区域固定下来,并标记为移动失败。等到当前其他region区域都移动完成后,也就是此次GC完成后,会将刚刚被标记为移动失败的region区域移动到老年代区域(标记,并非移动),以便下一次Young GC的时候有足够的空间用来移动对象。
此次优化便借鉴了固定region区域的特性来解决临界区的问题。当某个region区域增加了临界对象的时候,就会有一个计数器进行+1操作,当某个区域释放了某个临界对象后,就会在计数器进行-1操作,如果某个region区域的临界对象数为0时,就表明这个region区域是个普通的区域,可以正常进行GC回收操作。如果某个region区域的临界对象统计结果>0时,就意味着需要固定此region区域,然后Young GC的时候便不会移动此区域,会将此区域标记为老年代区域,当进行Full GC的时候依然不会移动被固定的region区域。
风险与假设
使用此方案的假设:我们假定使用JNI临界区的次数很少的,并且持续时间时较短的。
使用此方案可能会存在的风险:假如一个应用程序出现了很多个临界对象,且他们分布在不同的region区域,这可能会带来堆内存耗尽的风险,因为这些region区域都被固定了,不能进行回收。OpenJDK表示他们没有解决这个问题的方案,但是Shenandoah垃圾收集器在使用固定区域来解决临界区的问题时并没有遇到内存耗尽的问题,这也就表明了G1同样不会存在的这样的问题。(PS:官方也挺有意思...你就差直接说Shenandoah抄的G1了,哈哈哈,但是这句话貌似也表明了你这个优化方案是不是抄的Shenandoah的啊....)
链接
官方文档链接:https://openjdk.org/jeps/423
如果有同学对整个GC垃圾收集器有兴趣,可以看笔者的另外一篇文章:http://t.csdnimg.cn/2BBF2