點選上方“芋道原始碼”,選擇“置頂公眾號”
技術文章第一時間送達!
原始碼精品專欄
譯文出處: shenzhang 原文出處:原文連結
使用Java的一個好處就是你可以不用親自來管理記憶體的分配和釋放。當你用new
關鍵字來實體化一個物件時,它所需的記憶體會自動的在Java堆中分配。堆會被垃圾回收器進行管理,並且它會在物件超出作用域時進行記憶體回收。但是在JVM中有一個‘後門’可以讓你訪問不在堆中的本地記憶體(native memory)。在這篇文章中,我會給你演示一個物件是怎樣以連續的位元組碼的方式在記憶體中進行儲存,並且告訴你是應該怎樣儲存這些位元組,是在Java堆中還是在本地記憶體中。最後我會就怎樣從JVM中訪問記憶體更快給一些結論:是用Java堆還是本地記憶體。
使用Unsafe
來分配和回收記憶體
sun.misc.Unsafe
可以讓你在Java中分配和回收本地記憶體,就像C語言中的malloc
和free
。透過它分配的記憶體不在Java堆中,並且不受垃圾回收器的管理,因此在它被使用完的時候你需要自己來負責釋放和回收。下麵是我寫的一個使用Unsafe
來管理本地記憶體的一個工具類:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
|
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物件放到本地記憶體中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
|
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
中:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
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
物件。
1
2
3
4
5
6
7
8
9
10
11
12
13
|
// 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物件所管理的本地記憶體空間都是獨立的,因此讀寫操作都不是連續的。那麼我們接下來就來對比下讀寫連續的記憶體空間的效能。
訪問一大塊的連續記憶體空間
這個測試分別在堆中和一大塊連續本地記憶體中包含了相同的測試資料。然後我們來做多次的讀寫操作看看哪個更快。並且我們會做一些隨機地址的訪問來對比結果。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
// 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進行操作。
如果你對 Dubbo 感興趣,歡迎加入我的知識星球一起交流。
目前在知識星球(https://t.zsxq.com/2VbiaEu)更新瞭如下 Dubbo 原始碼解析如下:
01. 除錯環境搭建
02. 專案結構一覽
03. 配置 Configuration
04. 核心流程一覽
05. 拓展機制 SPI
06. 執行緒池
07. 服務暴露 Export
08. 服務取用 Refer
09. 註冊中心 Registry
10. 動態編譯 Compile
11. 動態代理 Proxy
12. 服務呼叫 Invoke
13. 呼叫特性
14. 過濾器 Filter
15. NIO 伺服器
16. P2P 伺服器
17. HTTP 伺服器
18. 序列化 Serialization
19. 叢集容錯 Cluster
20. 優雅停機
21. 日誌適配
22. 狀態檢查
23. 監控中心 Monitor
24. 管理中心 Admin
25. 運維命令 QOS
26. 鏈路追蹤 Tracing
…
一共 60 篇++
原始碼不易↓↓↓↓↓
點贊支援老艿艿↓↓