欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 房产 > 家装 > JVM常用概念之JNI临界区和GC锁定器

JVM常用概念之JNI临界区和GC锁定器

2025/3/10 12:18:07 来源:https://blog.csdn.net/nanxiaotao/article/details/146068698  浏览:    关键词:JVM常用概念之JNI临界区和GC锁定器

问题

JNI临界区是如何与 GC 配合的?什么是 GC锁定器?

基础知识

JNI有如下两个方法可以获取数组的内容:

void * GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy);

void ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carray, jint mode);

上述两个方法与JNI中提供用于访问和操作Java数组的两个函数Get/Release*ArrayElements非常相似,虚拟机可以直接返回指向原始数组的指针,或者将其进行复制,得到原始数组的副本,但是当虚拟机要求直接返回指向原始数组的指针时,这样提高性能,但是这种使用方式存在一些限制,是我们需要注意的,比如调用 GetPrimitiveArrayCritical 后,本地代码在调用 ReleasePrimitiveArrayCritical 之前不应运行较长时间。我们必须将这对函数内的代码视为在“关键区域”中运行。在关键区域内,本地代码不得调用其他 JNI 函数或任何可能导致当前线程阻塞并等待另一个 Java 线程的系统调用。(例如,当前线程不得对另一个 Java 线程正在写入的流调用 read。),这些限制使得本机代码更有可能获得数组的未复制版本,即使 VM 不支持钉选。例如,当本机代码持有通过 GetPrimitiveArrayCritical 获得的数组的指针时,VM 可能会暂时禁用垃圾收集。

VM 唯一需要维护的强不变量是“关键”获取的对象不会被移动。实现可以尝试不同的策略:

  • 在获取任何关键对象时完全禁用 GC 。这是迄今为止最简单的应对策略,因为它不会影响 GC 的其余部分。缺点是您必须无限期地阻止 GC(基本上听天由命,用户“释放”得足够快),这可能会带来问题。
  • 钉选对象,并在收集期间解决它的问题。如果收集器期望分配连续的空间,和/或期望收集处理整个堆子空间,那么这很难做到。例如,如果您在简单的分代 GC 中将对象钉选在年轻代中,那么您现在就无法“忽略”收集后年轻代中剩余的内容。您也无法从那里移动对象,因为它会破坏您想要强制执行的不变量。
  • 将包含对象的子空间钉选在堆中。同样,如果 GC 细化到整个代,那么这将毫无用处。但是,如果您有区域化的堆,那么您可以钉选单个区域,并避免仅针对该区域进行 GC,让每个人都满意。

实验

源码

Test Case

public class Case {static final int ITERS = Integer.getInteger("iters", 100);static final int ARR_SIZE = Integer.getInteger("arrSize", 10_000);static final int WINDOW = Integer.getInteger("window", 10_000_000);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("CriticalGC");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);}}}
}

Native Code

#include <jni.h>
#include <CriticalGC.h>static jbyte* sink;JNIEXPORT void JNICALL Java_CriticalGC_acquire
(JNIEnv* env, jclass klass, jintArray arr) {sink = (*env)->GetPrimitiveArrayCritical(env, arr, 0);
}JNIEXPORT void JNICALL Java_CriticalGC_release
(JNIEnv* env, jclass klass, jintArray arr) {(*env)->ReleasePrimitiveArrayCritical(env, arr, sink, 0);
}

测试

Parallel/CMS GC

$ make run-parallel
java -Djava.library.path=. -Xms4g -Xmx4g -verbose:gc -XX:+UseParallelGC Case
[0.745s][info][gc] Using Parallel
...
[29.098s][info][gc] GC(13) Pause Young (GCLocker Initiated GC) 1860M->1405M(3381M) 1651.290ms
Acquired
Releasing
[30.771s][info][gc] GC(14) Pause Young (GCLocker Initiated GC) 1863M->1408M(3381M) 1589.162ms
Acquired
Releasing
[32.567s][info][gc] GC(15) Pause Young (GCLocker Initiated GC) 1866M->1411M(3381M) 1710.092ms
Acquired
Releasing
...
1119.29user 3.71system 2:45.07elapsed 680%CPU (0avgtext+0avgdata 4782396maxresident)k
0inputs+224outputs (0major+1481912minor)pagefaults 0swaps

我们可从上述运行结果得知,GC 不会在“Acquired”和“Releasing”之间发生,这是向我们泄露的实现细节。但确凿的证据是“ GCLocker已启动 GC”消息。GCLocker 是一种锁,可在获取 JNI 关键状态时阻止 GC 运行。

JNI_ENTRY(void*, jni_GetPrimitiveArrayCritical(JNIEnv *env, jarray array, jboolean *isCopy))JNIWrapper("GetPrimitiveArrayCritical");GCLocker::lock_critical(thread);   // <--- acquire GCLocker!if (isCopy != NULL) {*isCopy = JNI_FALSE;}oop a = JNIHandles::resolve_non_null(array);...void* ret = arrayOop(a)->base(type);return ret;
JNI_ENDJNI_ENTRY(void, jni_ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carray, jint mode))JNIWrapper("ReleasePrimitiveArrayCritical");...// The array, carray and mode arguments are ignoredGCLocker::unlock_critical(thread); // <--- release GCLocker!...
JNI_END

如果尝试 GC,JVM 应该查看是否有人持有该锁。如果有,那么至少对于 Parallel、CMS 和 G1,我们无法继续 GC。当最后一个关键 JNI 操作以“release”结束时,VM 将检查是否有被 GCLocker 阻止的待处理 GC,如果有,则触发 GC 。这会产生“GCLocker 启动的 GC”收集。

G1 GC

$ make run-g1
java -Djava.library.path=. -Xms4g -Xmx4g -verbose:gc -XX:+UseG1GC Case
[0.012s][info][gc] Using G1
<HANGS>

在G1 GC运行的结果可知,JVM崩溃了,通过jstack查看相关的进程发现其状态为RUNNABLE。

"main" #1 prio=5 os_prio=0 tid=0x00007fdeb4013800 nid=0x4fd9 waiting on condition [0x00007fdebd5e0000]java.lang.Thread.State: RUNNABLEat CriticalGC.main(Case.java:22)

这种情况就需要通过fastdebug模式重新构建一个OpenJDK的版本,对问题进行定位及分析。

#
# A fatal error has been detected by the Java Runtime Environment:
#
#  Internal Error (/home/shade/trunks/jdk9-dev/hotspot/src/share/vm/gc/shared/gcLocker.cpp:96), pid=17842, tid=17843
#  assert(!JavaThread::current()->in_critical()) failed: Would deadlock
#
Native frames: (J=compiled Java code, A=aot compiled Java code, j=interpreted, Vv=VM code, C=native code)
V  [libjvm.so+0x15b5934]  VMError::report_and_die(...)+0x4c4
V  [libjvm.so+0x15b644f]  VMError::report_and_die(...)+0x2f
V  [libjvm.so+0xa2d262]  report_vm_error(...)+0x112
V  [libjvm.so+0xc51ac5]  GCLocker::stall_until_clear()+0xa5
V  [libjvm.so+0xb8b6ee]  G1CollectedHeap::attempt_allocation_slow(...)+0x92e
V  [libjvm.so+0xba423d]  G1CollectedHeap::attempt_allocation(...)+0x27d
V  [libjvm.so+0xb93cef]  G1CollectedHeap::allocate_new_tlab(...)+0x6f
V  [libjvm.so+0x94bdba]  CollectedHeap::allocate_from_tlab_slow(...)+0x1fa
V  [libjvm.so+0xd47cd7]  InstanceKlass::allocate_instance(Thread*)+0xc77
V  [libjvm.so+0x13cfef0]  OptoRuntime::new_instance_C(Klass*, JavaThread*)+0x830
v  ~RuntimeStub::_new_instance_Java
J 87% c2 CriticalGC.main([Ljava/lang/String;)V (82 bytes) ...
v  ~StubRoutines::call_stub
V  [libjvm.so+0xd99938]  JavaCalls::call_helper(...)+0x858
V  [libjvm.so+0xdbe7ab]  jni_invoke_static(...) ...
V  [libjvm.so+0xdde621]  jni_CallStaticVoidMethod+0x241
C  [libjli.so+0x463c]  JavaMain+0xa8c
C  [libpthread.so.0+0x76ba]  start_thread+0xca

仔细查看此堆栈跟踪,我们可以重现所发生的问题:我们尝试分配新对象,但没有TLAB来满足分配要求,因此我们跳转到 slowpath 分配以尝试获取新 TLAB。然后我们发现没有可用的 TLAB,尝试分配,但是失败了,并且发现我们需要等待 GCLocker 启动 GC。输入stall_until_clear等待此操作,但由于我们是持有 GCLocker 的线程,因此在此等待会导致死锁。

这符合规范,因为测试尝试在获取-释放块内分配内容。在 JNI 方法中不进行配对release是一个错误,这让我们暴露了这一点。如果我们没有离开,我们就无法在不调用 JNI 的情况下在acquire-release中进行分配,从而违反了“不得调用 JNI 函数”原则。

您可以调整收集器测试以避免以这种方式的失败,但随后您会发现 GCLocker 延迟收集意味着我们可以在堆中剩余空间太少时启动 GC,这将迫使我们进入完整 GC。而这种结果是我们最不愿看到的。

Shenandoah GC

区域化收集器可以锁定保存对象的特定区域,并让该对象不被收集,直到 JNI Critical 被释放。

$ make run-shenandoah
java -Djava.library.path=. -Xms4g -Xmx4g -verbose:gc -XX:+UseShenandoahGC Case
...
Releasing
Acquired
[3.325s][info][gc] GC(6) Pause Init Mark 0.287ms
[3.502s][info][gc] GC(6) Concurrent marking 3607M->3879M(4096M) 176.534ms
[3.503s][info][gc] GC(6) Pause Final Mark 3879M->1089M(4096M) 0.546ms
[3.503s][info][gc] GC(6) Concurrent evacuation  1089M->1095M(4096M) 0.390ms
[3.504s][info][gc] GC(6) Concurrent reset bitmaps 0.715ms
Releasing
Acquired
....
41.79user 0.86system 0:12.37elapsed 344%CPU (0avgtext+0avgdata 4314256maxresident)k
0inputs+1024outputs (0major+1085785minor)pagefaults 0swaps

由上述运行结果可知,尤其注意获取 JNI Critical 时 GC 周期是如何开始和结束的。Shenandoah 只是固定了保存数组的区域,然后继续收集其他区域,就像什么都没发生一样。它甚至可以对收集区域中的对象执行 JNI Critical,方法是先将其撤离,然后固定目标区域(显然不在收集集中)。这允许在没有 GCLocker 的情况下实现 JNI Critical,因此不会出现 GC 停顿。

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词