了解JNI参数的安全访问

我正在研究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似乎认为访问传递对象内存的唯一安全方法是通过GetArrayElementsGetPrimitiveArrayCritical

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

至少在当前的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 HotSpotfunction实际上是安全的。 是的,因为如果在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) 

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

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

JNI方法如何在HotSpot JVM中工作

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

  2. GC可能会移动Java对象,即使它们是从正在运行的本机方法引用的。 jobject句柄只是一个不可移动的对象引用数组的索引。 每当移动一个对象时,相应的数组槽都会更新,尽管索引保持不变。 也就是说, jobject句柄仍然有效。 每次本机方法调用JNI函数时,它都会检查JVM是否处于安全点状态。 如果是(例如GC正在运行),JNIfunction将阻塞,直到安全点操作完成。

  3. 在执行JNI函数(如GetByteArrayElements ,相应的线程标记为_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. state = _thread_in_native;
    Native方法可以与GC同时运行

  2. 调用JNI函数

  3. state = _thread_in_native_trans;
    GC无法在此时启动

  4. 如果正在进行VM操作,请阻止它完成

  5. state = _thread_in_vm;
    安全访问堆

似乎众所周知,物体可以随时移动。

它可能很常见,但它不是知识,也不是真的。 传递给JNI方法或由JNI方法保存的对象在方法返回之前无法移动,或者显式释放对象,或者弹出包含它的LocalFrame。

如果这是真的,那么每个JNI接口方法都必须要求锁定或某种内存同步?

不,见上文。