我正在研究HotSpot在JNI代码运行时如何执行垃圾收集和/或堆压缩。
似乎普遍认为在Java中对象随时可能被移动。我试图明确地了解JNI是否受到垃圾收集的影响。存在一些JNI函数可以显式地防止垃圾收集,例如GetPrimitiveArrayCritical
。如果引用确实是不稳定的,那么这样的功能是有意义的。但是,如果它们不是,则没有任何意义。
关于这个问题似乎存在大量矛盾的信息,我正在努力梳理。
JNI代码在安全点运行,并且可以继续运行,除非它回调Java或调用某些特定的JVM方法,在这种情况下,它可能会停止以防止离开安全点(感谢Nitsan的评论)。
上述内容让我想到垃圾收集将与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和/或垃圾收集器(如果有)实际上是固定数组而不是复制它们。
Aleksandr认为访问传递对象的内存的唯一安全方式是通过Get<PrimitiveType>ArrayElements
或GetPrimitiveArrayCritical
Trent的回答并不令人兴奋。
这封OpenJDK邮件似乎表明ConcurrentMarkAndSweep GC是非移动的。至少在当前的JVM中(我还没有检查这个问题有多远被后移),由于CMS GC是非移动的,所以它不受JNI关键部分的影响(除了在并发模式失败时可能会出现非停止世界压缩 - 在这种情况下,分配线程必须停顿,直到关键部分被清除 - 这后一种停顿很可能比您更频繁地看到老年代病理学中的慢路径直接分配)。请注意,老年代的直接分配本身不仅速度较慢(第一级性能影响),而且可能导致更多的继承(因为所谓的裙带关系),以及更慢的后续扫描需要扫描更多的脏卡片(后两者都是第二级效应)。http://mail.openjdk.java.net/pipermail/hotspot-runtime-dev/2007-December/000074.html这篇关于G1的帖子提到它确实会压缩堆,但没有具体提到数据移动。https://www.infoq.com/articles/G1-One-Garbage-Collector-To-Rule-Them-All
由于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的位置吗?