了解JNI参数的安全访问

Joh*_*y V 6 c c++ java java-native-interface jvm

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

似乎众所周知,可以随时在Java中移动对象.我试图明白,如果JNI受到垃圾收集的影响.存在许多JNI函数来明确地防止垃圾收集; 如GetPrimitiveArrayCritical.如果引用确实是volatile,则存在这样的函数是有道理的.但是,如果不是,那就没有意义.

关于这个问题似乎存在大量相互矛盾的信息,我正试图解决这个问题.

JNI代码在安全点运行并且可以继续运行,除非它回调到Java或调用某些特定的JVM方法,此时可以停止它以防止离开安全点(感谢Nitsan的注释).

JVM在停止世界暂停期间用于阻止线程的机制

上面让我认为垃圾收集将与JNI代码同时运行.那不可能是安全的,对吗?

为了实现本地引用,Java VM为从Java到本机方法的每次控制转换创建了一个注册表.注册表将不可移动的本地引用映射到Java对象,并防止对象被垃圾回收.传递给本机方法的所有Java对象(包括那些作为JNI函数调用结果返回的对象)都会自动添加到注册表中.在本机方法返回后删除注册表,允许其所有条目被垃圾回收.

https://docs.oracle.com/javase/7/docs/technotes/guides/jni/spec/design.html#wp16789

好的,所以local引用是不可移动的,但是没有说明压缩的内容.

JVM必须确保将作为参数从Java™传递到本机方法的对象以及由本机代码创建的任何新对象仍可由GC访问.为了处理GC要求,JVM分配了一个称为"本地参考根集"的小型专用存储区域.

在以下情况下创建本地引用根集:

  • 线程首先附加到JVM(线程的"最外层"根集).
  • 每次发生J2N转换.

JVM使用以下命令初始化为J2N转换创建的根集:

  • 对调用者的对象或类的本地引用.
  • 对作为参数传递给本机方法的每个对象的本地引用.

除非使用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将传递的对象存储在其中,local reference root set但它没有讨论内存压缩.这只是说对象不会被垃圾收集.

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或GC支持JNI固定?

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

特伦特的回答并不令人兴奋.

至少在当前的JVM中(我还没有检查过它被反向移植的程度),CMS GC,因为它不移动不受JNI关键部分的影响(模数如果有并发的话,可能会发生非停止压缩)模式失败 - 在这种情况下,分配线程必须停止,直到关键部分被清除 - 后一种停顿可能比你可能更频繁地看到的旧病理学中的慢速路径直接分配更罕见.请注意,旧版本中的直接分配不仅速度慢(一阶性能影响),而且可能反过来导致更多的任期(因为所谓的裙带关系),以及由于卡片更加严重而导致的后续清理速度变慢需要扫描(后者都是二次效果).

http://mail.openjdk.java.net/pipermail/hotspot-runtime-dev/2007-December/000074.html

OpenJDK邮件列表上的这封电子邮件似乎表明ConcurrentMarkAndSweep GC是不动的.

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
Run Code Online (Sandbox Code Playgroud)

这是宏 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)
Run Code Online (Sandbox Code Playgroud)

我已经按照这里的每个功能,但必须看到任何类型的互斥或内存同步器.我无法遵循的唯一功能__tiv似乎没有任何我能找到的定义.

  • 有人可以向我解释为什么JNI接口方法GetByteArrayElements是安全的吗?
  • 虽然我们正在使用它,但有人可以在JNI_QUICK_ENTRY退出时找到JNI调用从VM转换回Native的位置吗?

apa*_*gin 5

JNI 方法如何在 HotSpot JVM 中工作

  1. 本机方法可以与包括 GC 在内的 VM 操作同时运行。他们不会停在安全点

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

  3. 在执行诸如 之类的 JNI 函数期间GetByteArrayElements,相应的线程被标记为_thread_in_vm。当存在处于该状态的正在运行的线程时,无法到达安全点。例如,如果在执行期间请求GC GetByteArrayElements,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;
    安全访问堆