当前位置: 首页 > news >正文

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_END

JNI_ENTRY(void, jni_ReleasePrimitiveArrayCritical(JNIEnv *env, jarray array, void *carray, jint mode))
  JNIWrapper("ReleasePrimitiveArrayCritical");
  ...
  // The array, carray and mode arguments are ignored
  GCLocker::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: RUNNABLE
  at 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 停顿。

相关文章:

  • 【五.LangChain技术与应用】【31.LangChain ReAct Agent:反应式智能代理的实现】
  • 《会展管理:现场管理的实战经验分享》
  • 基于PyMuPDF与百度翻译的PDF翻译处理系统开发:中文乱码解决方案与自动化排版实践
  • Schematic Booster可以多模式打开原理图,兼容不同原理图设计图纸格式
  • 在 IntelliJ IDEA(2024) 中创建 JAR 包步骤
  • Cookie和Session
  • 电商行业门店管理软件架构设计与数据可视化实践
  • vue3,Element Plus中隐藏菜单el-menu滚动条
  • 实战指南:构建高可用生产级Kafka集群的完整教程
  • 关于OceanBase与CDH适配的经验分享
  • 【北京迅为】iTOP-RK3568OpenHarmony系统南向驱动开发GPIO基础知识
  • 深色系B端系统界面,在何种场景下更加适合?
  • 西门子1200:ModbusRTU-威纶通变频器
  • 量子布尔运算:AI与Python的量子世界探秘
  • 在MWC2025,读懂华为如何以行践言
  • 在Spring Boot项目中分层架构
  • 10-Agent循环分析新闻并输出总结报告
  • 《Python基础教程》第5章笔记:条件、循环及其他语句
  • AT89S51 单片机手册解读:架构、功能与应用深度剖析
  • 【GoTeams】-1:项目基础搭建
  • 赣州网站推广/廊坊百度seo公司
  • 网站开发是叫系统吗/今日头条最新版
  • php网站留言板模板/网络营销推广服务
  • 门户网站界面设计模板/重庆公司seo
  • 做网站开专票税钱是多少个点/代运营
  • 如何用ps做网站页面设计/seo数据监控平台