我正在研究如何在JNI代码运行时HotSpot执行垃圾收集和/或堆压缩。
似乎众所周知,可以随时在Java中移动对象。我最终试图理解,如果JNI受到垃圾收集的影响。存在许多JNI函数来明确地防止垃圾收集;例如GetPrimitiveArrayCritical
。如果引用确实是volatile,则存在这样的函数是有道理的。但是,如果不是这样就毫无意义。
关于这个问题似乎存在大量相互矛盾的信息,我试图将其解决。
JNI代码在安全点运行并且可以继续运行,除非它调用 回到Java或调用一些特定的JVM方法,就此而言 可能会停下来以防止离开安全点(感谢Nitsan 注释)。
What mechanism JVM use to block threads during stop-the-world pause
上面让我觉得垃圾收集将与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 分配一个名为" local的专用存储区域 参考根集"。
在以下情况下创建本地参考根集:
- 线程首先附加到JVM("最外层"线程的根集)。
- 发生每次J2N转换。
JVM使用以下命令初始化为J2N转换创建的根集:
- 对来电者的对象或类的本地引用。
- 对作为参数传递给本机方法的每个对象的本地引用。
在本机代码中创建的新本地引用将添加到此J2N根目录中 设置,除非你创建一个新的"本地框架"使用PushLocalFrame JNI功能。
好的,所以 IBM 将传递的对象存储在local reference root set
中,但它没有讨论内存压缩。这只是说对象不会被垃圾收集。
GC可能随时决定是否需要压缩 垃圾收集堆。压缩涉及物理移动对象 从一个地址到另一个地址。这些对象可能由a引用 JNI本地或全球参考。为了使压实安全, JNI引用不是指向堆的直接指针。至少一个级别 间接隔离本机代码与对象移动。
如果本机方法需要获得内部的直接可寻址性 一个对象,情况更复杂。要求 直接寻址或固定堆是典型的需求 用于快速,共享访问大型原始数组。一个例子可能 包括一个屏幕缓冲区。在这些情况下,JNI关键部分可以是 使用,这对程序员提出了额外的要求,如 在这些函数的JNI描述中指定。见JNI 细节说明。
- GetPrimitiveArrayCritical返回Java™数组的直接堆地址,禁用垃圾回收直到相应的 调用ReleasePrimitiveArrayCritical。
- GetStringCritical返回java.lang.String实例的直接堆地址,在调用ReleaseStringCritical之前禁用垃圾收集。
好的,所以 IBM 基本上说JNI传递的对象可以随时移动! HotSpot 怎么样?
GetArrayElements系列函数记录在案 复制数组,或将它们固定到位(并且,这样做,防止a 压缩垃圾收集器移动它们)。它被记录为a GetPrimitiveArrayCritical的更安全,限制性更小的替代方案。 但是,我想知道哪些VM和/或垃圾收集器(如果有的话) 实际上是pin数组而不是复制它们。
Aleksandr 似乎认为访问传递对象内存的唯一安全方法是Get<PrimitiveType>ArrayElements
或GetPrimitiveArrayCritical
特伦特的答案并不令人兴奋。
至少在目前的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的这篇文章提到它确实压缩了堆,但没有特别关于移动数据。
现在,我已尽力遵循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
,它似乎没有任何我能找到的定义。
GetByteArrayElements
等JNI接口方法是安全的吗? JNI_QUICK_ENTRY
退出时,有人能找到JNI呼叫从VM转换回Native的位置吗? 答案 0 :(得分:3)
本机方法可以与包括GC在内的VM操作同时运行。它们是not stopped at safepoints。
GC可能会移动Java对象,即使它们是从正在运行的本机方法引用的。 jobject
句柄只是一个不可移动的对象引用数组的索引。每当移动一个对象时,相应的数组槽都会更新,尽管索引保持不变。也就是说,jobject
句柄仍然有效。
每次本机方法调用JNI函数时,它都会检查JVM是否处于安全点状态。如果是(例如GC正在运行),JNI功能将一直阻塞,直到安全点操作完成。
在执行GetByteArrayElements
等JNI函数期间,相应的线程被标记为_thread_in_vm
。在此状态下有正在运行的线程时,无法访问安全点。例如。如果在执行GetByteArrayElements
期间请求GC,GC将被延迟,直到JNI函数返回。
线程状态转换魔术由您注意到的线执行:
ThreadInVMfromNative __tiv(thread)
。这里__tiv
只是该类的一个实例。它的唯一目的是自动调用ThreadInVMfromNative
构造函数和析构函数。
ThreadInVMfromNative
构造函数调用transition_from_native
checks为安全点,并在需要时暂停当前线程。 ~ThreadInVMfromNative
析构函数切换回_thread_in_native
状态。
GetPrimitiveArrayCritical
和GetStringCritical
是唯一提供Java堆原始指针的JNI函数。他们prevent GC from starting直到调用相应的Release
函数。
state = _thread_in_native;
Native方法可以与GC同时运行
称为JNI函数
state = _thread_in_native_trans;
此时GC无法启动
如果正在进行VM操作,请阻止它完成
state = _thread_in_vm;
安全访问堆
答案 1 :(得分:2)
似乎众所周知,物体可以随时移动。
这可能很常见,但不是知识,也不是真的。传递给JNI方法或由JNI方法保存的对象在方法返回之前无法移动,或者显式释放对象,或者弹出包含它的LocalFrame。
如果这是真的,那么每个JNI接口方法都必须要求锁定或某种内存同步?
不,见上文。