了解如何使用 TheUnsafe 进行 memcpy

Understanding how to memcpy with TheUnsafe

我阅读了有关 TheUnsafe 的内容,但我感到困惑的是,与 C/C++ 不同,我们必须计算出内容的偏移量,并且还有 32 位 VM 与 64 位 VM,这可能有也可能没有不同的指针大小,具体取决于打开或关闭的特定 VM 设置(另外,我假设所有数据偏移量实际上都基于指针算法,这会影响它们)。

不幸的是,似乎所有关于如何使用 TheUnsafe 的文章都只源于一篇文章(恰好是第一篇文章),其他所有文章都在一定程度上从中复制粘贴。存在的不多,有些不清楚,因为作者显然不会说英语。

我的问题是:

如何使用 TheUnsafe[=11] 找到字段的偏移量 + 指向拥有该字段(或字段的字段,或字段的字段,字段的字段...)的实例的指针=]

如何使用它对另一个指针+偏移内存地址执行 m​​emcpy

考虑到数据可能有几 GB 的大小,并且考虑到堆不提供对数据对齐的直接控制,而且它很可能是碎片化的,因为:

1) 我认为没有什么可以阻止 VM 在偏移量 + 10 处分配字段 1 并在偏移量 sizeof(field1) + 32 处分配字段 2,是吗?

2) 我还假设 GC 会四处移动大块数据,有时会导致 1GB 大小的字段碎片化。

那么我描述的 memcpy 操作是否可行?

如果数据因为 GC 而碎片化,堆当然有指向下一块数据所在位置的指针,但是使用上述简单过程似乎并不能涵盖这一点。

所以数据必须是堆外的才能(可能)工作吗?如果是这样,如何使用 TheUnsafe 分配堆外数据,使此类数据作为实例的字段工作,当然在完成后释放分配的内存?

我鼓励任何不太理解问题的人询问他们需要知道的任何细节。

如果人们的整个想法是“将您需要复制的所有对象放在一个数组中并使用 System.arraycopy,我也敦促他们不要回答。我知道​​在这个精彩的论坛中这是常见的做法,而不是回答被问到的问题,提供一个完整的替代解决方案,原则上,除了完成相同的工作之外,与原始问题无关。

此致。

先来个大大的警告:“不安全必死”http://blog.takipi.com/still-unsafe-the-major-bug-in-java-6-that-turned-into-a-java-9-feature/

一些先决条件

static class DataHolder {
    int i1;
    int i2;
    int i3;
    DataHolder d1;
    DataHolder d2;
    public DataHolder(int i1, int i2, int i3, DataHolder dh) {
        this.i1 = i1;
        this.i2 = i2;
        this.i3 = i3;
        this.d1 = dh;
        this.d2 = this;
    }
}

Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafe.setAccessible(true);
Unsafe unsafe = (Unsafe) theUnsafe.get(null);

DataHolder dh1 = new DataHolder(11, 13, 17, null);
DataHolder dh2 = new DataHolder(23, 29, 31, dh1);

基础知识

获取字段(i1)的偏移量,可以使用如下代码:

Field fi1 = DataHolder.class.getDeclaredField("i1");
long oi1 = unsafe.objectFieldOffset(fi1);

访问实例dh1的字段值你可以写

System.out.println(unsafe.getInt(dh1, oi1)); // will print 11

您可以使用类似的代码来访问对象引用 (d1):

Field fd1 = DataHolder.class.getDeclaredField("d1");
long od1 = unsafe.objectFieldOffset(fd1);

您可以使用它从 dh2 获取对 dh1 的引用:

System.out.println(dh1 == unsafe.getObject(dh2, od1)); // will print true

字段排序和对齐

获取对象所有声明字段的偏移量:

for (Field f: DataHolder.class.getDeclaredFields()) {
    if (!Modifier.isStatic(f.getModifiers())) {
        System.out.println(f.getName()+" "+unsafe.objectFieldOffset(f));
    }
}

在我的测试中,JVM 似乎会按照它认为合适的方式重新排序字段(即添加一个字段可以在下一个 运行 上产生完全不同的偏移量)

本机内存中的对象地址

了解以下代码迟早会使您的 JVM 崩溃很重要,因为垃圾收集器会随机移动您的对象,而您无法控制它发生的时间和原因。

同样重要的是要了解以下代码取决于 JVM 类型(32 位与 64 位)和 JVM 的一些启动参数(即,压缩 oops 在 64 位 JVM 上的使用)。

在 32 位 VM 上,对对象的引用与 int 具有相同的大小。那么,如果您调用 int addr = unsafe.getInt(dh2, od1)); 而不是 unsafe.getObject(dh2, od1)),您会得到什么?会不会是对象的本机地址?

让我们试试:

System.out.println(unsafe.getInt(null, unsafe.getInt(dh2, od1)+oi1));

将按预期打印出 11

在没有压缩 oops (-XX:-UseCompressedOops) 的 64 位 VM 上,您需要编写

System.out.println(unsafe.getInt(null, unsafe.getLong(dh2, od1)+oi1));

在带有压缩 oops (-XX:+UseCompressedOops) 的 64 位 VM 上,事情有点复杂。此变体具有 32 位对象引用,通过将它们与 8L 相乘将其转换为 64 位地址:

System.out.println(unsafe.getInt(null, 8L*(0xffffffffL&(dh2, od1)+oi1));

这些访问有什么问题

问题出在垃圾收集器和这段代码上。垃圾收集器可以随意移动对象。由于 JVM 知道它的对象引用(局部变量 dh1 和 dh2,这些对象的字段 d1 和 d2),它可以相应地调整这些引用,您的代码永远不会注意到。

通过将对象引用提取到 int/long 变量中,您可以将这些对象引用转换为原始值,这些值恰好与对象引用具有相同的位模式,但垃圾收集器不知道这些是对象引用(它们也可能是由随机生成器生成的)因此在四处移动对象时不会调整这些值。因此,一旦垃圾收集周期被触发,您提取的地址就不再有效,并且尝试访问这些地址的内存可能会立即使您的 JVM 崩溃(好的情况),或者您可能会在没有注意到的情况下浪费您的内存(坏的情况)例)。