使用 GetPrimitiveArrayCritical 和 Get<PrimitiveType>ArrayRegion 之间的权衡是什么?

当使用 JNI 桥接 c++ 和 Java 时,我们总是希望避免不必要的复制。我发现可能会给我们很高的机会不复制数组。但我不完全理解这里记录的限制:GetPrimitiveArrayCritical

调用 GetPrimitiveArrayCritical 后,本机代码在调用 ReleasePrimitiveArrayCritical 之前不应长时间运行。我们必须将这对函数中的代码视为在“关键区域”中运行。在关键区域内,本机代码不得调用其他 JNI 函数,也不得调用任何可能导致当前线程阻塞并等待另一个 Java 线程的系统调用。(例如,当前线程不得对另一个 Java 线程写入的流调用 read。

这些限制使得本机代码更有可能获得数组的未复制版本,即使 VM 不支持固定也是如此。

我的问题是:

  1. 延长时间的确切含义是什么?

  2. 那么,这是否意味着我们可以安全地调用其他 JNI 函数或系统调用,这些函数或系统调用永远不会导致当前线程阻塞并等待另一个 Java 线程?

  3. GetPrimitiveArrayCritical thread-safe-safe吗?

  4. 在使用GetPrimitiveArrayCritical而不是GetArrayRegion时,我应该知道什么吗?


答案 1

GetPrimitiveArrayCritical将阻止所有现有的垃圾回收器。(实验性的谢南多厄收集器通常不会阻止。阻止垃圾回收器将阻止所有对象分配(一旦垃圾堆积起来)。

因此,使用规则如下:GetPrimitiveArrayCritical

  1. 不要调用任何 JNI 函数。各个部分的文档没有充分强调这一点,但这是您必须遵守的规则。原因是 JNI 函数可能会分配内存,尤其是本地引用。由于没有记录哪些 JNI 函数分配内存或分配内存多少,因此您无法调用其中任何一个。据推测,函数 EnsureLocalCapacity 可以预先分配本地引用来解决此问题,但没有人记录如何使用它。不要在关键区域内调用除 GetPrimitiveArrayCritical、GetStringCritical、ReleasePrimitiveArrayCritical 和 ReleaseStringCritical 以外的 JNI 函数,否则将陷入死锁。
  2. 不要以任何其他方式阻止可能需要从堆中分配内存的代码。这主要禁止阻止在同一 VM 中运行的 Java 代码。可以想象(但我不能肯定地说),你可以阻止不分配的Java代码。
  3. 您可以从其他线程调用 JNI 函数,只要您不阻塞等待这些线程即可。在其中调用 JNI 函数的线程可能会停止。请参阅下一点。
  4. 在关键区域中花费太多时间会使其他线程停止。根据您正在运行的线程数及其分配速率,您可以在关键区域中花费的时间可能会有所不同。在单线程应用程序中,或者在分配很少的多线程应用程序中,您可以安全地在关键区域中无限期地度过。但是,在其他情况下,您将使线程停止太多,以至于性能优势将被完全否定。然而,从正确性的角度来看,失速是安全的(而不是死锁)。GetPrimitiveArrayCritical
  5. 您可以嵌套 和 方法,它们是线程安全的。Get*CriticalRelease*Critical
  6. 检查 的返回值并正确设置,因为允许方法失败和/或进行复制,就像 .nullmodeGet*CriticalGet*ArrayElements
  7. 如果您正在编写一个库,并且正在考虑使用 GetPrimitiveArrayCritical,请创建一个运行时选项来改用 Get*ArrayElements即使您没有遇到GetPrimitiveArrayCritical引起的停滞,您的用户也可能会遇到。

如果您在关键区域内调用 JNI 函数,Java 标志将向您发出警告。忽略那些说有时可以在关键区域内调用 JNI 函数的文档。事实并非如此。-Xcheck:jni

Java 8 标志将打印有关停滞的分配和集合的有用日志消息。要查找的消息可以从 src/share/vm/memory/gcLocker 中收集.cpp-XX:+PrintJNIGCStalls -XX:+PrintGCDetails

在 Java 9 中,日志记录已更改。打开 gc 和 jni 的日志记录。要查找的消息可以从 src/share/vm/gc/shared/gcLocker 中收集.cpp

更多信息:


答案 2

这里要了解的关键是,您正在获取此内存上的关键部分(例如锁)。

  1. 延长一段时间旨在表明,一旦您持有此锁,您将阻止JVM执行其通常的事情。因此,您应该尽快执行需要执行的任何处理。例如,您当然不希望执行可能阻塞的 do 操作,因为这样会使系统完全停止。

  2. 你也许能够逃脱它,因为我怀疑这个锁所做的主要事情是防止垃圾回收,但文档很清楚,调用其他JNI函数是不支持的行为。因此,您可能会发现您的代码可以在一个版本的JVM中工作,而不能在其他版本中工作。

  3. 由于这是在获取锁(关键部分),是的,它是线程安全的。因为它是一把锁,所以你不应该持有它太久(见1)。

  4. GetArrayRegion总是会给你一份副本,GetPrimitiveArrayCritical可能会给你一份副本,或者可能会给你一个直接的指针。它不确定的原因是,如果直接指针对一般VM性能的影响太大(即,可能对某些垃圾回收器产生太大影响,使其值得允许锁定),它为JVM实现者提供了更大的灵活性,以避免直接指针。