本章還是關(guān)于NIO的概念鋪底,有關(guān)NIO相關(guān)的代碼,我還是希望大家閑余時間取網(wǎng)上找一下有關(guān)使用JDK NIO開發(fā)服務(wù)端奢人、客戶端的代碼攻走,我會取寫這些殷勘,但是具體的代碼我不會很詳細(xì)的取介紹,下一章的話可能就要上代碼了昔搂,具體的規(guī)劃如下:
講一下NIO基礎(chǔ)API的使用玲销、分析Netty的核心思想,使用Reactor模式仿寫一個多線程版的Nio程序摘符、再然后就是關(guān)于Netty的源碼分析了贤斜!
回歸正題,NIO的高性能除了體現(xiàn)在Epoll模型之外议慰,還有很重要的一點蠢古,就是零拷貝!首先大家要先明白一點别凹,所謂的0拷貝草讶,并不是一次拷貝都沒有,而是數(shù)據(jù)由內(nèi)核空間向用戶空間的相互拷貝被取消了炉菲,所以稱之為零拷貝堕战!
系統(tǒng)如何操作底層數(shù)據(jù)文件
在了解整個IO的讀寫的過程中,我們需要知道我們的應(yīng)用程序是如何操作一些內(nèi)存拍霜、磁盤數(shù)據(jù)的嘱丢!
我們在開發(fā)中,假設(shè)要向硬盤中寫入一段文本數(shù)據(jù)祠饺,我們并不需要操作太多的細(xì)節(jié)越驻,而是只需要簡單的將數(shù)據(jù)轉(zhuǎn)為字節(jié)然后在告訴程序,我們要寫入的位置以及名稱就可以了道偷,為什么這么簡單呢缀旁?因為操作系統(tǒng)全部幫我們開發(fā)好了,我們只需要調(diào)用就可以了勺鸦,但是我們想一下并巍,如果我們的操作系統(tǒng)的全部權(quán)限,包括內(nèi)存都可以讓用戶隨意操作那是一個很危險的事情换途,例如某些病毒可以隨意篡改內(nèi)存中的數(shù)據(jù)懊渡,以達(dá)到某些不軌的目的刽射,那就很難受了!所以剃执,我們的操作系統(tǒng)就必須對這些底層的API進(jìn)行一些限制和保護(hù)誓禁!
但是如何保護(hù)呢?一方面忠蝗,我們希望外部系統(tǒng)能夠調(diào)用我的系統(tǒng)API现横,另一方面我又不想外部隨意訪問我的API怎么辦呢? 此時,我們就要引申出來一個組件叫做kernel,你可以把它理解為一段程序阁最,他在機(jī)器啟動的時候被加載進(jìn)來戒祠,被用于管理系統(tǒng)底層的一些設(shè)備,例如硬盤速种、內(nèi)存姜盈、網(wǎng)卡等硬件設(shè)備!當(dāng)我們又了kernel之后配阵,會發(fā)生什么呢馏颂?
我們還是以寫出文件為例,當(dāng)我們調(diào)用了一個write api的時候棋傍,他會將write的方法名以及參數(shù)加載到CPU的寄存器中救拉,同時執(zhí)行一個指令叫做 int 0x80的指令蔼囊,int 0x80是 interrupt 128(0x80的10進(jìn)制)的縮寫歌径,我們一般叫80中斷,當(dāng)調(diào)用了這個指令之后岭粤,CUP會停止當(dāng)前的調(diào)度麸拄,保存當(dāng)前的執(zhí)行中的線程的狀態(tài)派昧,然后在中斷向量表中尋找 128代表的回調(diào)函數(shù),將之前寫到寄存器中的數(shù)據(jù)(write /參數(shù))當(dāng)作參數(shù)拢切,傳遞到這個回調(diào)函數(shù)中蒂萎,由這個回調(diào)函數(shù)去尋找對應(yīng)的系統(tǒng)函數(shù)write進(jìn)行寫出操作!
大家回想一下淮椰,當(dāng)系統(tǒng)發(fā)起一個調(diào)用后不再是用戶程序直接調(diào)用系統(tǒng)API的而是切換成內(nèi)核調(diào)用這些API五慈,所以內(nèi)核是以這種方式來保護(hù)系統(tǒng)的而且這也就是 用戶態(tài)切換到內(nèi)核態(tài)!
傳統(tǒng)的I/O讀寫
場景:讀取一個圖片通過socket傳輸?shù)娇蛻舳苏故尽?/p>
- 程序發(fā)起read請求主穗,調(diào)用系統(tǒng)read api由用戶態(tài)切換至內(nèi)核態(tài)泻拦!
- CPU通過DMA引擎將磁盤數(shù)據(jù)加載到內(nèi)核緩沖區(qū),觸發(fā)中止指令黔牵,CPU將內(nèi)核緩沖區(qū)的數(shù)據(jù)拷貝到用戶空間!由內(nèi)核態(tài)切換至用戶態(tài)爷肝!
- 程序 發(fā)起write調(diào)用猾浦,調(diào)用系統(tǒng)API陆错,由用戶態(tài)切換只內(nèi)核態(tài),CPU將用戶空間的數(shù)據(jù)拷貝到Socket緩沖區(qū)金赦!再由內(nèi)核態(tài)切換至用戶態(tài)音瓷!
- DMA引擎異步將Socket緩沖區(qū)拷貝到網(wǎng)卡通過底層協(xié)議棧發(fā)送至對端!
我們可以了解一下夹抗,這當(dāng)中發(fā)生了4次上下文的切換和4次數(shù)據(jù)拷貝绳慎!我們大致分析一下,那些數(shù)據(jù)拷貝是多余的:
- 磁盤文件拷貝到內(nèi)核緩沖區(qū)是必須的不能省略漠烧,因為這個數(shù)據(jù)總歸要讀取出來的杏愤!
- 內(nèi)核空間拷貝到用戶空間,如果我們不準(zhǔn)備對數(shù)據(jù)做修改的話已脓,好像沒有必要呀珊楼,直接拷貝到Socket緩沖區(qū)不就可以了!
- Socket到網(wǎng)卡度液,好像也有點多余厕宗,為什么這么說呢?因為我們直接從內(nèi)核空間里面直接懟到網(wǎng)卡里面堕担,中間不就少了很多的拷貝和上下文的切換看嗎已慢?
sendfile
我們通過Centos man page指令查看該函數(shù)的定義!
也可以通過該鏈接下載:sendfile()函數(shù)介紹
基本介紹:
sendfile——在文件描述符之間傳輸數(shù)據(jù)
描述
ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);
sendfile()在一個文件描述符和另一個文件描述符之間復(fù)制數(shù)據(jù)霹购。因為這種復(fù)制是在內(nèi)核中完成的佑惠,所以sendfile()比read(2)和write(2)的組合更高效,后者需要在用戶空間之間來回傳輸數(shù)據(jù)厕鹃。
in_fd應(yīng)該是打開用于讀取的文件描述符兢仰,而out_fd應(yīng)該是打開用于寫入的文件描述符。
如果offset不為NULL剂碴,則它指向一個保存文件偏移量的變量把将,sendfile()將從這個變量開始從in_fd讀取數(shù)據(jù)。當(dāng)sendfile()返回時忆矛,這個變量將被設(shè)置為最后一個被讀取字節(jié)后面的字節(jié)的偏移量察蹲。如果offset不為NULL,則sendfile()不會修改當(dāng)前值
租用文件偏移in_fd;否則催训,將調(diào)整當(dāng)前文件偏移量以反映從in_fd讀取的字節(jié)數(shù)洽议。
如果offset為NULL,則從當(dāng)前文件偏移量開始從in_fd讀取數(shù)據(jù)漫拭,并通過調(diào)用更新文件偏移量亚兄。
count是要在文件描述符之間復(fù)制的字節(jié)數(shù)。
in_fd參數(shù)必須對應(yīng)于支持類似mmap(2)的操作的文件(也就是說采驻,它不能是套接字)审胚。
在2.6.33之前的Linux內(nèi)核中匈勋,out_fd必須引用一個套接字。從Linux 2.6.33開始膳叨,它可以是任何文件洽洁。如果是一個常規(guī)文件,則sendfile()適當(dāng)?shù)馗奈募屏俊?/p>
簡單來說菲嘴,sendfile函數(shù)可以將兩個文件描述符里面的數(shù)據(jù)來回復(fù)制饿自,再Linux中萬物皆文件!內(nèi)核空間和Socket也是一個個的對應(yīng)的文件龄坪,sendfile函數(shù)可以將兩個文件里面的數(shù)據(jù)來回傳輸昭雌,這也造就了,我們后面的零拷貝優(yōu)化悉默!
sendfile - linux2.4之前
- 用戶程序發(fā)起read請求城豁,程序由用戶態(tài)切換至內(nèi)核態(tài)!
- DMA引擎將數(shù)據(jù)從磁盤拷貝出來到內(nèi)核空間抄课!
- 調(diào)用sendfile函數(shù)將內(nèi)核空間的數(shù)據(jù)直接拷貝到Socket緩沖區(qū)唱星!
- 上下文從內(nèi)核態(tài)切換至用戶態(tài)
- Socket緩沖區(qū)通過DMA引擎,將數(shù)據(jù)拷貝到網(wǎng)卡跟磨,通過底層協(xié)議棧發(fā)送到對端间聊!
這個優(yōu)化不可謂不狠,上下文切換次數(shù)變?yōu)閮纱蔚志校瑪?shù)據(jù)拷貝變?yōu)閮纱伟チ瘢@基本符合了我們上面的優(yōu)化要求,但是我們還是會發(fā)現(xiàn)僵蛛,從內(nèi)核空間到Socket緩沖區(qū)尚蝌,然后從內(nèi)核緩沖區(qū)到網(wǎng)卡似乎也有點雞肋,所以充尉,Linux2.4之后再次進(jìn)行了優(yōu)化飘言!
sendfile - linux2.4之后
- 用戶程序發(fā)起read請求,程序由用戶態(tài)切換至內(nèi)核態(tài)驼侠!
- DMA引擎將數(shù)據(jù)從磁盤拷貝出來到內(nèi)核空間姿鸿!
- 調(diào)用sendfile函數(shù)將內(nèi)核空間的數(shù)據(jù)再內(nèi)存中的起始位置和偏移量寫入Socket緩沖區(qū)!然后內(nèi)核態(tài)切換至用戶態(tài)倒源!
- DMA引擎讀取Socket緩沖區(qū)的內(nèi)存信息苛预,直接由內(nèi)核空間拷貝至網(wǎng)卡!
這里的優(yōu)化是原本將內(nèi)核空間的數(shù)據(jù)拷貝至Socket緩沖區(qū)的步驟笋熬,變成了只記錄文件的起始位置和偏移量热某!然后程序直接返回,由DMA引擎異步的將數(shù)據(jù)從內(nèi)核空間拷貝到網(wǎng)卡!
為什么不是直接拷貝昔馋,而是多了一步記錄文件信息的步驟呢芜繁?因為相比于內(nèi)核空間,網(wǎng)卡的讀取速率實在是太慢了绒极,這一步如果由CPU來操作的話,會嚴(yán)重拉低CPU的運(yùn)行速度蔬捷,所以要交給DMA來做垄提,但是因為是異步的,DMA引擎又不知道為這個Socket到底發(fā)送多少數(shù)據(jù)周拐,所以要在Socket上記錄文件起始量和數(shù)據(jù)長度铡俐,再由DMA引擎讀取這些文件信息,將文件發(fā)送只網(wǎng)卡數(shù)據(jù)妥粟!
mmap
我們通過Centos man page指令查看該函數(shù)的定義审丘!
名字
mmap, munmap -將文件或設(shè)備映射到內(nèi)存中
void *mmap(void *addr, size_t length, int prot, int flags, int fd, off_t offset); int munmap(void *addr, size_t length);
描述:
mmap()在調(diào)用進(jìn)程的虛擬地址空間中創(chuàng)建一個新的映射。新映射的起始地址在addr中指定勾给。length參數(shù)指定映射的長度,如果addr為空滩报,則內(nèi)核選擇創(chuàng)建映射的地址;這是創(chuàng)建新映射的最可移植的方法。如果addr不為空播急,則內(nèi)核將其作為提示!關(guān)于在哪里放置映射;在Linux上脓钾,映射將在附近的頁面邊界創(chuàng)建。新映射的地址作為調(diào)用的結(jié)果返回桩警。
mmap()系統(tǒng)調(diào)用使得進(jìn)程之間通過映射同一個普通文件實現(xiàn)共享內(nèi)存可训。普通文件被映射到進(jìn)程地址空間后,進(jìn)程可以像訪問普通內(nèi)存一樣對文件進(jìn)行訪問捶枢,不必再調(diào)用read()握截,write()等操作。
什么叫區(qū)域共享烂叔,這個不能被理解為我們的應(yīng)用程序就可以直接到內(nèi)核空間讀取數(shù)據(jù)了谨胞,而是我們在用戶空間里面再開辟一個空間,將內(nèi)核空間的數(shù)據(jù)的起始以及偏移量映射到用戶空間长已!簡單點說 也就是用戶空間的內(nèi)存畜眨,持有對內(nèi)核空間這一段內(nèi)存區(qū)域的引用!這樣用戶空間在操作讀取到的數(shù)據(jù)的時候术瓮,就可以像直接操作自己空間下的數(shù)據(jù)一樣操作內(nèi)核空間的數(shù)據(jù)康聂!
- 用戶程序發(fā)起read請求,然后上下文由用戶態(tài)切換至內(nèi)核態(tài)胞四!
- cpu通知DMA恬汁,由DMA引擎異步將數(shù)據(jù)讀取至內(nèi)核區(qū)域,同時在用戶空間建立地址映射辜伟!
- 上下文由內(nèi)核態(tài)切換至用戶態(tài)
- 發(fā)起write請求氓侧,上下文由用戶態(tài)切換至內(nèi)核態(tài)脊另!
- CPU通知DMA引擎將數(shù)據(jù)拷貝至Socket緩存!程序切換至用戶態(tài)约巷!
- DMA引擎異步將數(shù)據(jù)拷貝至網(wǎng)卡偎痛!
很明白的發(fā)現(xiàn)mmap函數(shù)在read數(shù)據(jù)的時候,少了異步由內(nèi)核空間到用戶空間的數(shù)據(jù)復(fù)制独郎,而是直接建立一個映射關(guān)系踩麦,操作的時候,直接操作映射數(shù)據(jù)氓癌,但是上下文的切換沒有變谓谦!
mmap所建立的虛擬空間,空間量事實上可以遠(yuǎn)大于物理內(nèi)存空間贪婉,假設(shè)我們想虛擬內(nèi)存空間中寫入數(shù)據(jù)的時候反粥,超過物理內(nèi)存時,操作系統(tǒng)會進(jìn)行頁置換疲迂,根據(jù)淘汰算法才顿,將需要淘汰的頁置換成所需的新頁,所以mmap對應(yīng)的內(nèi)存是可以被淘汰的(若內(nèi)存頁是"臟"的尤蒿,則操作系統(tǒng)會先將數(shù)據(jù)回寫磁盤再淘汰)娜膘。這樣,就算mmap的數(shù)據(jù)遠(yuǎn)大于物理內(nèi)存优质,操作系統(tǒng)也能很好地處理竣贪,不會產(chǎn)生功能上的問題。
sendfile: 只經(jīng)歷兩次上線文的切換和兩次數(shù)據(jù)拷貝巩螃,但是缺點也顯而易見演怎,你無法對數(shù)據(jù)進(jìn)行修改操作!適合大文件的數(shù)據(jù)傳輸避乏!而且是沒有沒有修改數(shù)據(jù)的需求爷耀!
mmap: 經(jīng)歷4次上下文的切換、三次數(shù)據(jù)拷貝拍皮,但是用戶操作讀取來的數(shù)據(jù)歹叮,異常簡單!適合小文件的讀寫和傳輸铆帽!
nio的堆外內(nèi)存
堆外內(nèi)存的實現(xiàn)類是DirectByteBuffer
, 我們查看SocketChannel再向通道寫入數(shù)據(jù)的時候的代碼:
這段代碼是當(dāng)你調(diào)用SocketChannel.write的時候的源代碼咆耿,我們從代碼中可以得知,無論你是否使用的是不是堆外內(nèi)存爹橱,在內(nèi)部NIO都會將其轉(zhuǎn)換為堆外內(nèi)存萨螺,然后在進(jìn)行后續(xù)操作,那么堆外內(nèi)存究竟有何種魔力呢?
何為堆外內(nèi)存慰技,要知道我們的JAVA代碼運(yùn)行在了JVM容器里面椭盏,我們又叫做Java虛擬機(jī),java開發(fā)者為了方便內(nèi)存管理和內(nèi)存分配吻商,將JVM的空間與操作系統(tǒng)的空間隔離了起來掏颊,市面上所有的VM程序都是這樣做的,VM程序的空間結(jié)構(gòu)和操作系統(tǒng)的空間結(jié)構(gòu)是不一樣的艾帐,所以java程序無法直接的將數(shù)據(jù)寫出去蚯舱,必須先將數(shù)據(jù)拷貝到C的堆內(nèi)存上也就是常說的堆外內(nèi)存,然后在進(jìn)行后續(xù)的讀寫掩蛤,在NIO中直接使用堆外內(nèi)存可以省去JVM內(nèi)部數(shù)據(jù)向本次內(nèi)存空間拷貝的步驟,加快處理速度陈肛!
而且NIO中每次寫入寫出不在是以一個一個的字節(jié)寫出揍鸟,而是用了一個Buffer內(nèi)存塊的方式寫出,也就是說只需要告訴CPU 我這個數(shù)據(jù)塊的數(shù)據(jù)開始的索引以及數(shù)據(jù)偏移量就可以直接讀取句旱,但是JVM通過垃圾回收的時候阳藻,通過會做垃圾拷貝整理,這個時候會移動內(nèi)存谈撒,這個時候如果內(nèi)存地址改變腥泥,就勢必會出現(xiàn)問題,所以我們要想一個辦法啃匿,讓JVM垃圾回收不影響這個數(shù)據(jù)塊蛔外!
總結(jié)來說:它可以使用Native 函數(shù)庫直接分配堆外內(nèi)存,然后通過一個存儲在Java 堆里面的DirectByteBuffer 對象作為這塊內(nèi)存的引用進(jìn)行操作溯乒。這樣能在一些場景中顯著提高性能夹厌,因為避免了在Java 堆和Native 堆中來回復(fù)制數(shù)據(jù)。
能夠避免JVM垃圾回收過程中做內(nèi)存整理裆悄,所產(chǎn)生的的問題矛纹,當(dāng)數(shù)據(jù)產(chǎn)生在JVM內(nèi)部的時候,JVM的垃圾回收就無法影響這部分?jǐn)?shù)據(jù)了光稼,而且能夠變相的減輕JVM垃圾回收的壓力或南!因為不用再管理這一部分?jǐn)?shù)據(jù)了!
他的內(nèi)存結(jié)構(gòu)看起來像這樣:
為什么DirectByteBuffer
就能夠直接操作JVM外的內(nèi)存呢艾君?我們看下他的源碼實現(xiàn):
DirectByteBuffer(int cap) {
.....忽略....
try {
//分配內(nèi)存
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
....忽略....
}
....忽略....
if (pa && (base % ps != 0)) {
//對齊page 計算地址并保存
address = base + ps - (base & (ps - 1));
} else {
//計算地址并保存
address = base;
}
//釋放內(nèi)存的回調(diào)
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
....忽略..
}
我們主要關(guān)注:unsafe.allocateMemory(size);
public native long allocateMemory(long var1);
我們可以看到他調(diào)用的是 native方法采够,這種方法通常由C++實現(xiàn),是直接操作內(nèi)存空間的冰垄,這個是被jdk進(jìn)行安全保護(hù)的操作吁恍,也就是說你通過Unsafe.getUnsafe()
是獲取不到的,必須通過反射,具體的實現(xiàn)冀瓦,自行翻閱瀏覽器伴奥!
如此NIO就可以通過本地方法去操作JVM外的內(nèi)存,但是大家有沒有發(fā)現(xiàn)一點問題翼闽,我們現(xiàn)在是能夠讓操作系統(tǒng)直接讀取數(shù)據(jù)了拾徙,而且也能夠避免垃圾回收所帶來的影響了還能減輕垃圾回收的壓力,可謂是一舉三得感局,但是大家有沒有考慮過一個問題尼啡,這部分空間不經(jīng)過垃JVM管理了,他該什么時候釋放呢询微?JVM都管理不了了崖瞭,那么堆外內(nèi)存勢必會導(dǎo)致OOM的出現(xiàn),所以撑毛,我們必須要去手動的釋放這個內(nèi)存书聚,但是手動釋放對于編程復(fù)雜度難度太大,所以藻雌,JVM對堆外內(nèi)存的管理也做了一部分優(yōu)化雌续,首先我們先看一下上述DirectByteBuffer中的cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
,這個對象,他主要用于堆外內(nèi)存空間的釋放胯杭;
public class Cleaner extends PhantomReference<Object> {....}
虛引用
Cleaner繼承了一個PhantomReference驯杜,這代表著Cleaner是一個虛引用,有關(guān)強(qiáng)軟弱虛引用的使用做个,請大家自行百度鸽心,Netty更新完成之后,我會寫一篇文章做單獨(dú)的介紹居暖,這里就不一一介紹了再悼,這里直接說PhantomReference虛引用:
public class PhantomReference<T> extends Reference<T> {
public T get() {
return null;
}
public PhantomReference(T referent, ReferenceQueue<? super T> q) {
super(referent, q);
}
}
虛引用的構(gòu)造函數(shù)中要求必須傳遞的兩個參數(shù),被引用對象膝但、引用隊列冲九!
這兩個參數(shù)的用意是什么呢,看個圖
JVM中判斷一個對象是否需要回收跟束,一般都是使用可達(dá)性分析算法莺奸,什么是可達(dá)性分析呢?就是從所謂的方法區(qū)冀宴、椕鸫空間中找到被標(biāo)記為root的節(jié)點,然后沿著root節(jié)點向下找略贮,被找到的都任務(wù)是存活對象甚疟,當(dāng)所有的root節(jié)點尋找完畢后仗岖,剩余的節(jié)點也就被認(rèn)為是垃圾對象;
依據(jù)上圖览妖,我們明顯發(fā)現(xiàn)椩簦空間中持有對direct的引用,我們將該對象傳遞給弱引用和讽膏,弱引用也持有該對象檩电,現(xiàn)在相當(dāng)于direct引用和ref引用同時引用堆空間中的一塊數(shù)據(jù),當(dāng)direct使用完畢后府树,該引用斷開:
JVM通過可待性分析算法俐末,發(fā)現(xiàn)除了 ref引用之外,其余的沒有人引用他奄侠,因為ref是虛引用卓箫,所以本次垃圾回收一定會回收它,回收的時候垄潮,做了一件什么事呢烹卒?
我們在創(chuàng)建這個虛引用的時候傳入了一個隊列,在這個對象被回收的時候魂挂,被引用的對象會進(jìn)入到這個回調(diào)!
public class MyPhantomReference {
static ReferenceQueue<Object> queue = new ReferenceQueue<>();
public static void main(String[] args) throws InterruptedException {
byte[] bytes = new byte[10 * 1024];
//將該對象被虛引用引用
PhantomReference<Object> objectPhantomReference = new PhantomReference<Object>(bytes,queue);
//這個一定返回null 因為實在接口定義中寫死的
System.out.println(objectPhantomReference.get());
//此時jvm并沒有進(jìn)行對象的回收馁筐,該隊列返回為空
System.out.println(queue.poll());
//手動釋放該引用涂召,將該引用置為無效引用
bytes = null;
//觸發(fā)gc
System.gc();
//這里返回的還是null 接口定義中寫死的
System.out.println(objectPhantomReference.get());
//垃圾回收后,被回收對象進(jìn)入到引用隊列
System.out.println(queue.poll());
}
}
基本了解了虛引用之后敏沉,我們再來看DirectByteBuffer
對象果正,他在構(gòu)造函數(shù)創(chuàng)建的時候引用看一個虛引用Cleaner
!當(dāng)這個DirectByteBuffer使用完畢后盟迟,DirectByteBuffer被JVM回收秋泳,觸發(fā)Cleaner虛引用!JVM垃圾線程會將這個對象綁定到Reference
對象中的pending
屬性中攒菠,程序啟動后引用類Reference
類會創(chuàng)建一條守護(hù)線程:
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
Thread handler = new ReferenceHandler(tg, "Reference Handler");
//設(shè)置優(yōu)先級為系統(tǒng)最高優(yōu)先級
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();
//.......................
}
我們看一下該線程的定義:
static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
synchronized (lock) {
if (pending != null) {
//......忽略
c = r instanceof Cleaner ? (Cleaner) r : null;
pending = r.discovered;
r.discovered = null;
} else {
//隊列中沒有數(shù)據(jù)結(jié)阻塞 RefQueue入隊邏輯中有NF操作迫皱,感興趣可以自己去看下
if (waitForNotify) {
lock.wait();
}
// retry if waited
return waitForNotify;
}
}
} catch (OutOfMemoryError x) {
//發(fā)生OOM之后就讓出線程的使用權(quán),看能不能內(nèi)部消化這個OOM
Thread.yield();
return true;
} catch (InterruptedException x) {
// 線程中斷的話就直接返回
return true;
}
// 這里是關(guān)鍵辖众,如果虛引用是一個 cleaner對象卓起,就直接進(jìn)行清空操作,不在入隊
if (c != null) {
//TODO 重點關(guān)注
c.clean();
return true;
}
//如果不是 cleaner對象凹炸,就將該引用入隊
ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
那我們此時就應(yīng)該重點關(guān)注c.clean();方法了戏阅!
this.thunk.run();
重點關(guān)注這個,thunk是一個什么對象啤它? 我們需要重新回到 DirectByteBuffer創(chuàng)建的時候奕筐,看看他傳遞的是什么舱痘。
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
我們可以看到,傳入的是一個 Deallocator
對象离赫,那么他所調(diào)用的run方法芭逝,我們看下邏輯:
public void run() {
if (address == 0) {
// Paranoia
return;
}
//釋放內(nèi)存
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
重點關(guān)注unsafe.freeMemory(address);這個就是釋放內(nèi)存的!
至此笆怠,我們知道了JVM是如何管理堆外內(nèi)存的了铝耻!
公眾號:源碼學(xué)徒