2

所有这些都与新的 JDK 17 相关。

我正在尝试将堆上字节数组转换为 MemorySegment 并将其传递给本机函数。我创建了简单的示例代码来显示这一点:

        final CLinker cLinker = CLinker.getInstance();
        
        // int strlen(const char *str);
        final Optional<MemoryAddress> oSymbolAddress = CLinker.systemLookup().lookup("strlen");
        
        final MethodHandle mh = cLinker.downcallHandle(oSymbolAddress.get(),
                MethodType.methodType(int.class, MemoryAddress.class),
                FunctionDescriptor.of(C_INT, C_POINTER));        
        
        out.println("I found this method handle: " + mh);
        final byte[] ba = new byte[100];
        ba[0] = 'h';
        ba[1] = 'e';
        ba[2] = 'l';
        ba[3] = 'l';
        ba[4] = 'o';
        ba[5] = 0;
        
        final MemorySegment stringSegment = MemorySegment.ofArray(ba);
        final int result = (Integer) mh.invoke(stringSegment.address());
        
        out.println("The length of the string is: " + result);

它试图运行但它抛出:

Exception in thread "main" java.lang.UnsupportedOperationException: Not a native address
    at jdk.incubator.foreign/jdk.internal.foreign.MemoryAddressImpl.toRawLongValue(MemoryAddressImpl.java:91)

如果不是使用MemorySegment.ofArray(ba),我使用这个:

        final MemorySegment stringSegment = MemorySegment.allocateNative(100, newImplicitScope());
        stringSegment.asByteBuffer().put(ba);

它有效并给出了预期的答案(5)。

我查看了这个函数MemoryAddressImpl.java,我可以看到:

    @Override
    public long toRawLongValue() {
        if (segment != null) {
            if (segment.base() != null) {
                throw new UnsupportedOperationException("Not a native address");
            }
            segment.checkValidState();
        }
        return offset();
    }

显然segment.base()是返回null。

我真的不明白发生了什么事。我认为 Project Panama 的主要优势之一是本机代码可以访问堆内存,以避免复制。我当然可以做一个allocateNative()然后将字节数组复制到其中,但这是我认为应该避免的副本。

对此有什么想法吗?我做错了什么或误解了如何使用堆内存?

4

1 回答 1

2

我认为 Project Panama 的主要优势之一是本机代码可以访问堆内存,以避免复制。

实际上,尽管巴拿马项目在可用性方面取得了重大进展,但由于多种原因,这是不可能的。

  • GC 在 Java 堆内存中移动事物,因此任何对象(包括表)的地址都可能/将随着时间而改变。然而,本机代码被赋予一个指向内存地址的指针,该地址当然不会在 GC 周期后更新(甚至没有提到在周期中间访问该内存)。
  • JNI 有 API 可以真正防止在本机代码中间通过Get*Critical部分发生 GC。不幸的是,阻止 GC 可能会对应用程序性能产生重大影响。

事实上,Project Panama 正是在试图避免阻塞 GC。这就是为什么访问的内存有明确的分离以及为什么有必要复制到本机内存/从本机内存复制的原因。

这应该不是什么大问题,除非这是热代码路径(即,它被非常频繁地调用),或者代码处理非常大的数据。在这种情况下,代码可能希望在堆外完成大部分工作。如果数据在文件中,则可以从本机代码访问该文件,或使用巴拿马内存映射文件。

var big = Path.of("path/to/big.data");
try (var scope = ResourceScope.newConfinedScope()) {
  var bigMM = MemorySegment.mapFile(big, 0, Files.size(big), FileChannel.MapMode.READ_ONLY, scope);
  return (int) mh.invoke(bigMM.address());
}
于 2021-11-12T14:23:57.017 回答