哪個更快:Java堆還是本地記憶體
使用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堆記憶體Heap與非堆記憶體Non-HeapJava記憶體
- Java 堆疊記憶體分配Java記憶體
- [轉載] Java直接記憶體與堆記憶體Java記憶體
- java棧記憶體和堆記憶體的詮釋Java記憶體
- Java堆外直接記憶體回收Java記憶體
- RAM是記憶體還是外存記憶體
- JAVA堆外記憶體排查小結Java記憶體
- 記憶體堆疊記憶體
- 深入理解Java的堆記憶體和執行緒記憶體Java記憶體執行緒
- Java直接(堆外)記憶體使用詳解Java記憶體
- JS中的棧記憶體、堆記憶體JS記憶體
- 直接記憶體和堆記憶體誰快記憶體
- 什麼是堆外記憶體off-heap記憶體
- Redis記憶體——記憶體消耗(記憶體都去哪了?)Redis記憶體
- JAVA獲取時間戳,哪個更快Java時間戳
- 堆記憶體和棧記憶體詳解(轉載)記憶體
- JVM堆記憶體詳解JVM記憶體
- JVM堆記憶體設定JVM記憶體
- Tomcat增加堆記憶體Tomcat記憶體
- Netty之Java堆外記憶體掃盲貼NettyJava記憶體
- JAVA的堆疊和記憶體、垃圾回收解說Java記憶體
- Java記憶體模型FAQ(一) 什麼是記憶體模型Java記憶體模型
- 什麼是Java記憶體模型?Java記憶體模型
- 什麼是Java記憶體模型Java記憶體模型
- Java記憶體區域總結(堆、棧、方法區等)Java記憶體
- JVM 堆記憶體設定原理JVM記憶體
- 遺失的JVM堆記憶體JVM記憶體
- tomcat的執行的時候,GC進行記憶體回收,回收後的記憶體是還給OS還是還給JVMTomcatGC記憶體JVM
- JVM堆外記憶體問題排查JVM記憶體
- eclipse設定JVM記憶體堆EclipseJVM記憶體
- jvm堆記憶體和GC簡介JVM記憶體GC
- Java記憶體模型是什麼,為什麼要有Java記憶體模型,Java記憶體模型解決了什麼問題?Java記憶體模型
- java中堆與棧 java中資料佔用記憶體空間的大小Java記憶體
- MAT工具定位分析Java堆記憶體洩漏問題方法Java記憶體
- 常見電腦記憶體條顆粒知識科普:記憶體顆粒哪個好?記憶體顆粒排名記憶體
- 記憶體中發堆和棧,棧是執行時的單位,而堆是儲存的單位記憶體
- 乞丐是如何節約Java記憶體的Java記憶體
- Java的記憶體 -JVM 記憶體管理Java記憶體JVM