溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

Java網(wǎng)絡編程中如何淺析mmap和Direct Buffer

發(fā)布時間:2021-12-20 15:26:40 來源:億速云 閱讀:142 作者:柒染 欄目:編程語言

本篇文章為大家展示了Java網(wǎng)絡編程中如何淺析mmap和Direct Buffer,內(nèi)容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細介紹希望你能有所收獲。

mmap基礎概念

mmap是一種內(nèi)存映射文件的方法,即將一個文件或者其它對象映射到進程的地址空間,實現(xiàn)文件磁盤地址和進程虛擬地址空間中一段虛擬地址的一一對映關系。實現(xiàn)這樣的映射關系后,進程就可以采用指針的方式讀寫操作這一段內(nèi)存,而系統(tǒng)會自動回寫臟頁面到對應的文件磁盤上,即完成了對文件的操作而不必再調(diào)用read,write等系統(tǒng)調(diào)用函數(shù)。相反,內(nèi)核空間對這段區(qū)域的修改也直接反映用戶空間,從而可以實現(xiàn)不同進程間的文件共享。如下圖所示:

      ![](https://images0.cnblogs.com/blog2015/571793/201507/200501092691998.png)

由上圖可以看出,進程的虛擬地址空間,由多個虛擬內(nèi)存區(qū)域構成。虛擬內(nèi)存區(qū)域是進程的虛擬地址空間中的一個同質(zhì)區(qū)間,即具有同樣特性的連續(xù)地址范圍。上圖中所示的text數(shù)據(jù)段(代碼段)、初始數(shù)據(jù)段、BSS數(shù)據(jù)段、堆、棧和內(nèi)存映射,都是一個獨立的虛擬內(nèi)存區(qū)域。而為內(nèi)存映射服務的地址空間處在堆棧之間的空余部分。

linux內(nèi)核使用vm_area_struct結構來表示一個獨立的虛擬內(nèi)存區(qū)域,由于每個不同質(zhì)的虛擬內(nèi)存區(qū)域功能和內(nèi)部機制都不同,因此一個進程使用多個vm_area_struct結構來分別表示不同類型的虛擬內(nèi)存區(qū)域。各個vm_area_struct結構使用鏈表或者樹形結構鏈接,方便進程快速訪問,如下圖所示:

     ![](https://images0.cnblogs.com/blog2015/571793/201507/200501434261629.png)

vm_area_struct結構中包含區(qū)域起始和終止地址以及其他相關信息,同時也包含一個vm_ops指針,其內(nèi)部可引出所有針對這個區(qū)域可以使用的系統(tǒng)調(diào)用函數(shù)。這樣,進程對某一虛擬內(nèi)存區(qū)域的任何操作需要用要的信息,都可以從vm_area_struct中獲得。mmap函數(shù)就是要創(chuàng)建一個新的vm_area_struct結構,并將其與文件的物理磁盤地址相連。具體步驟請看下一節(jié)。

回到頂部

mmap內(nèi)存映射原理

mmap內(nèi)存映射的實現(xiàn)過程,總的來說可以分為三個階段:

(一)進程啟動映射過程,并在虛擬地址空間中為映射創(chuàng)建虛擬映射區(qū)域

1、進程在用戶空間調(diào)用庫函數(shù)mmap,原型:void mmap(void start, size_t length, int prot, int flags, int fd, off_t offset);

2、在當前進程的虛擬地址空間中,尋找一段空閑的滿足要求的連續(xù)的虛擬地址

3、為此虛擬區(qū)分配一個vm_area_struct結構,接著對這個結構的各個域進行了初始化

4、將新建的虛擬區(qū)結構(vm_area_struct)插入進程的虛擬地址區(qū)域鏈表或樹中

(二)調(diào)用內(nèi)核空間的系統(tǒng)調(diào)用函數(shù)mmap(不同于用戶空間函數(shù)),實現(xiàn)文件物理地址和進程虛擬地址的一一映射關系

5、為映射分配了新的虛擬地址區(qū)域后,通過待映射的文件指針,在文件描述符表中找到對應的文件描述符,通過文件描述符,鏈接到內(nèi)核“已打開文件集”中該文件的文件結構體(struct file),每個文件結構體維護著和這個已打開文件相關各項信息。

6、通過該文件的文件結構體,鏈接到file_operations模塊,調(diào)用內(nèi)核函數(shù)mmap,其原型為:int mmap(struct file filp, struct vm_area_struct vma),不同于用戶空間庫函數(shù)。

7、內(nèi)核mmap函數(shù)通過虛擬文件系統(tǒng)inode模塊定位到文件磁盤物理地址。

8、通過remap_pfn_range函數(shù)建立頁表,即實現(xiàn)了文件地址和虛擬地址區(qū)域的映射關系。此時,這片虛擬地址并沒有任何數(shù)據(jù)關聯(lián)到主存中。

(三)進程發(fā)起對這片映射空間的訪問,引發(fā)缺頁異常,實現(xiàn)文件內(nèi)容到物理內(nèi)存(主存)的拷貝

注:前兩個階段僅在于創(chuàng)建虛擬區(qū)間并完成地址映射,但是并沒有將任何文件數(shù)據(jù)的拷貝至主存。真正的文件讀取是當進程發(fā)起讀或寫操作時。

9、進程的讀或寫操作訪問虛擬地址空間這一段映射地址,通過查詢頁表,發(fā)現(xiàn)這一段地址并不在物理頁面上。因為目前只建立了地址映射,真正的硬盤數(shù)據(jù)還沒有拷貝到內(nèi)存中,因此引發(fā)缺頁異常。

10、缺頁異常進行一系列判斷,確定無非法操作后,內(nèi)核發(fā)起請求調(diào)頁過程。

11、調(diào)頁過程先在交換緩存空間(swap cache)中尋找需要訪問的內(nèi)存頁,如果沒有則調(diào)用nopage函數(shù)把所缺的頁從磁盤裝入到主存中。

12、之后進程即可對這片主存進行讀或者寫的操作,如果寫操作改變了其內(nèi)容,一定時間后系統(tǒng)會自動回寫臟頁面到對應磁盤地址,也即完成了寫入到文件的過程。

注:修改過的臟頁面并不會立即更新回文件中,而是有一段時間的延遲,可以調(diào)用msync()來強制同步, 這樣所寫的內(nèi)容就能立即保存到文件里了。

回到頂部

mmap和常規(guī)文件操作的區(qū)別

對linux文件系統(tǒng)不了解的朋友,請參閱我之前寫的博文《 從內(nèi)核文件系統(tǒng)看文件讀寫過程》,我們首先簡單的回顧一下常規(guī)文件系統(tǒng)操作(調(diào)用read/fread等類函數(shù))中,函數(shù)的調(diào)用過程:

1、進程發(fā)起讀文件請求。

2、內(nèi)核通過查找進程文件符表,定位到內(nèi)核已打開文件集上的文件信息,從而找到此文件的inode。

3、inode在address_space上查找要請求的文件頁是否已經(jīng)緩存在頁緩存中。如果存在,則直接返回這片文件頁的內(nèi)容。

4、如果不存在,則通過inode定位到文件磁盤地址,將數(shù)據(jù)從磁盤復制到頁緩存。之后再次發(fā)起讀頁面過程,進而將頁緩存中的數(shù)據(jù)發(fā)給用戶進程。

總結來說,常規(guī)文件操作為了提高讀寫效率和保護磁盤,使用了頁緩存機制。這樣造成讀文件時需要先將文件頁從磁盤拷貝到頁緩存中,由于頁緩存處在內(nèi)核空間,不能被用戶進程直接尋址,所以還需要將頁緩存中數(shù)據(jù)頁再次拷貝到內(nèi)存對應的用戶空間中。這樣,通過了兩次數(shù)據(jù)拷貝過程,才能完成進程對文件內(nèi)容的獲取任務。寫操作也是一樣,待寫入的buffer在內(nèi)核空間不能直接訪問,必須要先拷貝至內(nèi)核空間對應的主存,再寫回磁盤中(延遲寫回),也是需要兩次數(shù)據(jù)拷貝。

而使用mmap操作文件中,創(chuàng)建新的虛擬內(nèi)存區(qū)域和建立文件磁盤地址和虛擬內(nèi)存區(qū)域映射這兩步,沒有任何文件拷貝操作。而之后訪問數(shù)據(jù)時發(fā)現(xiàn)內(nèi)存中并無數(shù)據(jù)而發(fā)起的缺頁異常過程,可以通過已經(jīng)建立好的映射關系,只使用一次數(shù)據(jù)拷貝,就從磁盤中將數(shù)據(jù)傳入內(nèi)存的用戶空間中,供進程使用。

總而言之,常規(guī)文件操作需要從磁盤到頁緩存再到用戶主存的兩次數(shù)據(jù)拷貝。而mmap操控文件,只需要從磁盤到用戶主存的一次數(shù)據(jù)拷貝過程。說白了,mmap的關鍵點是實現(xiàn)了用戶空間和內(nèi)核空間的數(shù)據(jù)直接交互而省去了空間不同數(shù)據(jù)不通的繁瑣過程。因此mmap效率更高。

回到頂部

mmap優(yōu)點總結

由上文討論可知,mmap優(yōu)點共有一下幾點:

1、對文件的讀取操作跨過了頁緩存,減少了數(shù)據(jù)的拷貝次數(shù),用內(nèi)存讀寫取代I/O讀寫,提高了文件讀取效率。

2、實現(xiàn)了用戶空間和內(nèi)核空間的高效交互方式。兩空間的各自修改操作可以直接反映在映射的區(qū)域內(nèi),從而被對方空間及時捕捉。

3、提供進程間共享內(nèi)存及相互通信的方式。不管是父子進程還是無親緣關系的進程,都可以將自身用戶空間映射到同一個文件或匿名映射到同一片區(qū)域。從而通過各自對映射區(qū)域的改動,達到進程間通信和進程間共享的目的。

 同時,如果進程A和進程B都映射了區(qū)域C,當A第一次讀取C時通過缺頁從磁盤復制文件頁到內(nèi)存中;但當B再讀C的相同頁面時,雖然也會產(chǎn)生缺頁異常,但是不再需要從磁盤中復制文件過來,而可直接使用已經(jīng)保存在內(nèi)存中的文件數(shù)據(jù)。

4、可用于實現(xiàn)高效的大規(guī)模數(shù)據(jù)傳輸。內(nèi)存空間不足,是制約大數(shù)據(jù)操作的一個方面,解決方案往往是借助硬盤空間協(xié)助操作,補充內(nèi)存的不足。但是進一步會造成大量的文件I/O操作,極大影響效率。這個問題可以通過mmap映射很好的解決。換句話說,但凡是需要用磁盤空間代替內(nèi)存的時候,mmap都可以發(fā)揮其功效。

mmap使用細節(jié)

1、使用mmap需要注意的一個關鍵點是,mmap映射區(qū)域大小必須是物理頁大小(page_size)的整倍數(shù)(32位系統(tǒng)中通常是4k字節(jié))。原因是,內(nèi)存的最小粒度是頁,而進程虛擬地址空間和內(nèi)存的映射也是以頁為單位。為了匹配內(nèi)存的操作,mmap從磁盤到虛擬地址空間的映射也必須是頁。

2、內(nèi)核可以跟蹤被內(nèi)存映射的底層對象(文件)的大小,進程可以合法的訪問在當前文件大小以內(nèi)又在內(nèi)存映射區(qū)以內(nèi)的那些字節(jié)。也就是說,如果文件的大小一直在擴張,只要在映射區(qū)域范圍內(nèi)的數(shù)據(jù),進程都可以合法得到,這和映射建立時文件的大小無關。具體情形參見“情形三”。

3、映射建立之后,即使文件關閉,映射依然存在。因為映射的是磁盤的地址,不是文件本身,和文件句柄無關。同時可用于進程間通信的有效地址空間不完全受限于被映射文件的大小,因為是按頁映射。

在上面的知識前提下,我們下面看看如果大小不是頁的整倍數(shù)的具體情況:

情形一:一個文件的大小是5000字節(jié),mmap函數(shù)從一個文件的起始位置開始,映射5000字節(jié)到虛擬內(nèi)存中。

分析:因為單位物理頁面的大小是4096字節(jié),雖然被映射的文件只有5000字節(jié),但是對應到進程虛擬地址區(qū)域的大小需要滿足整頁大小,因此mmap函數(shù)執(zhí)行后,實際映射到虛擬內(nèi)存區(qū)域8192個 字節(jié),5000~8191的字節(jié)部分用零填充。映射后的對應關系如下圖所示:

           ![](https://images0.cnblogs.com/blog2015/571793/201507/200521495513717.png)

此時:

(1)讀/寫前5000個字節(jié)(0~4999),會返回操作文件內(nèi)容。

(2)讀字節(jié)5000~8191時,結果全為0。寫5000~8191時,進程不會報錯,但是所寫的內(nèi)容不會寫入原文件中 。

(3)讀/寫8192以外的磁盤部分,會返回一個SIGSECV錯誤。

情形二:一個文件的大小是5000字節(jié),mmap函數(shù)從一個文件的起始位置開始,映射15000字節(jié)到虛擬內(nèi)存中,即映射大小超過了原始文件的大小。

分析:由于文件的大小是5000字節(jié),和情形一一樣,其對應的兩個物理頁。那么這兩個物理頁都是合法可以讀寫的,只是超出5000的部分不會體現(xiàn)在原文件中。由于程序要求映射15000字節(jié),而文件只占兩個物理頁,因此8192字節(jié)~15000字節(jié)都不能讀寫,操作時會返回異常。如下圖所示:

             ![](https://images0.cnblogs.com/blog2015/571793/201507/200522381763096.png)

此時:

(1)進程可以正常讀/寫被映射的前5000字節(jié)(0~4999),寫操作的改動會在一定時間后反映在原文件中。

(2)對于5000~8191字節(jié),進程可以進行讀寫過程,不會報錯。但是內(nèi)容在寫入前均為0,另外,寫入后不會反映在文件中。

(3)對于8192~14999字節(jié),進程不能對其進行讀寫,會報SIGBUS錯誤。

(4)對于15000以外的字節(jié),進程不能對其讀寫,會引發(fā)SIGSEGV錯誤。

情形三:一個文件初始大小為0,使用mmap操作映射了1000*4K的大小,即1000個物理頁大約4M字節(jié)空間,mmap返回指針ptr。

分析:如果在映射建立之初,就對文件進行讀寫操作,由于文件大小為0,并沒有合法的物理頁對應,如同情形二一樣,會返回SIGBUS錯誤。

但是如果,每次操作ptr讀寫前,先增加文件的大小,那么ptr在文件大小內(nèi)部的操作就是合法的。例如,文件擴充4096字節(jié),ptr就能操作ptr ~ [ (char)ptr + 4095]的空間。只要文件擴充的范圍在1000個物理頁(映射范圍)內(nèi),ptr都可以對應操作相同的大小。

這樣,方便隨時擴充文件空間,隨時寫入文件,不造成空間浪費。

本文轉自: https://www.jianshu.com/p/007052ee3773

堆外內(nèi)存

堆外內(nèi)存是相對于堆內(nèi)內(nèi)存的一個概念。堆內(nèi)內(nèi)存是由JVM所管控的Java進程內(nèi)存,我們平時在Java中創(chuàng)建的對象都處于堆內(nèi)內(nèi)存中,并且它們遵循JVM的內(nèi)存管理機制,JVM會采用垃圾回收機制統(tǒng)一管理它們的內(nèi)存。那么堆外內(nèi)存就是存在于JVM管控之外的一塊內(nèi)存區(qū)域,因此它是不受JVM的管控。

在講解DirectByteBuffer之前,需要先簡單了解兩個知識點

java引用類型,因為DirectByteBuffer是通過虛引用(Phantom Reference)來實現(xiàn)堆外內(nèi)存的釋放的。

PhantomReference 是所有“弱引用”中最弱的引用類型。不同于軟引用和弱引用,虛引用無法通過 get() 方法來取得目標對象的強引用從而使用目標對象,觀察源碼可以發(fā)現(xiàn) get() 被重寫為永遠返回 null。
那虛引用到底有什么作用?其實虛引用主要被用來 跟蹤對象被垃圾回收的狀態(tài),通過查看引用隊列中是否包含對象所對應的虛引用來判斷它是否 即將被垃圾回收,從而采取行動。它并不被期待用來取得目標對象的引用,而目標對象被回收前,它的引用會被放入一個 ReferenceQueue 對象中,從而達到跟蹤對象垃圾回收的作用。
關于java引用類型的實現(xiàn)和原理可以閱讀之前的文章 Reference 、ReferenceQueue 詳解 和 Java 引用類型簡述

關于linux的內(nèi)核態(tài)和用戶態(tài)

Java網(wǎng)絡編程中如何淺析mmap和Direct Buffer

  • 內(nèi)核態(tài):控制計算機的硬件資源,并提供上層應用程序運行的環(huán)境。比如socket I/0操作或者文件的讀寫操作等

  • 用戶態(tài):上層應用程序的活動空間,應用程序的執(zhí)行必須依托于內(nèi)核提供的資源。

  • 系統(tǒng)調(diào)用:為了使上層應用能夠訪問到這些資源,內(nèi)核為上層應用提供訪問的接口。

Java網(wǎng)絡編程中如何淺析mmap和Direct Buffer

因此我們可以得知當我們通過JNI調(diào)用的native方法實際上就是從用戶態(tài)切換到了內(nèi)核態(tài)的一種方式。并且通過該系統(tǒng)調(diào)用使用操作系統(tǒng)所提供的功能。

Q:為什么需要用戶進程(位于用戶態(tài)中)要通過系統(tǒng)調(diào)用(Java中即使JNI)來調(diào)用內(nèi)核態(tài)中的資源,或者說調(diào)用操作系統(tǒng)的服務了?
A:intel cpu提供Ring0-Ring3四種級別的運行模式,Ring0級別最高,Ring3最低。Linux使用了Ring3級別運行用戶態(tài),Ring0作為內(nèi)核態(tài)。Ring3狀態(tài)不能訪問Ring0的地址空間,包括代碼和數(shù)據(jù)。因此用戶態(tài)是沒有權限去操作內(nèi)核態(tài)的資源的,它只能通過系統(tǒng)調(diào)用外完成用戶態(tài)到內(nèi)核態(tài)的切換,然后在完成相關操作后再有內(nèi)核態(tài)切換回用戶態(tài)。

DirectByteBuffer ———— 直接緩沖

DirectByteBuffer是Java用于實現(xiàn)堆外內(nèi)存的一個重要類,我們可以通過該類實現(xiàn)堆外內(nèi)存的創(chuàng)建、使用和銷毀。

Java網(wǎng)絡編程中如何淺析mmap和Direct Buffer

DirectByteBuffer該類本身還是位于Java內(nèi)存模型的堆中。堆內(nèi)內(nèi)存是JVM可以直接管控、操縱。
而DirectByteBuffer中的unsafe.allocateMemory(size);是個一個native方法,這個方法分配的是堆外內(nèi)存,通過C的malloc來進行分配的。分配的內(nèi)存是系統(tǒng)本地的內(nèi)存,并不在Java的內(nèi)存中,也不屬于JVM管控范圍,所以在DirectByteBuffer一定會存在某種方式來操縱堆外內(nèi)存。
在DirectByteBuffer的父類Buffer中有個address屬性:

    // Used only by direct buffers
    // NOTE: hoisted here for speed in JNI GetDirectBufferAddress
    long address;

address只會被直接緩存給使用到。之所以將address屬性升級放在Buffer中,是為了在JNI調(diào)用GetDirectBufferAddress時提升它調(diào)用的速率。
address表示分配的堆外內(nèi)存的地址。

Java網(wǎng)絡編程中如何淺析mmap和Direct Buffer

unsafe.allocateMemory(size);分配完堆外內(nèi)存后就會返回分配的堆外內(nèi)存基地址,并將這個地址賦值給了address屬性。這樣我們后面通過JNI對這個堆外內(nèi)存操作時都是通過這個address來實現(xiàn)的了。

在前面我們說過,在linux中內(nèi)核態(tài)的權限是最高的,那么在內(nèi)核態(tài)的場景下,操作系統(tǒng)是可以訪問任何一個內(nèi)存區(qū)域的,所以操作系統(tǒng)是可以訪問到Java堆的這個內(nèi)存區(qū)域的。
Q:那為什么操作系統(tǒng)不直接訪問Java堆內(nèi)的內(nèi)存區(qū)域了?
A:這是因為JNI方法訪問的內(nèi)存區(qū)域是一個已經(jīng)確定了的內(nèi)存區(qū)域地質(zhì),那么該內(nèi)存地址指向的是Java堆內(nèi)內(nèi)存的話,那么如果在操作系統(tǒng)正在訪問這個內(nèi)存地址的時候,Java在這個時候進行了GC操作,而GC操作會涉及到數(shù)據(jù)的移動操作[GC經(jīng)常會進行先標志在壓縮的操作。即,將可回收的空間做標志,然后清空標志位置的內(nèi)存,然后會進行一個壓縮,壓縮就會涉及到對象的移動,移動的目的是為了騰出一塊更加完整、連續(xù)的內(nèi)存空間,以容納更大的新對象],數(shù)據(jù)的移動會使JNI調(diào)用的數(shù)據(jù)錯亂。所以JNI調(diào)用的內(nèi)存是不能進行GC操作的。

Q:如上面所說,JNI調(diào)用的內(nèi)存是不能進行GC操作的,那該如何解決了?
A:①堆內(nèi)內(nèi)存與堆外內(nèi)存之間數(shù)據(jù)拷貝的方式(并且在將堆內(nèi)內(nèi)存拷貝到堆外內(nèi)存的過程JVM會保證不會進行GC操作):比如我們要完成一個從文件中讀數(shù)據(jù)到堆內(nèi)內(nèi)存的操作,即FileChannelImpl.read(HeapByteBuffer)。這里實際上File I/O會將數(shù)據(jù)讀到堆外內(nèi)存中,然后堆外內(nèi)存再講數(shù)據(jù)拷貝到堆內(nèi)內(nèi)存,這樣我們就讀到了文件中的內(nèi)存。

Java網(wǎng)絡編程中如何淺析mmap和Direct Buffer

    static int read(FileDescriptor var0, ByteBuffer var1, long var2, NativeDispatcher var4) throws IOException {
        if (var1.isReadOnly()) {
            throw new IllegalArgumentException("Read-only buffer");
        } else if (var1 instanceof DirectBuffer) {
            return readIntoNativeBuffer(var0, var1, var2, var4);
        } else {
            // 分配臨時的堆外內(nèi)存
            ByteBuffer var5 = Util.getTemporaryDirectBuffer(var1.remaining());
            int var7;
            try {
                // File I/O 操作會將數(shù)據(jù)讀入到堆外內(nèi)存中
                int var6 = readIntoNativeBuffer(var0, var5, var2, var4);
                var5.flip();
                if (var6 > 0) {
                    // 將堆外內(nèi)存的數(shù)據(jù)拷貝到堆外內(nèi)存中
                    var1.put(var5);
                }
                var7 = var6;
            } finally {
                // 里面會調(diào)用DirectBuffer.cleaner().clean()來釋放臨時的堆外內(nèi)存
                Util.offerFirstTemporaryDirectBuffer(var5);
            }
            return var7;
        }
    }

而寫操作則反之,我們會將堆內(nèi)內(nèi)存的數(shù)據(jù)線寫到對堆外內(nèi)存中,然后操作系統(tǒng)會將堆外內(nèi)存的數(shù)據(jù)寫入到文件中。
② 直接使用堆外內(nèi)存,如DirectByteBuffer:這種方式是直接在堆外分配一個內(nèi)存(即,native memory)來存儲數(shù)據(jù),程序通過JNI直接將數(shù)據(jù)讀/寫到堆外內(nèi)存中。因為數(shù)據(jù)直接寫入到了堆外內(nèi)存中,所以這種方式就不會再在JVM管控的堆內(nèi)再分配內(nèi)存來存儲數(shù)據(jù)了,也就不存在堆內(nèi)內(nèi)存和堆外內(nèi)存數(shù)據(jù)拷貝的操作了。這樣在進行I/O操作時,只需要將這個堆外內(nèi)存地址傳給JNI的I/O的函數(shù)就好了。

DirectByteBuffer堆外內(nèi)存的創(chuàng)建和回收的源碼解讀

堆外內(nèi)存分配
    DirectByteBuffer(int cap) {                   // package-private
        super(-1, 0, cap, cap);
        boolean pa = VM.isDirectMemoryPageAligned();
        int ps = Bits.pageSize();
        long size = Math.max(1L, (long)cap + (pa ? ps : 0));
        // 保留總分配內(nèi)存(按頁分配)的大小和實際內(nèi)存的大小
        Bits.reserveMemory(size, cap);
        long base = 0;
        try {
            // 通過unsafe.allocateMemory分配堆外內(nèi)存,并返回堆外內(nèi)存的基地址
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size, cap);
            throw x;
        }
        unsafe.setMemory(base, size, (byte) 0);
        if (pa && (base % ps != 0)) {
            // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        }
        // 構建Cleaner對象用于跟蹤DirectByteBuffer對象的垃圾回收,以實現(xiàn)當DirectByteBuffer被垃圾回收時,堆外內(nèi)存也會被釋放
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;
    }
Bits.reserveMemory(size, cap) 方法
    static void reserveMemory(long size, int cap) {
        if (!memoryLimitSet && VM.isBooted()) {
            maxMemory = VM.maxDirectMemory();
            memoryLimitSet = true;
        }
        // optimist!
        if (tryReserveMemory(size, cap)) {
            return;
        }
        final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
        // retry while helping enqueue pending Reference objects
        // which includes executing pending Cleaner(s) which includes
        // Cleaner(s) that free direct buffer memory
        while (jlra.tryHandlePendingReference()) {
            if (tryReserveMemory(size, cap)) {
                return;
            }
        }
        // trigger VM's Reference processing
        System.gc();
        // a retry loop with exponential back-off delays
        // (this gives VM some time to do it's job)
        boolean interrupted = false;
        try {
            long sleepTime = 1;
            int sleeps = 0;
            while (true) {
                if (tryReserveMemory(size, cap)) {
                    return;
                }
                if (sleeps >= MAX_SLEEPS) {
                    break;
                }
                if (!jlra.tryHandlePendingReference()) {
                    try {
                        Thread.sleep(sleepTime);
                        sleepTime <<= 1;
                        sleeps++;
                    } catch (InterruptedException e) {
                        interrupted = true;
                    }
                }
            }
            // no luck
            throw new OutOfMemoryError("Direct buffer memory");
        } finally {
            if (interrupted) {
                // don't swallow interrupts
                Thread.currentThread().interrupt();
            }
        }
    }

該方法用于在系統(tǒng)中保存總分配內(nèi)存(按頁分配)的大小和實際內(nèi)存的大小。

其中,如果系統(tǒng)中內(nèi)存( 即,堆外內(nèi)存 )不夠的話:

        final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
        // retry while helping enqueue pending Reference objects
        // which includes executing pending Cleaner(s) which includes
        // Cleaner(s) that free direct buffer memory
        while (jlra.tryHandlePendingReference()) {
            if (tryReserveMemory(size, cap)) {
                return;
            }
        }

jlra.tryHandlePendingReference()會觸發(fā)一次非堵塞的Reference#tryHandlePending(false)。該方法會將已經(jīng)被JVM垃圾回收的DirectBuffer對象的堆外內(nèi)存釋放。
因為在Reference的靜態(tài)代碼塊中定義了:

        SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            @Override
            public boolean tryHandlePendingReference() {
                return tryHandlePending(false);
            }
        });

如果在進行一次堆外內(nèi)存資源回收后,還不夠進行本次堆外內(nèi)存分配的話,則

        // trigger VM's Reference processing
        System.gc();

System.gc()會觸發(fā)一個full gc,當然前提是你沒有顯示的設置-XX:+DisableExplicitGC來禁用顯式GC。并且你需要知道,調(diào)用System.gc()并不能夠保證full gc馬上就能被執(zhí)行。
所以在后面打代碼中,會進行最多9次嘗試,看是否有足夠的可用堆外內(nèi)存來分配堆外內(nèi)存。并且每次嘗試之前,都對延遲等待時間,已給JVM足夠的時間去完成full gc操作。如果9次嘗試后依舊沒有足夠的可用堆外內(nèi)存來分配本次堆外內(nèi)存,則拋出OutOfMemoryError(“Direct buffer memory”)異常。

Java網(wǎng)絡編程中如何淺析mmap和Direct Buffer

注意,這里之所以用使用full gc的很重要的一個原因是:System.gc()會對新生代的老生代都會進行內(nèi)存回收,這樣會比較徹底地回收DirectByteBuffer對象以及他們關聯(lián)的堆外內(nèi)存.
DirectByteBuffer對象本身其實是很小的,但是它后面可能關聯(lián)了一個非常大的堆外內(nèi)存,因此我們通常稱之為冰山對象.
我們做ygc的時候會將新生代里的不可達的DirectByteBuffer對象及其堆外內(nèi)存回收了,但是無法對old里的DirectByteBuffer對象及其堆外內(nèi)存進行回收,這也是我們通常碰到的最大的問題。( 并且堆外內(nèi)存多用于生命期中等或較長的對象 )
如果有大量的DirectByteBuffer對象移到了old,但是又一直沒有做cms gc或者full gc,而只進行ygc,那么我們的物理內(nèi)存可能被慢慢耗光,但是我們還不知道發(fā)生了什么,因為heap明明剩余的內(nèi)存還很多(前提是我們禁用了System.gc – JVM參數(shù)DisableExplicitGC)。

總的來說,Bits.reserveMemory(size, cap)方法在可用堆外內(nèi)存不足以分配給當前要創(chuàng)建的堆外內(nèi)存大小時,會實現(xiàn)以下的步驟來嘗試完成本次堆外內(nèi)存的創(chuàng)建:
① 觸發(fā)一次非堵塞的Reference#tryHandlePending(false)。該方法會將已經(jīng)被JVM垃圾回收的DirectBuffer對象的堆外內(nèi)存釋放。
② 如果進行一次堆外內(nèi)存資源回收后,還不夠進行本次堆外內(nèi)存分配的話,則進行 System.gc()。System.gc()會觸發(fā)一個full gc,但你需要知道,調(diào)用System.gc()并不能夠保證full gc馬上就能被執(zhí)行。所以在后面打代碼中,會進行最多9次嘗試,看是否有足夠的可用堆外內(nèi)存來分配堆外內(nèi)存。并且每次嘗試之前,都對延遲等待時間,已給JVM足夠的時間去完成full gc操作。
注意,如果你設置了-XX:+DisableExplicitGC,將會禁用顯示GC,這會使System.gc()調(diào)用無效。
③ 如果9次嘗試后依舊沒有足夠的可用堆外內(nèi)存來分配本次堆外內(nèi)存,則拋出OutOfMemoryError(“Direct buffer memory”)異常。

那么可用堆外內(nèi)存到底是多少了?,即默認堆外存內(nèi)存有多大:
① 如果我們沒有通過-XX:MaxDirectMemorySize來指定最大的堆外內(nèi)存。則????
② 如果我們沒通過-Dsun.nio.MaxDirectMemorySize指定了這個屬性,且它不等于-1。則????
③ 那么最大堆外內(nèi)存的值來自于directMemory = Runtime.getRuntime().maxMemory(),這是一個native方法

JNIEXPORT jlong JNICALL
Java_java_lang_Runtime_maxMemory(JNIEnv *env, jobject this)
{
    return JVM_MaxMemory();
}
JVM_ENTRY_NO_ENV(jlong, JVM_MaxMemory(void))
  JVMWrapper("JVM_MaxMemory");
  size_t n = Universe::heap()->max_capacity();
  return convert_size_t_to_jlong(n);
JVM_END

其中在我們使用CMS GC的情況下也就是我們設置的-Xmx的值里除去一個survivor的大小就是默認的堆外內(nèi)存的大小了。

堆外內(nèi)存回收

Cleaner是PhantomReference的子類,并通過自身的next和prev字段維護的一個雙向鏈表。PhantomReference的作用在于跟蹤垃圾回收過程,并不會對對象的垃圾回收過程造成任何的影響。
所以cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); 用于對當前構造的DirectByteBuffer對象的垃圾回收過程進行跟蹤。
當DirectByteBuffer對象從pending狀態(tài) ——> enqueue狀態(tài)時,會觸發(fā)Cleaner的clean(),而Cleaner的clean()的方法會實現(xiàn)通過unsafe對堆外內(nèi)存的釋放。

Java網(wǎng)絡編程中如何淺析mmap和Direct Buffer

Java網(wǎng)絡編程中如何淺析mmap和Direct Buffer

????雖然Cleaner不會調(diào)用到Reference.clear(),但Cleaner的clean()方法調(diào)用了remove(this),即將當前Cleaner從Cleaner鏈表中移除,這樣當clean()執(zhí)行完后,Cleaner就是一個無引用指向的對象了,也就是可被GC回收的對象。

thunk方法:

Java網(wǎng)絡編程中如何淺析mmap和Direct Buffer

通過配置參數(shù)的方式來回收堆外內(nèi)存

同時我們可以通過-XX:MaxDirectMemorySize來指定最大的堆外內(nèi)存大小,當使用達到了閾值的時候將調(diào)用System.gc()來做一次full gc,以此來回收掉沒有被使用的堆外內(nèi)存。

堆外內(nèi)存那些事

使用堆外內(nèi)存的原因
  • 對垃圾回收停頓的改善
    因為full gc 意味著徹底回收,徹底回收時,垃圾收集器會對所有分配的堆內(nèi)內(nèi)存進行完整的掃描,這意味著一個重要的事實——這樣一次垃圾收集對Java應用造成的影響,跟堆的大小是成正比的。過大的堆會影響Java應用的性能。如果使用堆外內(nèi)存的話,堆外內(nèi)存是直接受操作系統(tǒng)管理( 而不是虛擬機 )。這樣做的結果就是能保持一個較小的堆內(nèi)內(nèi)存,以減少垃圾收集對應用的影響。

  • 在某些場景下可以提升程序I/O操縱的性能。少去了將數(shù)據(jù)從堆內(nèi)內(nèi)存拷貝到堆外內(nèi)存的步驟。

什么情況下使用堆外內(nèi)存
  • 堆外內(nèi)存適用于生命周期中等或較長的對象。( 如果是生命周期較短的對象,在YGC的時候就被回收了,就不存在大內(nèi)存且生命周期較長的對象在FGC對應用造成的性能影響 )。

  • 直接的文件拷貝操作,或者I/O操作。直接使用堆外內(nèi)存就能少去內(nèi)存從用戶內(nèi)存拷貝到系統(tǒng)內(nèi)存的操作,因為I/O操作是系統(tǒng)內(nèi)核內(nèi)存和設備間的通信,而不是通過程序直接和外設通信的。

  • 同時,還可以使用 池+堆外內(nèi)存 的組合方式,來對生命周期較短,但涉及到I/O操作的對象進行堆外內(nèi)存的再使用。( Netty中就使用了該方式 )

堆外內(nèi)存 VS 內(nèi)存池
  • 內(nèi)存池:主要用于兩類對象:①生命周期較短,且結構簡單的對象,在內(nèi)存池中重復利用這些對象能增加CPU緩存的命中率,從而提高性能;②加載含有大量重復對象的大片數(shù)據(jù),此時使用內(nèi)存池能減少垃圾回收的時間。

  • 堆外內(nèi)存:它和內(nèi)存池一樣,也能縮短垃圾回收時間,但是它適用的對象和內(nèi)存池完全相反。內(nèi)存池往往適用于生命期較短的可變對象,而生命期中等或較長的對象,正是堆外內(nèi)存要解決的。

堆外內(nèi)存的特點
  • 對于大內(nèi)存有良好的伸縮性

  • 對垃圾回收停頓的改善可以明顯感覺到

  • 在進程間可以共享,減少虛擬機間的復制

堆外內(nèi)存的一些問題
  • 堆外內(nèi)存回收問題,以及堆外內(nèi)存的泄漏問題。這個在上面的源碼解析已經(jīng)提到了

  • 堆外內(nèi)存的數(shù)據(jù)結構問題:堆外內(nèi)存最大的問題就是你的數(shù)據(jù)結構變得不那么直觀,如果數(shù)據(jù)結構比較復雜,就要對它進行串行化(serialization),而串行化本身也會影響性能。另一個問題是由于你可以使用更大的內(nèi)存,你可能開始擔心虛擬內(nèi)存(即硬盤)的速度對你的影響了。

上述內(nèi)容就是Java網(wǎng)絡編程中如何淺析mmap和Direct Buffer,你們學到知識或技能了嗎?如果還想學到更多技能或者豐富自己的知識儲備,歡迎關注億速云行業(yè)資訊頻道。

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網(wǎng)站立場,如果涉及侵權請聯(lián)系站長郵箱:is@yisu.com進行舉報,并提供相關證據(jù),一經(jīng)查實,將立刻刪除涉嫌侵權內(nèi)容。

AI