1. 深入分析NIO的零拷貝

        共 16257字,需瀏覽 33分鐘

         ·

        2021-06-28 09:42

        有道無(wú)術(shù),術(shù)尚可求也!有術(shù)無(wú)道,止于術(shù)!

        本章還是關(guān)于NIO的概念鋪底,有關(guān)NIO相關(guān)的代碼,我還是希望大家閑余時(shí)間取網(wǎng)上找一下有關(guān)使用JDK NIO開(kāi)發(fā)服務(wù)端、客戶(hù)端的代碼,我會(huì)取寫(xiě)這些,但是具體的代碼我不會(huì)很詳細(xì)的取介紹,下一章的話(huà)可能就要上代碼了,具體的規(guī)劃如下:

        講一下NIO基礎(chǔ)API的使用、分析Netty的核心思想,使用Reactor模式仿寫(xiě)一個(gè)多線(xiàn)程版的Nio程序、再然后就是關(guān)于Netty的源碼分析了!

        回歸正題,NIO的高性能除了體現(xiàn)在Epoll模型之外,還有很重要的一點(diǎn),就是零拷貝!首先大家要先明白一點(diǎn),所謂的0拷貝,并不是一次拷貝都沒(méi)有,而是數(shù)據(jù)由內(nèi)核空間向用戶(hù)空間的相互拷貝被取消了,所以稱(chēng)之為零拷貝!

        系統(tǒng)如何操作底層數(shù)據(jù)文件

        在了解整個(gè)IO的讀寫(xiě)的過(guò)程中,我們需要知道我們的應(yīng)用程序是如何操作一些內(nèi)存、磁盤(pán)數(shù)據(jù)的!

        我們?cè)陂_(kāi)發(fā)中,假設(shè)要向硬盤(pán)中寫(xiě)入一段文本數(shù)據(jù),我們并不需要操作太多的細(xì)節(jié),而是只需要簡(jiǎn)單的將數(shù)據(jù)轉(zhuǎn)為字節(jié)然后在告訴程序,我們要寫(xiě)入的位置以及名稱(chēng)就可以了,為什么這么簡(jiǎn)單呢?因?yàn)椴僮飨到y(tǒng)全部幫我們開(kāi)發(fā)好了,我們只需要調(diào)用就可以了,但是我們想一下,如果我們的操作系統(tǒng)的全部權(quán)限,包括內(nèi)存都可以讓用戶(hù)隨意操作那是一個(gè)很危險(xiǎn)的事情,例如某些病毒可以隨意篡改內(nèi)存中的數(shù)據(jù),以達(dá)到某些不軌的目的,那就很難受了!所以,我們的操作系統(tǒng)就必須對(duì)這些底層的API進(jìn)行一些限制和保護(hù)!

        但是如何保護(hù)呢?一方面,我們希望外部系統(tǒng)能夠調(diào)用我的系統(tǒng)API,另一方面我又不想外部隨意訪問(wèn)我的API怎么辦呢? 此時(shí),我們就要引申出來(lái)一個(gè)組件叫做kernel,你可以把它理解為一段程序,他在機(jī)器啟動(dòng)的時(shí)候被加載進(jìn)來(lái),被用于管理系統(tǒng)底層的一些設(shè)備,例如硬盤(pán)、內(nèi)存、網(wǎng)卡等硬件設(shè)備!當(dāng)我們又了kernel之后,會(huì)發(fā)生什么呢?

        我們還是以寫(xiě)出文件為例,當(dāng)我們調(diào)用了一個(gè)write api的時(shí)候,他會(huì)將write的方法名以及參數(shù)加載到CPU的寄存器中,同時(shí)執(zhí)行一個(gè)指令叫做  int 0x80的指令,int 0x80是 interrupt 128(0x80的10進(jìn)制)的縮寫(xiě),我們一般叫80中斷,當(dāng)調(diào)用了這個(gè)指令之后,CUP會(huì)停止當(dāng)前的調(diào)度,保存當(dāng)前的執(zhí)行中的線(xiàn)程的狀態(tài),然后在中斷向量表中尋找 128代表的回調(diào)函數(shù),將之前寫(xiě)到寄存器中的數(shù)據(jù)(write /參數(shù))當(dāng)作參數(shù),傳遞到這個(gè)回調(diào)函數(shù)中,由這個(gè)回調(diào)函數(shù)去尋找對(duì)應(yīng)的系統(tǒng)函數(shù)write進(jìn)行寫(xiě)出操作!

        大家回想一下,當(dāng)系統(tǒng)發(fā)起一個(gè)調(diào)用后不再是用戶(hù)程序直接調(diào)用系統(tǒng)API的而是切換成內(nèi)核調(diào)用這些API,所以?xún)?nèi)核是以這種方式來(lái)保護(hù)系統(tǒng)的而且這也就是 用戶(hù)態(tài)切換到內(nèi)核態(tài)!

        傳統(tǒng)的I/O讀寫(xiě)

        場(chǎng)景:讀取一個(gè)圖片通過(guò)socket傳輸?shù)娇蛻?hù)端展示。

        image-20210314222351485
        1. 程序發(fā)起read請(qǐng)求,調(diào)用系統(tǒng)read api由用戶(hù)態(tài)切換至內(nèi)核態(tài)!
        2. CPU通過(guò)DMA引擎將磁盤(pán)數(shù)據(jù)加載到內(nèi)核緩沖區(qū),觸發(fā)中止指令,CPU將內(nèi)核緩沖區(qū)的數(shù)據(jù)拷貝到用戶(hù)空間!由內(nèi)核態(tài)切換至用戶(hù)態(tài)!
        3. 程序 發(fā)起write調(diào)用,調(diào)用系統(tǒng)API,由用戶(hù)態(tài)切換只內(nèi)核態(tài),CPU將用戶(hù)空間的數(shù)據(jù)拷貝到Socket緩沖區(qū)!再由內(nèi)核態(tài)切換至用戶(hù)態(tài)!
        4. DMA引擎異步將Socket緩沖區(qū)拷貝到網(wǎng)卡通過(guò)底層協(xié)議棧發(fā)送至對(duì)端!

        我們可以了解一下,這當(dāng)中發(fā)生了4次上下文的切換和4次數(shù)據(jù)拷貝!我們大致分析一下,那些數(shù)據(jù)拷貝是多余的:

        • 磁盤(pán)文件拷貝到內(nèi)核緩沖區(qū)是必須的不能省略,因?yàn)檫@個(gè)數(shù)據(jù)總歸要讀取出來(lái)的!
        • 內(nèi)核空間拷貝到用戶(hù)空間,如果我們不準(zhǔn)備對(duì)數(shù)據(jù)做修改的話(huà),好像沒(méi)有必要呀,直接拷貝到Socket緩沖區(qū)不就可以了!
        • Socket到網(wǎng)卡,好像也有點(diǎn)多余,為什么這么說(shuō)呢?因?yàn)槲覀冎苯訌膬?nèi)核空間里面直接懟到網(wǎng)卡里面,中間不就少了很多的拷貝和上下文的切換看嗎?

        sendfile

        我們通過(guò)Centos man page指令查看該函數(shù)的定義!

        也可以通過(guò)該鏈接下載:sendfile()函數(shù)介紹

        基本介紹:

        sendfile——在文件描述符之間傳輸數(shù)據(jù)

        描述

        ssize_t sendfile(int out_fd, int in_fd, off_t *offset, size_t count);

        sendfile()在一個(gè)文件描述符和另一個(gè)文件描述符之間復(fù)制數(shù)據(jù)。因?yàn)檫@種復(fù)制是在內(nèi)核中完成的,所以sendfile()比read(2)和write(2)的組合更高效,后者需要在用戶(hù)空間之間來(lái)回傳輸數(shù)據(jù)。

        in_fd應(yīng)該是打開(kāi)用于讀取的文件描述符,而out_fd應(yīng)該是打開(kāi)用于寫(xiě)入的文件描述符。

        如果offset不為NULL,則它指向一個(gè)保存文件偏移量的變量,sendfile()將從這個(gè)變量開(kāi)始從in_fd讀取數(shù)據(jù)。當(dāng)sendfile()返回時(shí),這個(gè)變量將被設(shè)置為最后一個(gè)被讀取字節(jié)后面的字節(jié)的偏移量。如果offset不為NULL,則sendfile()不會(huì)修改當(dāng)前值

        租用文件偏移in_fd;否則,將調(diào)整當(dāng)前文件偏移量以反映從in_fd讀取的字節(jié)數(shù)。

        如果offset為NULL,則從當(dāng)前文件偏移量開(kāi)始從in_fd讀取數(shù)據(jù),并通過(guò)調(diào)用更新文件偏移量。

        count是要在文件描述符之間復(fù)制的字節(jié)數(shù)。

        in_fd參數(shù)必須對(duì)應(yīng)于支持類(lèi)似mmap(2)的操作的文件(也就是說(shuō),它不能是套接字)。

        在2.6.33之前的Linux內(nèi)核中,out_fd必須引用一個(gè)套接字。從Linux 2.6.33開(kāi)始,它可以是任何文件。如果是一個(gè)常規(guī)文件,則sendfile()適當(dāng)?shù)馗奈募屏俊?/p>

        簡(jiǎn)單來(lái)說(shuō),sendfile函數(shù)可以將兩個(gè)文件描述符里面的數(shù)據(jù)來(lái)回復(fù)制,再Linux中萬(wàn)物皆文件!內(nèi)核空間和Socket也是一個(gè)個(gè)的對(duì)應(yīng)的文件,sendfile函數(shù)可以將兩個(gè)文件里面的數(shù)據(jù)來(lái)回傳輸,這也造就了,我們后面的零拷貝優(yōu)化!

        sendfile - linux2.4之前

        image-20210314230009773
        1. 用戶(hù)程序發(fā)起read請(qǐng)求,程序由用戶(hù)態(tài)切換至內(nèi)核態(tài)!
        2. DMA引擎將數(shù)據(jù)從磁盤(pán)拷貝出來(lái)到內(nèi)核空間!
        3. 調(diào)用sendfile函數(shù)將內(nèi)核空間的數(shù)據(jù)直接拷貝到Socket緩沖區(qū)!
        4. 上下文從內(nèi)核態(tài)切換至用戶(hù)態(tài)
        5. Socket緩沖區(qū)通過(guò)DMA引擎,將數(shù)據(jù)拷貝到網(wǎng)卡,通過(guò)底層協(xié)議棧發(fā)送到對(duì)端!

        這個(gè)優(yōu)化不可謂不狠,上下文切換次數(shù)變?yōu)閮纱?,?shù)據(jù)拷貝變?yōu)閮纱危@基本符合了我們上面的優(yōu)化要求,但是我們還是會(huì)發(fā)現(xiàn),從內(nèi)核空間到Socket緩沖區(qū),然后從內(nèi)核緩沖區(qū)到網(wǎng)卡似乎也有點(diǎn)雞肋,所以,Linux2.4之后再次進(jìn)行了優(yōu)化!

        sendfile - linux2.4之后

        image-20210314231519642
        1. 用戶(hù)程序發(fā)起read請(qǐng)求,程序由用戶(hù)態(tài)切換至內(nèi)核態(tài)!
        2. DMA引擎將數(shù)據(jù)從磁盤(pán)拷貝出來(lái)到內(nèi)核空間!
        3. 調(diào)用sendfile函數(shù)將內(nèi)核空間的數(shù)據(jù)再內(nèi)存中的起始位置和偏移量寫(xiě)入Socket緩沖區(qū)!然后內(nèi)核態(tài)切換至用戶(hù)態(tài)!
        4. DMA引擎讀取Socket緩沖區(qū)的內(nèi)存信息,直接由內(nèi)核空間拷貝至網(wǎng)卡!

        這里的優(yōu)化是原本將內(nèi)核空間的數(shù)據(jù)拷貝至Socket緩沖區(qū)的步驟,變成了只記錄文件的起始位置和偏移量!然后程序直接返回,由DMA引擎異步的將數(shù)據(jù)從內(nèi)核空間拷貝到網(wǎng)卡!

        為什么不是直接拷貝,而是多了一步記錄文件信息的步驟呢?因?yàn)橄啾扔趦?nèi)核空間,網(wǎng)卡的讀取速率實(shí)在是太慢了,這一步如果由CPU來(lái)操作的話(huà),會(huì)嚴(yán)重拉低CPU的運(yùn)行速度,所以要交給DMA來(lái)做,但是因?yàn)槭钱惒降?,DMA引擎又不知道為這個(gè)Socket到底發(fā)送多少數(shù)據(jù),所以要在Socket上記錄文件起始量和數(shù)據(jù)長(zhǎng)度,再由DMA引擎讀取這些文件信息,將文件發(fā)送只網(wǎng)卡數(shù)據(jù)!

        mmap

        我們通過(guò)Centos man page指令查看該函數(shù)的定義!

        mmap()函數(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)建一個(gè)新的映射。新映射的起始地址在addr中指定。length參數(shù)指定映射的長(zhǎng)度,如果addr為空,則內(nèi)核選擇創(chuàng)建映射的地址;這是創(chuàng)建新映射的最可移植的方法。如果addr不為空,則內(nèi)核將其作為提示!關(guān)于在哪里放置映射;在Linux上,映射將在附近的頁(yè)面邊界創(chuàng)建。新映射的地址作為調(diào)用的結(jié)果返回。

        mmap()系統(tǒng)調(diào)用使得進(jìn)程之間通過(guò)映射同一個(gè)普通文件實(shí)現(xiàn)共享內(nèi)存。普通文件被映射到進(jìn)程地址空間后,進(jìn)程可以像訪問(wèn)普通內(nèi)存一樣對(duì)文件進(jìn)行訪問(wèn),不必再調(diào)用read(),write()等操作。

        什么叫區(qū)域共享,這個(gè)不能被理解為我們的應(yīng)用程序就可以直接到內(nèi)核空間讀取數(shù)據(jù)了,而是我們?cè)谟脩?hù)空間里面再開(kāi)辟一個(gè)空間,將內(nèi)核空間的數(shù)據(jù)的起始以及偏移量映射到用戶(hù)空間!簡(jiǎn)單點(diǎn)說(shuō) **也就是用戶(hù)空間的內(nèi)存,持有對(duì)內(nèi)核空間這一段內(nèi)存區(qū)域的引用!**這樣用戶(hù)空間在操作讀取到的數(shù)據(jù)的時(shí)候,就可以像直接操作自己空間下的數(shù)據(jù)一樣操作內(nèi)核空間的數(shù)據(jù)!

        image-20210315092915111
        1. 用戶(hù)程序發(fā)起read請(qǐng)求,然后上下文由用戶(hù)態(tài)切換至內(nèi)核態(tài)!
        2. cpu通知DMA,由DMA引擎異步將數(shù)據(jù)讀取至內(nèi)核區(qū)域,同時(shí)在用戶(hù)空間建立地址映射!
        3. 上下文由內(nèi)核態(tài)切換至用戶(hù)態(tài)
        4. 發(fā)起write請(qǐng)求,上下文由用戶(hù)態(tài)切換至內(nèi)核態(tài)!
        5. CPU通知DMA引擎將數(shù)據(jù)拷貝至Socket緩存!程序切換至用戶(hù)態(tài)!
        6. DMA引擎異步將數(shù)據(jù)拷貝至網(wǎng)卡!

        很明白的發(fā)現(xiàn)mmap函數(shù)在read數(shù)據(jù)的時(shí)候,少了異步由內(nèi)核空間到用戶(hù)空間的數(shù)據(jù)復(fù)制,而是直接建立一個(gè)映射關(guān)系,操作的時(shí)候,直接操作映射數(shù)據(jù),但是上下文的切換沒(méi)有變!

        mmap所建立的虛擬空間,空間量事實(shí)上可以遠(yuǎn)大于物理內(nèi)存空間,假設(shè)我們想虛擬內(nèi)存空間中寫(xiě)入數(shù)據(jù)的時(shí)候,超過(guò)物理內(nèi)存時(shí),操作系統(tǒng)會(huì)進(jìn)行頁(yè)置換,根據(jù)淘汰算法,將需要淘汰的頁(yè)置換成所需的新頁(yè),所以mmap對(duì)應(yīng)的內(nèi)存是可以被淘汰的(若內(nèi)存頁(yè)是"臟"的,則操作系統(tǒng)會(huì)先將數(shù)據(jù)回寫(xiě)磁盤(pán)再淘汰)。這樣,就算mmap的數(shù)據(jù)遠(yuǎn)大于物理內(nèi)存,操作系統(tǒng)也能很好地處理,不會(huì)產(chǎn)生功能上的問(wèn)題。

        sendfile: 只經(jīng)歷兩次上線(xiàn)文的切換和兩次數(shù)據(jù)拷貝,但是缺點(diǎn)也顯而易見(jiàn),你無(wú)法對(duì)數(shù)據(jù)進(jìn)行修改操作!適合大文件的數(shù)據(jù)傳輸!而且是沒(méi)有沒(méi)有修改數(shù)據(jù)的需求!

        mmap: 經(jīng)歷4次上下文的切換、三次數(shù)據(jù)拷貝,但是用戶(hù)操作讀取來(lái)的數(shù)據(jù),異常簡(jiǎn)單!適合小文件的讀寫(xiě)和傳輸!

        nio的堆外內(nèi)存

        堆外內(nèi)存的實(shí)現(xiàn)類(lèi)是DirectByteBuffer, 我們查看SocketChannel再向通道寫(xiě)入數(shù)據(jù)的時(shí)候的代碼:

        image-20210315121518393

        這段代碼是當(dāng)你調(diào)用SocketChannel.write的時(shí)候的源代碼,我們從代碼中可以得知,無(wú)論你是否使用的是不是堆外內(nèi)存,在內(nèi)部NIO都會(huì)將其轉(zhuǎn)換為堆外內(nèi)存,然后在進(jìn)行后續(xù)操作,那么堆外內(nèi)存究竟有何種魔力呢?

        何為堆外內(nèi)存,要知道我們的JAVA代碼運(yùn)行在了JVM容器里面,我們又叫做Java虛擬機(jī),java開(kāi)發(fā)者為了方便內(nèi)存管理和內(nèi)存分配,將JVM的空間與操作系統(tǒng)的空間隔離了起來(lái),市面上所有的VM程序都是這樣做的,VM程序的空間結(jié)構(gòu)和操作系統(tǒng)的空間結(jié)構(gòu)是不一樣的,所以java程序無(wú)法直接的將數(shù)據(jù)寫(xiě)出去,必須先將數(shù)據(jù)拷貝到C的堆內(nèi)存上也就是常說(shuō)的堆外內(nèi)存,然后在進(jìn)行后續(xù)的讀寫(xiě),在NIO中直接使用堆外內(nèi)存可以省去JVM內(nèi)部數(shù)據(jù)向本次內(nèi)存空間拷貝的步驟,加快處理速度!

        而且NIO中每次寫(xiě)入寫(xiě)出不在是以一個(gè)一個(gè)的字節(jié)寫(xiě)出,而是用了一個(gè)Buffer內(nèi)存塊的方式寫(xiě)出,也就是說(shuō)只需要告訴CPU 我這個(gè)數(shù)據(jù)塊的數(shù)據(jù)開(kāi)始的索引以及數(shù)據(jù)偏移量就可以直接讀取,但是JVM通過(guò)垃圾回收的時(shí)候,通過(guò)會(huì)做垃圾拷貝整理,這個(gè)時(shí)候會(huì)移動(dòng)內(nèi)存,這個(gè)時(shí)候如果內(nèi)存地址改變,就勢(shì)必會(huì)出現(xiàn)問(wèn)題,所以我們要想一個(gè)辦法,讓JVM垃圾回收不影響這個(gè)數(shù)據(jù)塊!

        總結(jié)來(lái)說(shuō):它可以使用Native 函數(shù)庫(kù)直接分配堆外內(nèi)存,然后通過(guò)一個(gè)存儲(chǔ)在Java 堆里面的DirectByteBuffer 對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場(chǎng)景中顯著提高性能,因?yàn)楸苊饬嗽贘ava 堆和Native 堆中來(lái)回復(fù)制數(shù)據(jù)。

        能夠避免JVM垃圾回收過(guò)程中做內(nèi)存整理,所產(chǎn)生的的問(wèn)題,當(dāng)數(shù)據(jù)產(chǎn)生在JVM內(nèi)部的時(shí)候,JVM的垃圾回收就無(wú)法影響這部分?jǐn)?shù)據(jù)了,而且能夠變相的減輕JVM垃圾回收的壓力!因?yàn)椴挥迷俟芾磉@一部分?jǐn)?shù)據(jù)了!

        他的內(nèi)存結(jié)構(gòu)看起來(lái)像這樣:

        image-20210315125336866

        為什么DirectByteBuffer就能夠直接操作JVM外的內(nèi)存呢?我們看下他的源碼實(shí)現(xiàn):

        DirectByteBuffer(int cap) { 
          .....忽略....
                try {
                    //分配內(nèi)存
                    base = unsafe.allocateMemory(size);
                } catch (OutOfMemoryError x) {
                    ....忽略....
                }
                ....忽略....
                if (pa && (base % ps != 0)) {
                    //對(duì)齊page 計(jì)算地址并保存
                    address = base + ps - (base & (ps - 1));
                } else {
                    //計(jì)算地址并保存
                    address = base;
                }
                //釋放內(nèi)存的回調(diào)
                cleaner = Cleaner.create(thisnew Deallocator(base, size, cap));
                ....忽略..
            }

        我們主要關(guān)注:unsafe.allocateMemory(size);

        public native long allocateMemory(long var1);

        我們可以看到他調(diào)用的是 native方法,這種方法通常由C++實(shí)現(xiàn),是直接操作內(nèi)存空間的,這個(gè)是被jdk進(jìn)行安全保護(hù)的操作,也就是說(shuō)你通過(guò)Unsafe.getUnsafe()是獲取不到的,必須通過(guò)反射,具體的實(shí)現(xiàn),自行翻閱瀏覽器!

        如此NIO就可以通過(guò)本地方法去操作JVM外的內(nèi)存,但是大家有沒(méi)有發(fā)現(xiàn)一點(diǎn)問(wèn)題,我們現(xiàn)在是能夠讓操作系統(tǒng)直接讀取數(shù)據(jù)了,而且也能夠避免垃圾回收所帶來(lái)的影響了還能減輕垃圾回收的壓力,可謂是一舉三得,但是大家有沒(méi)有考慮過(guò)一個(gè)問(wèn)題,這部分空間不經(jīng)過(guò)垃JVM管理了,他該什么時(shí)候釋放呢?JVM都管理不了了,那么堆外內(nèi)存勢(shì)必會(huì)導(dǎo)致OOM的出現(xiàn),所以,我們必須要去手動(dòng)的釋放這個(gè)內(nèi)存,但是手動(dòng)釋放對(duì)于編程復(fù)雜度難度太大,所以,JVM對(duì)堆外內(nèi)存的管理也做了一部分優(yōu)化,首先我們先看一下上述DirectByteBuffer中的cleaner = Cleaner.create(this, new Deallocator(base, size, cap));,這個(gè)對(duì)象,他主要用于堆外內(nèi)存空間的釋放;

        public class Cleaner extends PhantomReference<Object{....}

        虛引用

        Cleaner繼承了一個(gè)PhantomReference,這代表著Cleaner是一個(gè)虛引用,有關(guān)強(qiáng)軟弱虛引用的使用,請(qǐng)大家自行百度,Netty更新完成之后,我會(huì)寫(xiě)一篇文章做單獨(dú)的介紹,這里就不一一介紹了,這里直接說(shuō)PhantomReference虛引用:

        public class PhantomReference<Textends Reference<T{
            public T get() {
                return null;
            }
            public PhantomReference(T referent, ReferenceQueue<? super T> q) {
                super(referent, q);
            }
        }

        虛引用的構(gòu)造函數(shù)中要求必須傳遞的兩個(gè)參數(shù),被引用對(duì)象、引用隊(duì)列!

        這兩個(gè)參數(shù)的用意是什么呢,看個(gè)圖

        image-20210315131401311

        JVM中判斷一個(gè)對(duì)象是否需要回收,一般都是使用可達(dá)性分析算法,什么是可達(dá)性分析呢?就是從所謂的方法區(qū)、棧空間中找到被標(biāo)記為root的節(jié)點(diǎn),然后沿著root節(jié)點(diǎn)向下找,被找到的都任務(wù)是存活對(duì)象,當(dāng)所有的root節(jié)點(diǎn)尋找完畢后,剩余的節(jié)點(diǎn)也就被認(rèn)為是垃圾對(duì)象;

        依據(jù)上圖,我們明顯發(fā)現(xiàn)??臻g中持有對(duì)direct的引用,我們將該對(duì)象傳遞給弱引用和,弱引用也持有該對(duì)象,現(xiàn)在相當(dāng)于direct引用和ref引用同時(shí)引用堆空間中的一塊數(shù)據(jù),當(dāng)direct使用完畢后,該引用斷開(kāi):

        image-20210315131857691

        JVM通過(guò)可待性分析算法,發(fā)現(xiàn)除了 ref引用之外,其余的沒(méi)有人引用他,因?yàn)閞ef是虛引用,所以本次垃圾回收一定會(huì)回收它,回收的時(shí)候,做了一件什么事呢?

        我們?cè)趧?chuàng)建這個(gè)虛引用的時(shí)候傳入了一個(gè)隊(duì)列,在這個(gè)對(duì)象被回收的時(shí)候,被引用的對(duì)象會(huì)進(jìn)入到這個(gè)回調(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];
                //將該對(duì)象被虛引用引用
                PhantomReference<Object> objectPhantomReference = new PhantomReference<Object>(bytes,queue);
                //這個(gè)一定返回null  因?yàn)閷?shí)在接口定義中寫(xiě)死的
                System.out.println(objectPhantomReference.get());
                //此時(shí)jvm并沒(méi)有進(jìn)行對(duì)象的回收,該隊(duì)列返回為空
                System.out.println(queue.poll());
                //手動(dòng)釋放該引用,將該引用置為無(wú)效引用
                bytes = null;
                //觸發(fā)gc
                System.gc();
                //這里返回的還是null  接口定義中寫(xiě)死的
                System.out.println(objectPhantomReference.get());
                //垃圾回收后,被回收對(duì)象進(jìn)入到引用隊(duì)列
                System.out.println(queue.poll());
            }
        }

        基本了解了虛引用之后,我們?cè)賮?lái)看DirectByteBuffer對(duì)象,他在構(gòu)造函數(shù)創(chuàng)建的時(shí)候引用看一個(gè)虛引用Cleaner!當(dāng)這個(gè)DirectByteBuffer使用完畢后,DirectByteBuffer被JVM回收,觸發(fā)Cleaner虛引用!JVM垃圾線(xiàn)程會(huì)將這個(gè)對(duì)象綁定到Reference對(duì)象中的pending屬性中,程序啟動(dòng)后引用類(lèi)Reference類(lèi)會(huì)創(chuàng)建一條守護(hù)線(xiàn)程:

        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)先級(jí)為系統(tǒng)最高優(yōu)先級(jí)
                handler.setPriority(Thread.MAX_PRIORITY);
                handler.setDaemon(true);
                handler.start();
          //.......................
            }

        我們看一下該線(xiàn)程的定義:

        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 {
                            //隊(duì)列中沒(méi)有數(shù)據(jù)結(jié)阻塞  RefQueue入隊(duì)邏輯中有NF操作,感興趣可以自己去看下
                            if (waitForNotify) {
                                lock.wait();
                            }
                            // retry if waited
                            return waitForNotify;
                        }
                    }
                } catch (OutOfMemoryError x) {
                    //發(fā)生OOM之后就讓出線(xiàn)程的使用權(quán),看能不能內(nèi)部消化這個(gè)OOM
                    Thread.yield();
                    return true;
                } catch (InterruptedException x) {
                    // 線(xiàn)程中斷的話(huà)就直接返回
                    return true;
                }

                // 這里是關(guān)鍵,如果虛引用是一個(gè) cleaner對(duì)象,就直接進(jìn)行清空操作,不在入隊(duì)
                if (c != null) {
                    //TODO 重點(diǎn)關(guān)注
                    c.clean();
                    return true;
                }
          //如果不是 cleaner對(duì)象,就將該引用入隊(duì)
                ReferenceQueue<? super Object> q = r.queue;
                if (q != ReferenceQueue.NULL) q.enqueue(r);
                return true;
            }

        那我們此時(shí)就應(yīng)該重點(diǎn)關(guān)注**c.clean();**方法了!

        this.thunk.run();

        重點(diǎn)關(guān)注這個(gè),thunk是一個(gè)什么對(duì)象?我們需要重新回到 DirectByteBuffer創(chuàng)建的時(shí)候,看看他傳遞的是什么。

         cleaner = Cleaner.create(thisnew Deallocator(base, size, cap));

        我們可以看到,傳入的是一個(gè) Deallocator對(duì)象,那么他所調(diào)用的run方法,我們看下邏輯:

        public void run() {
            if (address == 0) {
                // Paranoia
                return;
            }
            //釋放內(nèi)存
            unsafe.freeMemory(address);
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }

        重點(diǎn)關(guān)注**unsafe.freeMemory(address);**這個(gè)就是釋放內(nèi)存的!

        至此,我們知道了JVM是如何管理堆外內(nèi)存的了!

        image-20210315143654610

        才疏學(xué)淺,如果文章中理解有誤,歡迎大佬們私聊指正!歡迎關(guān)注作者的公眾號(hào),一起進(jìn)步,一起學(xué)習(xí)!



        ??「轉(zhuǎn)發(fā)」「在看」,是對(duì)我最大的支持??



        瀏覽 58
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
        評(píng)論
        圖片
        表情
        推薦
        點(diǎn)贊
        評(píng)論
        收藏
        分享

        手機(jī)掃一掃分享

        分享
        舉報(bào)
          
          

            1. 国产视频一区二区在线观看 | 久久久久久亚洲综合影院红桃 | 欧美在线色图 | 免费看国产曰批40分钟 | 操逼视频在线观看 |