使用Java的一個好處就是你可以不用親自來管理記憶體的分配和釋放。當你用new
關鍵字來實例化一個物件時,它所需的記憶體會自動的在Java堆中分配。堆會被垃圾回收器進行管理,並且它會在物件超出作用域時進行記憶體回收。但是在JVM中有一個『後門』可以讓你存取不在堆中的本地記憶體(native memory)。在這篇文章中,我會給你演示一個物件是怎樣以連續的字節碼的方式在內存中進行存儲,並且告訴你是應該怎樣存儲這些字節,是在Java堆中還是在本地內存中。最後我會就怎樣從JVM存取記憶體更快給一些結論:是用Java堆還是本地記憶體。
Unsafe
來分配和回收記憶體sun.misc.Unsafe
可以讓你在Java中分配和回收本地內存,就像C語言中的malloc
和free
。透過它分配的記憶體不在Java堆中,並且不受垃圾回收器的管理,因此在它被使用完的時候你需要自己來負責釋放和回收。下面是我寫的一個使用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); } }
讓我們來將下面的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; } }
我們所做的只是把物件的屬性放入到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); } }
現在讓我們來看看兩個陣列的讀寫效能:其中一個含有數百萬的SomeObject
對象,另外一個含有數百萬的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
結論:跨越JVM的屏障來讀取本地記憶體大約會比直接讀取Java堆中的記憶體慢10倍,而對於寫入操作會慢大約2倍。但要注意的是,由於每一個SomeMemoryObject物件所管理的本地記憶體空間都是獨立的,因此讀寫操作都不是連續的。那我們接下來就來比較下讀寫連續的記憶體空間的效能。
這個測試分別在堆中和一大塊連續本地記憶體中包含了相同的測試資料。然後我們來做多次的讀寫操作看看哪個比較快。並且我們會做一些隨機地址的訪問來對比結果。
// 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
結論:在做連續存取的時候,Java堆記憶體通常都比本機記憶體快。對於隨機地址訪問,堆內存僅比本地內存慢一點點,並且是針對大塊連續數據的時候,而且沒有慢很多。
在Java中使用本地記憶體有它的意義,例如當你要操作大塊的資料時(>2G)並且不想使用垃圾回收器(GC)的時候。從延遲的角度來說,直接存取本機記憶體不會比存取Java堆快。這個結論其實是有道理的,因為跨越JVM屏障肯定是有開銷的。這樣的結論對使用本地還是堆的ByteBuffer
同樣適用。使用本地ByteBuffer的速度提升不在於存取這些內存,而是它可以直接與作業系統提供的本地IO進行操作
以上是Java堆和本地記憶體哪個更快的詳細內容。更多資訊請關注PHP中文網其他相關文章!