了解JNI参数的安全访问

时间:2016-09-08 01:37:43

标签: java c++ c jvm java-native-interface

我正在研究如何在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功能。

     

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可能随时决定是否需要压缩   垃圾收集堆。压缩涉及物理移动对象   从一个地址到另一个地址。这些对象可能由a引用   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系列函数记录在案   复制数组,或将它们固定到位(并且,这样做,防止a   压缩垃圾收集器移动它们)。它被记录为a   GetPrimitiveArrayCritical的更安全,限制性更小的替代方案。   但是,我想知道哪些VM和/或垃圾收集器(如果有的话)   实际上是pin数组而不是复制它们。

     

Which VMs or GCs support JNI pinning?

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

以下是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的位置吗?

2 个答案:

答案 0 :(得分:3)

JNI方法如何在HotSpot JVM中工作

  1. 本机方法可以与包括GC在内的VM操作同时运行。它们是not stopped at safepoints

  2. GC可能会移动Java对象,即使它们是从正在运行的本机方法引用的。 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 checks为安全点,并在需要时暂停当前线程。 ~ThreadInVMfromNative析构函数切换回_thread_in_native状态。

  5. GetPrimitiveArrayCriticalGetStringCritical是唯一提供Java堆原始指针的JNI函数。他们prevent GC from starting直到调用相应的Release函数。

  6. 从本机代码

    调用JNI函数时的线程状态转换
    1. state = _thread_in_native;
      Native方法可以与GC同时运行

    2. 称为JNI函数

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

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

    5. state = _thread_in_vm;
      安全访问堆

答案 1 :(得分:2)

  

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

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

  

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

不,见上文。