Java教程:9种常见的CMS GC问题详解(十三)

开课吧开课吧锤锤2021-03-24 11:09

    Java编程语言是一种简单、面向对象、分布式、解释型、健壮安全、与系统无关、可移植、高性能、多线程和动态的语言。如今Java已经广泛应用于各个领域的编程开发。

Java

    4.9场景九:JNI引发的GC问题

    4.9.1现象

    在GC日志中,出现GCCause为GCLockerInitiatedGC。

2020-09-23T16:49:09.727+0800: 504426.742: [GC (GCLocker Initiated GC) 504426.742: [ParNew (promotion failed): 209716K->6042K(1887488K), 0.0843330 secs] 1449487K->1347626K(3984640K), 0.0848963 secs] [Times: user=0.19 sys=0.00, real=0.09 secs]
2020-09-23T16:49:09.812+0800: 504426.827: [Full GC (GCLocker Initiated GC) 504426.827: [CMS: 1341583K->419699K(2097152K), 1.8482275 secs] 1347626K->419699K(3984640K), [Metaspace: 297780K->297780K(1329152K)], 1.8490564 secs] [Times: user=1.62 sys=0.20, real=1.85 secs]

    4.9.2原因

    JNI(JavaNativeInterface)意为Java本地调用,它允许Java代码和其他语言写的Native代码进行交互。

    JNI如果需要获取JVM中的String或者数组,有两种方式:

    拷贝传递。

    共享引用(指针),性能更高。

    由于Native代码直接使用了JVM堆区的指针,如果这时发生GC,就会导致数据错误。因此,在发生此类JNI调用时,禁止GC的发生,同时阻止其他线程进入JNI临界区,直到最后一个线程退出临界区时触发一次GC。

    GCLocker实验:

public class GCLockerTest {

  static final int ITERS = 100;
  static final int ARR_SIZE =  10000;
  static final int WINDOW = 10000000;

  static native void acquire(int[] arr);
  static native void release(int[] arr);

  static final Object[] window = new Object[WINDOW];

  public static void main(String... args) throws Throwable {
    System.loadLibrary("GCLockerTest");
    int[] arr = new int[ARR_SIZE];

    for (int i = 0; i < ITERS; i++) {
      acquire(arr);
      System.out.println("Acquired");
      try {
        for (int c = 0; c < WINDOW; c++) {
          window[c] = new Object();
        }
      } catch (Throwable t) {
        // omit
      } finally {
        System.out.println("Releasing");
        release(arr);
      }
    }
  }
}
#include <jni.h>
#include "GCLockerTest.h"

static jbyte* sink;

JNIEXPORT void JNICALL Java_GCLockerTest_acquire(JNIEnv* env, jclass klass, jintArray arr) {
sink = (*env)->GetPrimitiveArrayCritical(env, arr, 0);
}

JNIEXPORT void JNICALL Java_GCLockerTest_release(JNIEnv* env, jclass klass, jintArray arr) {
(*env)->ReleasePrimitiveArrayCritical(env, arr, sink, 0);
}

    运行该JNI程序,可以看到发生的GC都是GCLockerInitiatedGC,并且注意在“Acquired”和“Released”时不可能发生GC。

Java

    GCLocker可能导致的不良后果有:

    如果此时是Young区不够AllocationFailure导致的GC,由于无法进行YoungGC,会将对象直接分配至Old区。

    如果Old区也没有空间了,则会等待锁释放,导致线程阻塞。

    可能触发额外不必要的YoungGC,JDK有一个Bug,有一定的几率,本来只该触发一次GCLockerInitiatedGC的YoungGC,实际发生了一次AllocationFailureGC又紧接着一次GCLockerInitiatedGC。是因为GCLockerInitiatedGC的属性被设为full,导致两次GC不能收敛。

    4.9.3策略

    添加-XX+PrintJNIGCStalls参数,可以打印出发生JNI调用时的线程,进一步分析,找到引发问题的JNI调用。

    JNI调用需要谨慎,不一定可以提升性能,反而可能造成GC问题。

    升级JDK版本到14,避免JDK-8048556导致的重复GC。

Java

    4.9.4小结

    JNI产生的GC问题较难排查,需要谨慎使用。

    以上内容由开课吧老师、新宇、湘铭、祥璞提供,更多Java教程尽在开课吧广场Java教程频道。

有用
分享