哪個更快:Java堆還是本地內(nèi)存
使用Java的一個好處就是你可以不用親自來管理內(nèi)存的分配和釋放。當(dāng)你用new
關(guān)鍵字來實例化一個對象時,它所需的內(nèi)存會自動的在Java堆中分配。堆會被垃圾回收器進(jìn)行管理,并且它會在對象超出作用域時進(jìn)行內(nèi)存回收。但是在JVM中有一個‘后門’可以讓你訪問不在堆中的本地內(nèi)存(native memory)。在這篇文章中,我會給你演示一個對象是怎樣以連續(xù)的字節(jié)碼的方式在內(nèi)存中進(jìn)行存儲,并且告訴你是應(yīng)該怎樣存儲這些字節(jié),是在Java堆中還是在本地內(nèi)存中。最后我會就怎樣從JVM中訪問內(nèi)存更快給一些結(jié)論:是用Java堆還是本地內(nèi)存。
使用Unsafe
來分配和回收內(nèi)存
sun.misc.Unsafe
可以讓你在Java中分配和回收本地內(nèi)存,就像C語言中的malloc
和free
。通過它分配的內(nèi)存不在Java堆中,并且不受垃圾回收器的管理,因此在它被使用完的時候你需要自己來負(fù)責(zé)釋放和回收。下面是我寫的一個使用Unsafe
來管理本地內(nèi)存的一個工具類:
- 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);
- }
- }
在本地內(nèi)存中分配一個對象
讓我們來將下面的Java對象放到本地內(nèi)存中:
- 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;
- }
- }
我們所做的僅僅是把對象的屬性放入到Memory
中:
- 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);
- }
- }
現(xiàn)在我們來看看對兩個數(shù)組的讀寫性能:其中一個含有數(shù)百萬的SomeObject
對象,另外一個含有數(shù)百萬的SomeMemoryObject
對象。
// 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
結(jié)論:跨越JVM的屏障來讀本地內(nèi)存大約會比直接讀Java堆中的內(nèi)存慢10倍,而對于寫操作會慢大約2倍。但是需要注意的是,由于每一個SomeMemoryObject對象所管理的本地內(nèi)存空間都是獨立的,因此讀寫操作都不是連續(xù)的。那么我們接下來就來對比下讀寫連續(xù)的內(nèi)存空間的性能。
訪問一大塊的連續(xù)內(nèi)存空間
這個測試分別在堆中和一大塊連續(xù)本地內(nèi)存中包含了相同的測試數(shù)據(jù)。然后我們來做多次的讀寫操作看看哪個更快。并且我們會做一些隨機(jī)地址的訪問來對比結(jié)果。
// 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
結(jié)論:在做連續(xù)訪問的時候,Java堆內(nèi)存通常都比本地內(nèi)存要快。對于隨機(jī)地址訪問,堆內(nèi)存僅僅比本地內(nèi)存慢一點點,并且是針對大塊連續(xù)數(shù)據(jù)的時候,而且沒有慢很多。
最后的結(jié)論
在Java中使用本地內(nèi)存有它的意義,比如當(dāng)你要操作大塊的數(shù)據(jù)時(>2G)并且不想使用垃圾回收器(GC)的時候。從延遲的角度來說,直接訪問本地內(nèi)存不會比訪問Java堆快。這個結(jié)論其實是有道理的,因為跨越JVM屏障肯定是有開銷的。這樣的結(jié)論對使用本地還是堆的ByteBuffer
同樣適用。使用本地ByteBuffer的速度提升不在于訪問這些內(nèi)存,而是它可以直接與操作系統(tǒng)提供的本地IO進(jìn)行操作。