理解JNI参数的安全访问

7

我正在研究HotSpot在JNI代码运行时如何执行垃圾收集和/或堆压缩。

似乎普遍认为在Java中对象随时可能被移动。我试图明确地了解JNI是否受到垃圾收集的影响。存在一些JNI函数可以显式地防止垃圾收集,例如GetPrimitiveArrayCritical。如果引用确实是不稳定的,那么这样的功能是有意义的。但是,如果它们不是,则没有任何意义。

关于这个问题似乎存在大量矛盾的信息,我正在努力梳理。

JNI代码在安全点运行,并且可以继续运行,除非它回调Java或调用某些特定的JVM方法,在这种情况下,它可能会停止以防止离开安全点(感谢Nitsan的评论)。

JVM使用什么机制在停顿期间阻塞线程

上述内容让我想到垃圾收集将与JNI代码并发运行。这肯定不安全,对吗?

为了实现本地引用,Java虚拟机会为每个从Java到本地方法的控制转移创建一个注册表。该注册表将不可移动的本地引用映射到Java对象,并防止这些对象被垃圾回收。所有传递给本地方法的Java对象(包括作为JNI函数调用结果返回的对象)都会自动添加到注册表中。在本地方法返回后,注册表将被删除,允许其所有条目被垃圾回收。更多信息请参考https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/design.html#wp16789
好的,所以这里的“local”引用是不可移动的,但这并没有涉及到压缩的问题。
JVM必须确保从Java™传递给本地方法的对象以及本地代码创建的任何新对象都可以被GC访问。为了处理GC要求,JVM分配了一个称为“本地引用根集”的专用存储区域。
当以下情况发生时,将创建本地引用根集: 1.线程首次附加到JVM(线程的“最外层”根集)。 2.每次J2N转换发生。
JVM使用以下内容初始化为J2N转换创建的根集: 1.调用者的对象或类的本地引用。 2.传递给本地方法的每个对象的本地引用。
除非您使用PushLocalFrame JNI函数创建新的“本地帧”,否则在本地代码中创建的新本地引用将添加到此J2N根集中。 http://www.ibm.com/support/knowledgecenter/en/SSYKE2_5.0.0/com.ibm.java.doc.diagnostics.50/diag/understanding/jni_transitions_j2n.html 好的,所以IBM将传递的对象存储在本地引用根集中,但它没有讨论内存压缩。这只是说明对象不会被垃圾回收。
GC可能随时决定需要压缩垃圾收集堆。压缩涉及将对象从一个地址物理移动到另一个地址。这些对象可能被JNI本地或全局引用所引用。为了安全地允许压缩,JNI引用不是指向堆的直接指针。至少有一级间接隔离原生代码与对象运动。如果本地方法需要获得对象内部的直接可寻址性,则情况就更加复杂。在需要快速、共享访问大型原始数组的情况下,通常需要直接寻址或固定堆,例如屏幕缓冲区。在这些情况下,可以使用JNI临界区,该区域对程序员施加了额外的要求,如在这些函数的JNI描述中所指定的。有关详细信息,请参阅JNI规范。 GetPrimitiveArrayCritical返回Java数组的直接堆地址,禁用垃圾收集,直到调用相应的ReleasePrimitiveArrayCritical为止。 GetStringCritical返回java.lang.String实例的直接堆地址,禁用垃圾收集,直到调用ReleaseStringCritical为止。 http://www.ibm.com/support/knowledgecenter/SSYKE2_6.0.0/com.ibm.java.doc.diagnostics.60/diag/understanding/jni_copypin.html

好的,所以IBM基本上说JNI传递的对象可能随时被移动!那HotSpot呢?

GetArrayElements函数族被记录为要么复制数组,要么将它们固定在原地(这样做可以防止紧凑垃圾收集器移动它们)。它被记录为比GetPrimitiveArrayCritical更安全、更自由。然而,我想知道哪些VM和/或垃圾收集器(如果有)实际上是固定数组而不是复制它们。

哪些VM或GC支持JNI固定?

Aleksandr认为访问传递对象的内存的唯一安全方式是通过Get<PrimitiveType>ArrayElementsGetPrimitiveArrayCritical

Trent的回答并不令人兴奋。

这封OpenJDK邮件似乎表明ConcurrentMarkAndSweep GC是非移动的。至少在当前的JVM中(我还没有检查这个问题有多远被后移),由于CMS GC是非移动的,所以它不受JNI关键部分的影响(除了在并发模式失败时可能会出现非停止世界压缩 - 在这种情况下,分配线程必须停顿,直到关键部分被清除 - 这后一种停顿很可能比您更频繁地看到老年代病理学中的慢路径直接分配)。请注意,老年代的直接分配本身不仅速度较慢(第一级性能影响),而且可能导致更多的继承(因为所谓的裙带关系),以及更慢的后续扫描需要扫描更多的脏卡片(后两者都是第二级效应)。http://mail.openjdk.java.net/pipermail/hotspot-runtime-dev/2007-December/000074.html

https://www.infoq.com/articles/G1-One-Garbage-Collector-To-Rule-Them-All

这篇关于G1的帖子提到它确实会压缩堆,但没有具体提到数据移动。
由于IBM文档暗示对象随时可能被压缩,我们需要弄清楚为什么JNI HotSpot函数实际上是安全的。因为如果内存压缩确实发生在JNI代码运行时,它们必须需要移动到一个安全状态以防止并发内存效果。
现在,我一直在尽力跟踪HotSpot代码。让我们看看GetByteArrayElements。该方法似乎必须确保在复制元素之前指针是正确的。让我们试着找出如何做到这一点。
下面是GetByteArrayElements的宏。
#ifndef USDT2
#define DEFINE_GETSCALARARRAYELEMENTS(ElementTag,ElementType,Result, Tag) 
JNI_QUICK_ENTRY(ElementType*,
          jni_Get##Result##ArrayElements(JNIEnv *env, ElementType##Array array, jboolean *isCopy))
  JNIWrapper("Get" XSTR(Result) "ArrayElements");
  DTRACE_PROBE3(hotspot_jni, Get##Result##ArrayElements__entry, env, array, isCopy);
  /* allocate an chunk of memory in c land */
  typeArrayOop a = typeArrayOop(JNIHandles::resolve_non_null(array));
  ElementType* result;
  int len = a->length();
  if (len == 0) {
    result = (ElementType*)get_bad_address();
  } else {
    result = NEW_C_HEAP_ARRAY_RETURN_NULL(ElementType, len, mtInternal);
    if (result != NULL) {                                    
          memcpy(result, a->Tag##_at_addr(0), sizeof(ElementType)*len);
      if (isCopy) {
        *isCopy = JNI_TRUE;
      }
    }  
  }
  DTRACE_PROBE1(hotspot_jni, Get##Result##ArrayElements__return, result);
  return result;
JNI_END

这里是JNI_QUICK_ENTRY的宏。
#define JNI_QUICK_ENTRY(result_type, header)                         \
extern "C" {                                                         \
  result_type JNICALL header {                                \
    JavaThread* thread=JavaThread::thread_from_jni_environment(env); \
    assert( !VerifyJNIEnvThread || (thread == Thread::current()), "JNIEnv is only valid in same thread"); \
    ThreadInVMfromNative __tiv(thread);                              \
    debug_only(VMNativeEntryWrapper __vew;)                          \
VM_QUICK_ENTRY_BASE(result_type, header, thread)

我已经跟踪了这里的每个函数,但仍然没有看到任何互斥锁或内存同步器。唯一我无法理解的函数是__tiv,似乎在我能找到的任何地方都没有定义。
  • 有人能解释一下为什么JNI接口方法(如GetByteArrayElements)是安全的吗?
  • 顺便问一下,在JNI_QUICK_ENTRY退出时,有人能找到JNI调用从VM转换回Native的位置吗?

“由于IBM文档暗示对象可能随时被压缩,我们需要弄清楚为什么JNI HotSpot函数实际上是安全的。” — 您知道IBM的JVM和HotSpot是两种完全不同的实现吗? - Holger
是的,它们是相似的。但这并不意味着不存在相似之处。有关IBM VM的信息是为了提供额外的上下文。 - Johnny V
你需要了解的一切都包含在JNI规范“引用Java对象”部分中。这定义了你必须做什么以及JVM必须做什么。所有实现都必须遵守。不要担心Sun、Oracle、IBM或其他任何供应商可能会说什么,也不要从StackOverflow答案或其他第三方资源中挑选混淆自己。 - user207421
@user207421 我正在寻求更深入的理解,因为出于性能原因,我想要做一些非常不安全和不可移植的事情。 - Johnny V
2个回答

5

HotSpot JVM 中 JNI 方法的工作原理

  1. 本地方法可以与VM操作并发运行,包括GC。它们不会在安全点停止

  2. 即使从正在运行的本地方法引用Java对象,GC也可能移动这些对象。 jobject句柄不是指向堆中原始地址,而是一种间接级别:将其视为指针,指向非可移动对象引用数组中的一个位置。每当对象被移动时,相应的数组槽位会被更新,但指向此槽位的指针仍然保持不变。也就是说,jobject句柄仍然有效。每次本地方法调用JNI函数时,都会检查JVM是否处于安全点状态。如果是(例如GC正在运行),JNI函数将阻塞,直到安全点操作完成。

  3. 在执行像GetByteArrayElements这样的JNI函数期间,相应的线程被标记为_thread_in_vm状态。只要处于此状态下有运行中的线程,就无法到达安全点。例如,在执行GetByteArrayElements期间请求GC,GC将延迟直到JNI函数返回。

  4. 线程状态转换的魔术是由你注意到的这行代码执行的:
    ThreadInVMfromNative __tiv(thread)。这里的__tiv只是类的一个实例。它的唯一目的是自动调用ThreadInVMfromNative构造函数和析构函数。

    ThreadInVMfromNative构造函数调用transition_from_native,该函数检查是否处于安全点,并在需要时暂停当前线程。~ThreadInVMfromNative析构函数将切换回_thread_in_native状态。

  5. GetPrimitiveArrayCriticalGetStringCritical是唯一提供指向Java堆的原始指针的JNI函数。它们防止GC启动,直到调用相应的Release函数为止。

调用JNI函数时线程状态的转换

  1. 状态 = _thread_in_native;
    本地方法可以与GC并发运行。

  2. 调用JNI函数。

  3. 状态 = _thread_in_native_trans;
    此时无法启动GC。

  4. 如果VM操作正在进行中,则阻塞直到其完成。

  5. 状态 = _thread_in_vm;
    可以安全地访问堆。


这也引出了JNI调用为什么昂贵的问题;当JNI返回时释放in_vm锁并重新获取它是昂贵的。如果我们能编写被视为in_vm的JNI代码,那么它将会更快。 - Johnny V
为什么在VM状态下无法到达安全点?在该状态下是否执行了某些特殊操作,可能会影响GC操作? - choxsword
1
@choxsword 在 _thread_in_vm 期间,JVM 可以使用原始指针访问堆。 - apangin

2
似乎普遍认为对象随时可以移动,但这不是知识,也不是真实的。传递给JNI方法或由其持有的对象在方法返回之前、对象被显式释放之前或包含它的LocalFrame弹出之前都无法移动。 如果这是真的,那么每个JNI接口方法都必须需要锁定或某种内存同步吗?不需要,见上文。

这份文档 https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/design.html#wp16789 确实是这么说的,但是有很多混乱的信号。也许我搜索得太多了。 - Johnny V
1
如果某人的话与规范相矛盾,您不需要担心。GetXXXCritical() '使本地代码更有可能获得未复制的数组版本'。这与“传递参数中引用的对象”无关。 - user207421
3
"JNI方法传递或持有的对象在方法返回之前无法移动" - 不正确。HotSpot JVM可以移动从运行中的JNI方法引用的Java对象。然而,jobject句柄仍然有效,因为它们不是对堆的直接引用。 - apangin
1
“我在这里做的只是引用文档”——一个引用不应该被标记并附上源链接吗? - Holger
2
@EJP:你的回答中没有包含任何链接,但却有一个强烈的说法:“传递给JNI方法或由其持有的对象不可移动…”,这个说法没有任何依据。 - Holger
显示剩余8条评论

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接