了解如何使用TheUnsafe进行memcpy

时间:2016-06-30 04:40:28

标签: java pointers garbage-collection memcpy

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

不幸的是,似乎所有关于如何使用TheUnsafe的内容都只来自一篇文章(碰巧是第一篇),而其他所有文章都是在某种程度上粘贴的。其中不存在很多,有些不清楚,因为作者显然不会说英语。

我的问题是:

如何使用TheUnsafe

找到字段的偏移量+指向拥有该字段的实例的指针(或字段的字段,字段的字段,字段的字段......)

如何使用它来执行memcpy到另一个指针+偏移内存地址

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

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

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

我所描述的memcpy操作是否可能?

如果由于GC而导致数据碎片化,那么堆当然有一个指向下一个数据块的指针,但是使用上述简单的过程似乎并不能解决这个问题。

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

我鼓励任何不太了解这个问题的人询问他们需要知道的具体细节。

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

最好的问候。

1 个答案:

答案 0 :(得分:1)

首先是一个大警告:“不安全必须死” 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的一些启动参数(即64位JVM上压缩oops的使用)。

在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的64位VM上(-XX:-UseCompressedOops),你需要编写

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

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

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

这些访问有什么问题

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

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