了解如何使用TheUnsafe进行memcpy

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

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

我的问题是:

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

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

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

1)我认为没有什么能阻止VM在offset + 10处分配field1而在offset sizeof(field1)+ 32处分配field2,是吗?

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的某些启动参数(即在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崩溃(好的情况),或者您可能在没有现场注意的情况下丢弃您的内存(坏的)案件)。