这是我的第一篇翻译的比较完整的博文,若有错处请指出。这篇文章从两个测试去比较了JAVA堆和原生内存的读写操作。
译文出处:http://lipspace.duapp.com
JAVA语言有一个优点就是你不需要去处理内存的分配和释放。当你使用new关键字去实例化一个对象的时候,必要的内存就会分配到JVM堆里面。这个堆是由垃圾回收器管理着,当这个对象没有被引用的时候它的内存就会被回收掉。但是这里还是有一个后门可以直接通过JVM访问原生内存。在这篇文章里我将会展示一下如何把一个对象作为一个字节序列存储在内存里,并且讲讲你该选择堆还是直接内存(即原生内存)去保存这些字节。最后我会总结一下在JVM里面去读取堆内存还是直接内存更快。
使用Unsafe去分配和释放
sun.misc.Unsafe 类允许你像在C里面调用malloc和free那样在JAVA里面去分配和释放原生内存。你创建的内存将会脱离堆并且不再受到垃圾回收器的管理,也就是说这部分内存在使用完之后的释放就是你的责任了。下面我的Direct类展示如何使用unsafe类。
public class Direct implements Memory {
private static Unsafe unsafe;
private static boolean AVAILABLE = false;
static {
try {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
unsafe = (Unsafe)field.get(null);
AVAILABLE = true;
} catch(Exception e) {
// NOOP: throw exception later when allocating memory
}
}
public static boolean isAvailable() {
return AVAILABLE;
}
private static Direct INSTANCE = null;
public static Memory getInstance() {
if (INSTANCE == null) {
INSTANCE = new Direct();
}
return INSTANCE;
}
private Direct() {
}
@Override
public long alloc(long size) {
if (!AVAILABLE) {
throw new IllegalStateException("sun.misc.Unsafe is not accessible!");
}
return unsafe.allocateMemory(size);
}
@Override
public void free(long address) {
unsafe.freeMemory(address);
}
@Override
public final long getLong(long address) {
return unsafe.getLong(address);
}
@Override
public final void putLong(long address, long value) {
unsafe.putLong(address, value);
}
@Override
public final int getInt(long address) {
return unsafe.getInt(address);
}
@Override
public final void putInt(long address, int value) {
unsafe.putInt(address, value);
}
@Override
public final void putByte(long address, byte value) {
unsafe.putByte(address, value);
}
@Override
public final byte getByte(long address) {
return unsafe.getByte(address);
}
}
把一个对象存储在原生内存
等一下我们就会将下面的一个JAVA对象存储到原生内存里面:
public class SomeObject {
private long someLong;
private int someInt;
public long getSomeLong() {
return someLong;
}
public void setSomeLong(long someLong) {
this.someLong = someLong;
}
public int getSomeInt() {
return someInt;
}
public void setSomeInt(int someInt) {
this.someInt = someInt;
}
}
注意我们下面做的是为了能够在 内存里面存储它的属性:
public class SomeMemoryObject {
private final static int someLong_OFFSET = 0;
private final static int someInt_OFFSET = 8;
private final static int SIZE = 8 + 4; // one long + one int
private long address;
private final Memory memory;
public SomeMemoryObject(Memory memory) {
this.memory = memory;
this.address = memory.alloc(SIZE);
}
@Override
public void finalize() {
memory.free(address);
}
public final void setSomeLong(long someLong) {
memory.putLong(address + someLong_OFFSET, someLong);
}
public final long getSomeLong() {
return memory.getLong(address + someLong_OFFSET);
}
public final void setSomeInt(int someInt) {
memory.putInt(address + someInt_OFFSET, someInt);
}
public final int getSomeInt() {
return memory.getInt(address + someInt_OFFSET);
}
}
现在我们来为两个数组进行标准的读写访问操作:一个数组有成千上百万个SomeObjects ,另一个有成千上百万个SomeMemoryObjects。完整的代码可以在这里看到,输出结果如下:
// with JIT:
Number of Objects: 1,000 1,000,000 10,000,000 60,000,000
Heap Avg Write: 107 2.30 2.51 2.58
Native Avg Write: 305 6.65 5.94 5.26
Heap Avg Read: 61 0.31 0.28 0.28
Native Avg Read: 309 3.50 2.96 2.16
// without JIT: (-Xint)
Number of Objects: 1,000 1,000,000 10,000,000 60,000,000
Heap Avg Write: 104 107 105 102
Native Avg Write: 292 293 300 297
Heap Avg Read: 59 63 60 58
Native Avg Read: 297 298 302 299
总结一下:通过JVM去直接访问原生内存读取大约要慢上十倍,写入的话大约要慢两倍。但是注意每一个SomeMemoryObject都是分配了它自己的原生内存空间,所以读取和写入都是不连续的,也就是说,每一个直接内存对象读写的来源和去往它自己的分配内存空间都是有可能在任何地方的。接下来我们继续利用标准的读写访问操作去确定一下连续的直接内存和堆内存哪个更快。
访问连续的大块内存
这个测试包括了在堆内存里面分配一个byte数组和在原生内存里面对应分配一个chunk,两者存储的数据量是相同的。然后我们连续的写入和读取几次来看看哪一个更快。同时我们也测试了随机访问数组里面任何一个位置去比较一下结果。连续块测试代码在这里,随机测试代码在这里。结果如下:
// with JIT and sequential access:
Number of Objects: 1,000 1,000,000 1,000,000,000
Heap Avg Write: 12 0.34 0.35
Native Avg Write: 102 0.71 0.69
Heap Avg Read: 12 0.29 0.28
Native Avg Read: 110 0.32 0.32
// without JIT and sequential access: (-Xint)
Number of Objects: 1,000 1,000,000 10,000,000
Heap Avg Write: 8 8 8
Native Avg Write: 91 92 94
Heap Avg Read: 10 10 10
Native Avg Read: 91 90 94
// with JIT and random access:
Number of Objects: 1,000 1,000,000 1,000,000,000
Heap Avg Write: 61 1.01 1.12
Native Avg Write: 151 0.89 0.90
Heap Avg Read: 59 0.89 0.92
Native Avg Read: 156 0.78 0.84
// without JIT and random access: (-Xint)
Number of Objects: 1,000 1,000,000 10,000,000
Heap Avg Write: 55 55 55
Native Avg Write: 141 142 140
Heap Avg Read: 55 55 55
Native Avg Read: 138 140 138
结论:在连续块访问里,堆内存总是比直接内存要快。在随机访问里,堆内存比大的chunks存储要慢上一点,不是很多。
最后结论
在JAVA里面使用原生内存有着一定的用途,例如当你想要操作大量的数据(>2 gigabytes)或者当你想脱离垃圾回收机制的时候。然而,在上面做的测试可以看到,通过JVM里面访问直接内存是不比访问堆内存快的。这个结果是有道理的,因为在穿过JVM的时候是有一定的消耗的,不论是在使用堆的ByteBuffer还是使用直接内存的都是有相同的问题。而直接内存 ByteBuffer 的速度优势并不是指访问的速度优势,而是指直接和操作系统原生I/O操作的直接会话能力优势。另一个不错的讨论在 Peter Lawrey 的博客里面是关于随着工作时间序列的推移内存映射文件的使用( the use of memory-mapped files when working with time-series.),大家可以看看。