介紹
最近在工作中使用到了DirectBuffer來進行臨時數據的存放,由於使用的是堆外內存,省去了數據到內核的拷貝,因此效率比用ByteBuffer要高不少。之前看過許多介紹DirectBuffer的文章,在這里從源碼的角度上來看一下DirectBuffer的原理。
用戶態和內核態
Intel的 X86架構下,為了實現外部應用程序與操作系統運行時的隔離,分為了Ring0-Ring3四種級別的運行模式。Linux/Unix只使用了Ring0和Ring3兩個級別。Ring0被稱為用戶態,Ring3被稱為內核態。普通的應用程序只能運行在Ring3,並且不能訪問Ring0的地址空間。操作系統運行在Ring0,並提供系統調用供用戶態的程序使用。如果用戶態的程序的某一個操作需要內核態來協助完成(例如讀取磁盤上的某一段數據),那么用戶態的程序就會通過系統調用來調用內核態的接口,請求操作系統來完成某種操作。
下圖是用戶態調用內核態的示意圖:
DirectBuffer的創建
使用下面一行代碼就可以創建一個1024字節的DirectBuffer:
1 |
ByteBuffer.allocateDirect(1024); |
該方法調用的是new DirectByteBuffer(int cap)。DirectByteBuffer的構造函數是包級私有的,因此外部是調用不到的。
下面我們來看一下這行代碼背后的邏輯:
1 |
DirectByteBuffer(int cap) { // package-private |
DirectBuffer的構造函數主要做以下三個事情:
1、根據頁對齊和pageSize來確定本次的要分配內存實際大小
2、實際分配內存,並且記錄分配的內存大小
3、聲明一個Cleaner對象用於清理該DirectBuffer內存
需要注意的是DirectBuffer的創建是比較耗時的,所以在一些高性能的中間件或者應用下一般會做一個對象池,用於重復利用DirectBuffer。
DirectBuffer的使用
查看DirectBuffer類的方法聲明,對於DirectBuffer的使用主要有兩類方法,putXXX和getXXX。
putXXX方法(以putInt為例):
1 |
public ByteBuffer putInt(int x) { |
putInt方法會根據是否是內存對齊分別調用unsafe.putInt或者Bits.putInt來把數據放到直接內存中。Bits.putInt實際上會根據是大端或者是小端來區分如何把數據放到直接內存中,放的方式同樣是調用unsage.putInt。
getXXX方法(以getInt為例):
1 |
public int getInt() { |
首先判斷是否是頁對齊,如果不是頁對齊,那么直接通過unsafe.getInt來獲取數據;如果是頁對齊,那么通過Bits.getInt方法來獲取數據。Bits.getInt同樣是根據大端還是小端,調用unsafe.getInt來獲取數據。
DirectBuffer內存回收
DirectBuffer內存回收主要有兩種方式,一種是通過System.gc來回收,另一種是通過構造函數里創建的Cleaner對象來回收。
System.gc回收
在DirectBuffer的構造函數中,用到了Bit.reserveMemory這個方法,該方法如下
1 |
static void reserveMemory(long size, int cap) { |
reserveMemory方法首先嘗試分配內存,如果分配成功的話,那么就直接退出。如果分配失敗那么就通過調用tryHandlePendingReference來嘗試清理堆外內存(最終調用的是Cleaner的clean方法,其實就是unsafe.freeMemory然后釋放內存),清理完內存之后再嘗試分配內存。如果還是失敗,調用System.gc()來觸發一次FullGC進行回收(前提是沒有加-XX:-+DisableExplicitGC參數)。GC完之后再進行內存分配,失敗的話就會進行sleep,然后再進行嘗試。每次sleep的時間是逐步增加的,規律是1, 2, 4, 8, 16, 32, 64, 128, 256 (total 511 ms ~ 0.5 s)。如果最終還沒有可分配的內存,那么就會拋出OOM異常。
為什么是通過調用tryHandlePendingReference來回收內存呢?答案是JVM在判斷內存不可達之后會把需要GC的不可達對象放在一個PendingList中,然后應用程序就可以看到這些對象。通過調用tryHandlePendingReference來訪問這些不可達對象。如果不可達對象是Cleaner類型,也就是說關聯了堆外的DirectBuffer,那么該DirectBuffer就可以被回收了,通過調用Cleaner的clean方法來回收這部分堆外內存。
這個邏輯就是進行堆外內存分配時觸發的回收內存邏輯,也就是說在分配的時候如果遇到堆外內存不足,可能會觸發FullGC,然后嘗試進行分配。這也是為什么在一些用到堆外內存的應用中不建議加上-XX:-+DisableExplicitGC參數。
Cleaner對象回收
另個觸發堆外內存回收的時機是通過Cleaner對象的clean方法進行回收。在每次新建一個DirectBuffer對象的時候,會同時創建一個Cleaner對象,同一個進程創建的所有的DirectBuffer對象跟Cleaner對象的個數是一樣的,並且所有的Cleaner對象會組成一個鏈表,前后相連。
1 |
public static Cleaner create(Object ob, Runnable thunk) { |
Cleaner對象的clean方法執行時機是JVM在判斷該Cleaner對象關聯的DirectBuffer已經不被任何對象引用了(也就是經過可達性分析判定為不可達的時候)。此時Cleaner對象會被JVM掛到PendingList上。然后有一個固定的線程掃描這個List,如果遇到Cleaner對象,那么就執行clean方法。
DirectBuffer在一些高性能的中間件上使用還是相當廣泛的。正確的使用可以提升程序的性能,降低GC的頻率。
----------------------------------------------------------------------------------------------
歡迎關注我的微信公眾號:yunxi-talk,分享Java干貨,進階Java程序員必備。