使用sun.misc.Unsafe获取Java数组项的地址?

8

我很难理解sun.misc.Unsafe的文档 —— 我猜这是因为它不是为了普通使用而设计的,所以没有人真正关心它是否易读。但我实际上确实需要一种方法来查找数组中元素的地址(这样我就可以传递一个指向该地址的指针给本机代码)。是否有人有任何有效的代码可以做到这一点?它是可靠的吗?


1
@BalusC,如果你知道自己在做什么(即不在SO上提问),那么unsafe是好的和适当的,而且它可能比JNI更具可移植性。 - bestsss
1
@bestsss - 你知道一种可靠的方法来阻止GC在使用Unsafe返回的指针时移动对象吗? - Stephen C
@StephenC - 你担心的事情可能根本不会发生。据我所知,JVM在JIT编译代码中不会跟踪对象移动时的指针。因此,结果是收集器必须将任何存储在堆栈上看起来像是对象引用的值视为实际上是对象引用(请参见https://dev59.com/MWoy5IYBdhLWcg3wieq6#12097214以了解其工作原理)。这意味着在使用指针时对象不会移动,您无需采取任何措施来防止它。 - Jules
@Jules,它不能“不移动它”。这根本就没有意义。Java GC都是分代(复制)收集器。当发现非垃圾时,它会被复制到另一个空间,然后旧空间被擦除。未移动的对象最终会成为焦土。我认为您将Java GC的行为与保守GC的行为混淆了。 - Stephen C
也许我们在这里混淆了事情。像JNI allocateMemory这样的方法实际上是在堆之外分配内存。这就是为什么它们不会移动的原因。但是@Jules所要求的是一种获取堆中对象地址的方法。那是不同的。我相信你可以使用Unsafe来做到这一点...但这是不安全的! - Stephen C
显示剩余8条评论
3个回答

8

这是一个可用的示例,请注意,如果不合适地使用Unsafe类,可能会导致JVM崩溃。

import java.lang.reflect.Field;

import sun.misc.Unsafe;

public class UnsafeTest {

    public static void main(String... args) {
        Unsafe unsafe = null;

        try {
            Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (sun.misc.Unsafe) field.get(null);
        } catch (Exception e) {
            throw new AssertionError(e);
        }

        int ten = 10;
        byte size = 1;
        long mem = unsafe.allocateMemory(size);
        unsafe.putAddress(mem, ten);
        long readValue = unsafe.getAddress(mem);
        System.out.println("Val: " + readValue);

    }
}

2
那段代码必须是完全安全的,因为它正是 DirectByteBuffer 的实现方式。不幸的是,它并不能满足我的需求,我需要访问一个已存在数组中的数据。 - Jules
4
@StephenC,这段代码完全没问题,基本上就是 char* mem = malloc(size);...,它不会被GC所触及,并且会导致本地C泄漏,除非释放它。 - bestsss
2
代码没问题,但你还需要考虑释放已分配的内存。查看DirectByteBuffer以了解一种方法。此外,请注意已分配的内存未被清零,因此您不能假设您的直接数组已进行零初始化。 - Nitsan Wakart

8

如果不想使用数组,可以使用ByteBuffer.allocateDirect()直接缓冲区。该缓冲区在一个字段中具有地址,并且此地址在ByteBuffer的生命周期内不会改变。直接ByteBuffer使用最小的堆空间。您可以使用反射来获取地址。


您可以使用Unsafe来获取地址,但问题是GC随时可能将其移动。对象在内存中不是固定的。

在JNI中,您可以使用特殊方法将数据复制到/从Java对象中以避免此问题(以及其他问题)。如果要在C代码和对象之间交换数据,建议使用这些方法。


1
我宁愿不使用直接字节缓冲区(这将使解决问题变得相当容易),因为我正试图实现一个已经定义为字节数组的现有API,并且正在尝试避免复制到和从额外一组缓冲区的惩罚。这将作为最后的手段足够,但肯定有更好的方法。使用JNI可以使此过程更加容易,但不幸的是,我正在使用JNA,它似乎没有必要的接口来处理除整个数组以外的任何其他内容。 - Jules
@Jules,JNI会自己进行复制,除非您希望使用GetPrimitiveArrayCritical,但这可能会影响GC。勇敢地将其复制到直接(缓冲区)内存中。这是唯一可行的解决方案。例如,实现FileOutputStream(SocketOutputStream扩展它)使用堆栈上元素的副本。复制的惩罚并不高,因为最高成本是数据的加载成本(甚至是缓存未命中),无论如何都必须支付。复制还会导致预取缓存行,因此根据本机代码的工作方式,情况可能会更好。 - bestsss
@Jules,顺便说一下:即使javax.net.ssl.SSLEngine允许使用缓冲区,所有算法都是byte[],它们最终会将直接缓冲区复制到临时数组中,这是反向的故事和道德:不要在SSLEngine中使用直接缓冲区。 - bestsss

0

为什么?JNI中有很多处理Java数组内容的工具,您不需要使用未经记录的内部Sun类,因为它们可能在下周不存在。


我正在使用JNA而不是JNI,并且我正在与的函数进行接口交互需要将指针传递到数组的中间,而JNA似乎只能产生指向数组开头的指针。 - Jules
@Jules,不要混合使用JNA和普通数组,请使用直接缓冲区。 - bestsss

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接