本文轉(zhuǎn)自:https://www.cnblogs.com/huxiao-tee/p/4660352.html
本系列文章將整理到我在GitHub上的《Java面試指南》倉(cāng)庫(kù),更多精彩內(nèi)容請(qǐng)到我的倉(cāng)庫(kù)里查看
喜歡的話麻煩點(diǎn)下Star哈
文章將同步到我的個(gè)人博客:
本文是微信公眾號(hào)【Java技術(shù)江湖】的《不可輕視的Java網(wǎng)絡(luò)編程》其中一篇祖驱,本文部分內(nèi)容來(lái)源于網(wǎng)絡(luò)官研,為了把本文主題講得清晰透徹,也整合了很多我認(rèn)為不錯(cuò)的技術(shù)博客內(nèi)容递鹉,引用其中了一些比較好的博客文章河狐,如有侵權(quán),請(qǐng)聯(lián)系作者款侵。
該系列博文會(huì)告訴你如何從計(jì)算機(jī)網(wǎng)絡(luò)的基礎(chǔ)知識(shí)入手,一步步地學(xué)習(xí)Java網(wǎng)絡(luò)基礎(chǔ)侧纯,從socket到nio新锈、bio、aio和netty等網(wǎng)絡(luò)編程知識(shí)眶熬,并且進(jìn)行實(shí)戰(zhàn)妹笆,網(wǎng)絡(luò)編程是每一個(gè)Java后端工程師必須要學(xué)習(xí)和理解的知識(shí)點(diǎn),進(jìn)一步來(lái)說(shuō)娜氏,你還需要掌握Linux中的網(wǎng)絡(luò)編程原理拳缠,包括IO模型、網(wǎng)絡(luò)編程框架netty的進(jìn)階原理贸弥,才能更完整地了解整個(gè)Java網(wǎng)絡(luò)編程的知識(shí)體系窟坐,形成自己的知識(shí)框架。
為了更好地總結(jié)和檢驗(yàn)?zāi)愕膶W(xué)習(xí)成果,本系列文章也會(huì)提供部分知識(shí)點(diǎn)對(duì)應(yīng)的面試題以及參考答案。
如果對(duì)本系列文章有什么建議点晴,或者是有什么疑問的話霎桅,也可以關(guān)注公眾號(hào)【Java技術(shù)江湖】聯(lián)系作者,歡迎你參與本系列博文的創(chuàng)作和修訂罪既。
閱讀目錄
- mmap基礎(chǔ)概念
- mmap內(nèi)存映射原理
- mmap和常規(guī)文件操作的區(qū)別
- mmap優(yōu)點(diǎn)總結(jié)
- mmap相關(guān)函數(shù)
- mmap使用細(xì)節(jié)
mmap基礎(chǔ)概念
mmap是一種內(nèi)存映射文件的方法,即將一個(gè)文件或者其它對(duì)象映射到進(jìn)程的地址空間,實(shí)現(xiàn)文件磁盤地址和進(jìn)程虛擬地址空間中一段虛擬地址的一一對(duì)映關(guān)系懒豹。實(shí)現(xiàn)這樣的映射關(guān)系后,進(jìn)程就可以采用指針的方式讀寫操作這一段內(nèi)存驯用,而系統(tǒng)會(huì)自動(dòng)回寫臟頁(yè)面到對(duì)應(yīng)的文件磁盤上脸秽,即完成了對(duì)文件的操作而不必再調(diào)用read,write等系統(tǒng)調(diào)用函數(shù)。相反蝴乔,內(nèi)核空間對(duì)這段區(qū)域的修改也直接反映用戶空間记餐,從而可以實(shí)現(xiàn)不同進(jìn)程間的文件共享。如下圖所示:
![](https://upload-images.jianshu.io/upload_images/16968642-9b421e841877a988.png)
由上圖可以看出薇正,進(jìn)程的虛擬地址空間片酝,由多個(gè)虛擬內(nèi)存區(qū)域構(gòu)成。虛擬內(nèi)存區(qū)域是進(jìn)程的虛擬地址空間中的一個(gè)同質(zhì)區(qū)間挖腰,即具有同樣特性的連續(xù)地址范圍雕沿。上圖中所示的text數(shù)據(jù)段(代碼段)、初始數(shù)據(jù)段猴仑、BSS數(shù)據(jù)段审轮、堆、棧和內(nèi)存映射,都是一個(gè)獨(dú)立的虛擬內(nèi)存區(qū)域疾渣。而為內(nèi)存映射服務(wù)的地址空間處在堆棧之間的空余部分篡诽。
linux內(nèi)核使用vm_area_struct結(jié)構(gòu)來(lái)表示一個(gè)獨(dú)立的虛擬內(nèi)存區(qū)域,由于每個(gè)不同質(zhì)的虛擬內(nèi)存區(qū)域功能和內(nèi)部機(jī)制都不同榴捡,因此一個(gè)進(jìn)程使用多個(gè)vm_area_struct結(jié)構(gòu)來(lái)分別表示不同類型的虛擬內(nèi)存區(qū)域杈女。各個(gè)vm_area_struct結(jié)構(gòu)使用鏈表或者樹形結(jié)構(gòu)鏈接,方便進(jìn)程快速訪問吊圾,如下圖所示:
![](https://upload-images.jianshu.io/upload_images/16968642-260f35f8559ad7ff.png)
vm_area_struct結(jié)構(gòu)中包含區(qū)域起始和終止地址以及其他相關(guān)信息达椰,同時(shí)也包含一個(gè)vm_ops指針,其內(nèi)部可引出所有針對(duì)這個(gè)區(qū)域可以使用的系統(tǒng)調(diào)用函數(shù)项乒。這樣砰碴,進(jìn)程對(duì)某一虛擬內(nèi)存區(qū)域的任何操作需要用要的信息,都可以從vm_area_struct中獲得板丽。mmap函數(shù)就是要?jiǎng)?chuàng)建一個(gè)新的vm_area_struct結(jié)構(gòu)呈枉,并將其與文件的物理磁盤地址相連。具體步驟請(qǐng)看下一節(jié)埃碱。
mmap內(nèi)存映射原理
mmap內(nèi)存映射的實(shí)現(xiàn)過程猖辫,總的來(lái)說(shuō)可以分為三個(gè)階段:
(一)進(jìn)程啟動(dòng)映射過程,并在虛擬地址空間中為映射創(chuàng)建虛擬映射區(qū)域
1砚殿、進(jìn)程在用戶空間調(diào)用庫(kù)函數(shù)mmap啃憎,原型:void *mmap(void *start, size_t length, int prot, int flags, int fd, off_t offset);
2、在當(dāng)前進(jìn)程的虛擬地址空間中似炎,尋找一段空閑的滿足要求的連續(xù)的虛擬地址
3辛萍、為此虛擬區(qū)分配一個(gè)vm_area_struct結(jié)構(gòu),接著對(duì)這個(gè)結(jié)構(gòu)的各個(gè)域進(jìn)行了初始化
4羡藐、將新建的虛擬區(qū)結(jié)構(gòu)(vm_area_struct)插入進(jìn)程的虛擬地址區(qū)域鏈表或樹中
(二)調(diào)用內(nèi)核空間的系統(tǒng)調(diào)用函數(shù)mmap(不同于用戶空間函數(shù))贩毕,實(shí)現(xiàn)文件物理地址和進(jìn)程虛擬地址的一一映射關(guān)系
5、為映射分配了新的虛擬地址區(qū)域后仆嗦,通過待映射的文件指針辉阶,在文件描述符表中找到對(duì)應(yīng)的文件描述符,通過文件描述符瘩扼,鏈接到內(nèi)核“已打開文件集”中該文件的文件結(jié)構(gòu)體(struct file)谆甜,每個(gè)文件結(jié)構(gòu)體維護(hù)著和這個(gè)已打開文件相關(guān)各項(xiàng)信息。
6集绰、通過該文件的文件結(jié)構(gòu)體规辱,鏈接到file_operations模塊,調(diào)用內(nèi)核函數(shù)mmap栽燕,其原型為:int mmap(struct file *filp, struct vm_area_struct *vma)罕袋,不同于用戶空間庫(kù)函數(shù)改淑。
7、內(nèi)核mmap函數(shù)通過虛擬文件系統(tǒng)inode模塊定位到文件磁盤物理地址炫贤。
8、通過remap_pfn_range函數(shù)建立頁(yè)表付秕,即實(shí)現(xiàn)了文件地址和虛擬地址區(qū)域的映射關(guān)系兰珍。此時(shí),這片虛擬地址并沒有任何數(shù)據(jù)關(guān)聯(lián)到主存中询吴。
(三)進(jìn)程發(fā)起對(duì)這片映射空間的訪問掠河,引發(fā)缺頁(yè)異常,實(shí)現(xiàn)文件內(nèi)容到物理內(nèi)存(主存)的拷貝
注:前兩個(gè)階段僅在于創(chuàng)建虛擬區(qū)間并完成地址映射猛计,但是并沒有將任何文件數(shù)據(jù)的拷貝至主存唠摹。真正的文件讀取是當(dāng)進(jìn)程發(fā)起讀或?qū)懖僮鲿r(shí)。
9奉瘤、進(jìn)程的讀或?qū)懖僮髟L問虛擬地址空間這一段映射地址勾拉,通過查詢頁(yè)表,發(fā)現(xiàn)這一段地址并不在物理頁(yè)面上盗温。因?yàn)槟壳爸唤⒘说刂酚成渑涸蓿嬲挠脖P數(shù)據(jù)還沒有拷貝到內(nèi)存中,因此引發(fā)缺頁(yè)異常卖局。
10斧蜕、缺頁(yè)異常進(jìn)行一系列判斷,確定無(wú)非法操作后砚偶,內(nèi)核發(fā)起請(qǐng)求調(diào)頁(yè)過程批销。
11、調(diào)頁(yè)過程先在交換緩存空間(swap cache)中尋找需要訪問的內(nèi)存頁(yè)染坯,如果沒有則調(diào)用nopage函數(shù)把所缺的頁(yè)從磁盤裝入到主存中均芽。
12、之后進(jìn)程即可對(duì)這片主存進(jìn)行讀或者寫的操作单鹿,如果寫操作改變了其內(nèi)容骡技,一定時(shí)間后系統(tǒng)會(huì)自動(dòng)回寫臟頁(yè)面到對(duì)應(yīng)磁盤地址,也即完成了寫入到文件的過程羞反。
注:修改過的臟頁(yè)面并不會(huì)立即更新回文件中布朦,而是有一段時(shí)間的延遲,可以調(diào)用msync()來(lái)強(qiáng)制同步, 這樣所寫的內(nèi)容就能立即保存到文件里了昼窗。
mmap和常規(guī)文件操作的區(qū)別
對(duì)linux文件系統(tǒng)不了解的朋友是趴,請(qǐng)參閱我之前寫的博文《從內(nèi)核文件系統(tǒng)看文件讀寫過程》,我們首先簡(jiǎn)單的回顧一下常規(guī)文件系統(tǒng)操作(調(diào)用read/fread等類函數(shù))中澄惊,函數(shù)的調(diào)用過程:
1唆途、進(jìn)程發(fā)起讀文件請(qǐng)求富雅。
2、內(nèi)核通過查找進(jìn)程文件符表肛搬,定位到內(nèi)核已打開文件集上的文件信息没佑,從而找到此文件的inode。
3温赔、inode在address_space上查找要請(qǐng)求的文件頁(yè)是否已經(jīng)緩存在頁(yè)緩存中蛤奢。如果存在,則直接返回這片文件頁(yè)的內(nèi)容陶贼。
4啤贩、如果不存在,則通過inode定位到文件磁盤地址拜秧,將數(shù)據(jù)從磁盤復(fù)制到頁(yè)緩存痹屹。之后再次發(fā)起讀頁(yè)面過程,進(jìn)而將頁(yè)緩存中的數(shù)據(jù)發(fā)給用戶進(jìn)程枉氮。
總結(jié)來(lái)說(shuō)志衍,常規(guī)文件操作為了提高讀寫效率和保護(hù)磁盤,使用了頁(yè)緩存機(jī)制聊替。這樣造成讀文件時(shí)需要先將文件頁(yè)從磁盤拷貝到頁(yè)緩存中足画,由于頁(yè)緩存處在內(nèi)核空間,不能被用戶進(jìn)程直接尋址佃牛,所以還需要將頁(yè)緩存中數(shù)據(jù)頁(yè)再次拷貝到內(nèi)存對(duì)應(yīng)的用戶空間中淹辞。這樣,通過了兩次數(shù)據(jù)拷貝過程俘侠,才能完成進(jìn)程對(duì)文件內(nèi)容的獲取任務(wù)象缀。寫操作也是一樣,待寫入的buffer在內(nèi)核空間不能直接訪問爷速,必須要先拷貝至內(nèi)核空間對(duì)應(yīng)的主存央星,再寫回磁盤中(延遲寫回),也是需要兩次數(shù)據(jù)拷貝惫东。
而使用mmap操作文件中莉给,創(chuàng)建新的虛擬內(nèi)存區(qū)域和建立文件磁盤地址和虛擬內(nèi)存區(qū)域映射這兩步,沒有任何文件拷貝操作廉沮。而之后訪問數(shù)據(jù)時(shí)發(fā)現(xiàn)內(nèi)存中并無(wú)數(shù)據(jù)而發(fā)起的缺頁(yè)異常過程颓遏,可以通過已經(jīng)建立好的映射關(guān)系,只使用一次數(shù)據(jù)拷貝滞时,就從磁盤中將數(shù)據(jù)傳入內(nèi)存的用戶空間中叁幢,供進(jìn)程使用。
總而言之坪稽,常規(guī)文件操作需要從磁盤到頁(yè)緩存再到用戶主存的兩次數(shù)據(jù)拷貝曼玩。而mmap操控文件鳞骤,只需要從磁盤到用戶主存的一次數(shù)據(jù)拷貝過程。說(shuō)白了黍判,mmap的關(guān)鍵點(diǎn)是實(shí)現(xiàn)了用戶空間和內(nèi)核空間的數(shù)據(jù)直接交互而省去了空間不同數(shù)據(jù)不通的繁瑣過程豫尽。因此mmap效率更高。
mmap優(yōu)點(diǎn)總結(jié)
由上文討論可知顷帖,mmap優(yōu)點(diǎn)共有一下幾點(diǎn):
1美旧、對(duì)文件的讀取操作跨過了頁(yè)緩存,減少了數(shù)據(jù)的拷貝次數(shù)窟她,用內(nèi)存讀寫取代I/O讀寫陈症,提高了文件讀取效率蔼水。
2震糖、實(shí)現(xiàn)了用戶空間和內(nèi)核空間的高效交互方式。兩空間的各自修改操作可以直接反映在映射的區(qū)域內(nèi)趴腋,從而被對(duì)方空間及時(shí)捕捉吊说。
3、提供進(jìn)程間共享內(nèi)存及相互通信的方式优炬。不管是父子進(jìn)程還是無(wú)親緣關(guān)系的進(jìn)程颁井,都可以將自身用戶空間映射到同一個(gè)文件或匿名映射到同一片區(qū)域。從而通過各自對(duì)映射區(qū)域的改動(dòng)蠢护,達(dá)到進(jìn)程間通信和進(jìn)程間共享的目的雅宾。
同時(shí),如果進(jìn)程A和進(jìn)程B都映射了區(qū)域C葵硕,當(dāng)A第一次讀取C時(shí)通過缺頁(yè)從磁盤復(fù)制文件頁(yè)到內(nèi)存中眉抬;但當(dāng)B再讀C的相同頁(yè)面時(shí),雖然也會(huì)產(chǎn)生缺頁(yè)異常懈凹,但是不再需要從磁盤中復(fù)制文件過來(lái)蜀变,而可直接使用已經(jīng)保存在內(nèi)存中的文件數(shù)據(jù)。
4介评、可用于實(shí)現(xiàn)高效的大規(guī)模數(shù)據(jù)傳輸库北。內(nèi)存空間不足,是制約大數(shù)據(jù)操作的一個(gè)方面们陆,解決方案往往是借助硬盤空間協(xié)助操作寒瓦,補(bǔ)充內(nèi)存的不足。但是進(jìn)一步會(huì)造成大量的文件I/O操作坪仇,極大影響效率孵构。這個(gè)問題可以通過mmap映射很好的解決。換句話說(shuō)烟很,但凡是需要用磁盤空間代替內(nèi)存的時(shí)候颈墅,mmap都可以發(fā)揮其功效蜡镶。
mmap使用細(xì)節(jié)
1、使用mmap需要注意的一個(gè)關(guān)鍵點(diǎn)是恤筛,mmap映射區(qū)域大小必須是物理頁(yè)大小(page_size)的整倍數(shù)(32位系統(tǒng)中通常是4k字節(jié))官还。原因是,內(nèi)存的最小粒度是頁(yè)毒坛,而進(jìn)程虛擬地址空間和內(nèi)存的映射也是以頁(yè)為單位望伦。為了匹配內(nèi)存的操作,mmap從磁盤到虛擬地址空間的映射也必須是頁(yè)煎殷。
2屯伞、內(nèi)核可以跟蹤被內(nèi)存映射的底層對(duì)象(文件)的大小,進(jìn)程可以合法的訪問在當(dāng)前文件大小以內(nèi)又在內(nèi)存映射區(qū)以內(nèi)的那些字節(jié)豪直。也就是說(shuō)劣摇,如果文件的大小一直在擴(kuò)張,只要在映射區(qū)域范圍內(nèi)的數(shù)據(jù)弓乙,進(jìn)程都可以合法得到末融,這和映射建立時(shí)文件的大小無(wú)關(guān)。具體情形參見“情形三”暇韧。
3勾习、映射建立之后,即使文件關(guān)閉懈玻,映射依然存在巧婶。因?yàn)橛成涞氖谴疟P的地址,不是文件本身涂乌,和文件句柄無(wú)關(guān)艺栈。同時(shí)可用于進(jìn)程間通信的有效地址空間不完全受限于被映射文件的大小,因?yàn)槭前错?yè)映射骂倘。
在上面的知識(shí)前提下眼滤,我們下面看看如果大小不是頁(yè)的整倍數(shù)的具體情況:
情形一:一個(gè)文件的大小是5000字節(jié),mmap函數(shù)從一個(gè)文件的起始位置開始历涝,映射5000字節(jié)到虛擬內(nèi)存中诅需。
分析:因?yàn)閱挝晃锢眄?yè)面的大小是4096字節(jié),雖然被映射的文件只有5000字節(jié)荧库,但是對(duì)應(yīng)到進(jìn)程虛擬地址區(qū)域的大小需要滿足整頁(yè)大小堰塌,因此mmap函數(shù)執(zhí)行后,實(shí)際映射到虛擬內(nèi)存區(qū)域8192個(gè) 字節(jié)分衫,5000~8191的字節(jié)部分用零填充场刑。映射后的對(duì)應(yīng)關(guān)系如下圖所示:
![](https://upload-images.jianshu.io/upload_images/16968642-656f0099e5716ed7.png)
此時(shí):
(1)讀/寫前5000個(gè)字節(jié)(0~4999),會(huì)返回操作文件內(nèi)容蚪战。
(2)讀字節(jié)50008191時(shí)牵现,結(jié)果全為0铐懊。寫50008191時(shí),進(jìn)程不會(huì)報(bào)錯(cuò)瞎疼,但是所寫的內(nèi)容不會(huì)寫入原文件中 科乎。
(3)讀/寫8192以外的磁盤部分,會(huì)返回一個(gè)SIGSECV錯(cuò)誤贼急。
情形二:一個(gè)文件的大小是5000字節(jié)茅茂,mmap函數(shù)從一個(gè)文件的起始位置開始,映射15000字節(jié)到虛擬內(nèi)存中太抓,即映射大小超過了原始文件的大小空闲。
分析:由于文件的大小是5000字節(jié),和情形一一樣走敌,其對(duì)應(yīng)的兩個(gè)物理頁(yè)碴倾。那么這兩個(gè)物理頁(yè)都是合法可以讀寫的,只是超出5000的部分不會(huì)體現(xiàn)在原文件中悔常。由于程序要求映射15000字節(jié)影斑,而文件只占兩個(gè)物理頁(yè)给赞,因此8192字節(jié)~15000字節(jié)都不能讀寫机打,操作時(shí)會(huì)返回異常。如下圖所示:
![](https://upload-images.jianshu.io/upload_images/16968642-ab28c02a22055dd2.png)
此時(shí):
(1)進(jìn)程可以正常讀/寫被映射的前5000字節(jié)(0~4999)片迅,寫操作的改動(dòng)會(huì)在一定時(shí)間后反映在原文件中残邀。
(2)對(duì)于5000~8191字節(jié),進(jìn)程可以進(jìn)行讀寫過程柑蛇,不會(huì)報(bào)錯(cuò)芥挣。但是內(nèi)容在寫入前均為0,另外耻台,寫入后不會(huì)反映在文件中空免。
(3)對(duì)于8192~14999字節(jié),進(jìn)程不能對(duì)其進(jìn)行讀寫盆耽,會(huì)報(bào)SIGBUS錯(cuò)誤蹋砚。
(4)對(duì)于15000以外的字節(jié),進(jìn)程不能對(duì)其讀寫摄杂,會(huì)引發(fā)SIGSEGV錯(cuò)誤坝咐。
情形三:一個(gè)文件初始大小為0,使用mmap操作映射了10004K的大小析恢,即1000個(gè)物理頁(yè)大約4M字節(jié)空間墨坚,mmap返回指針ptr。*
分析:如果在映射建立之初映挂,就對(duì)文件進(jìn)行讀寫操作泽篮,由于文件大小為0盗尸,并沒有合法的物理頁(yè)對(duì)應(yīng),如同情形二一樣帽撑,會(huì)返回SIGBUS錯(cuò)誤振劳。
但是如果,每次操作ptr讀寫前油狂,先增加文件的大小历恐,那么ptr在文件大小內(nèi)部的操作就是合法的。例如专筷,文件擴(kuò)充4096字節(jié)弱贼,ptr就能操作ptr ~ [ (char)ptr + 4095]的空間。只要文件擴(kuò)充的范圍在1000個(gè)物理頁(yè)(映射范圍)內(nèi)磷蛹,ptr都可以對(duì)應(yīng)操作相同的大小吮旅。
這樣,方便隨時(shí)擴(kuò)充文件空間味咳,隨時(shí)寫入文件庇勃,不造成空間浪費(fèi)。
本文轉(zhuǎn)自:http://www.reibang.com/p/007052ee3773
堆外內(nèi)存
堆外內(nèi)存是相對(duì)于堆內(nèi)內(nèi)存的一個(gè)概念槽驶。堆內(nèi)內(nèi)存是由JVM所管控的Java進(jìn)程內(nèi)存责嚷,我們平時(shí)在Java中創(chuàng)建的對(duì)象都處于堆內(nèi)內(nèi)存中,并且它們遵循JVM的內(nèi)存管理機(jī)制掂铐,JVM會(huì)采用垃圾回收機(jī)制統(tǒng)一管理它們的內(nèi)存罕拂。那么堆外內(nèi)存就是存在于JVM管控之外的一塊內(nèi)存區(qū)域,因此它是不受JVM的管控全陨。
在講解DirectByteBuffer之前爆班,需要先簡(jiǎn)單了解兩個(gè)知識(shí)點(diǎn)
java引用類型,因?yàn)镈irectByteBuffer是通過虛引用(Phantom Reference)來(lái)實(shí)現(xiàn)堆外內(nèi)存的釋放的辱姨。
PhantomReference 是所有“弱引用”中最弱的引用類型柿菩。不同于軟引用和弱引用,虛引用無(wú)法通過 get() 方法來(lái)取得目標(biāo)對(duì)象的強(qiáng)引用從而使用目標(biāo)對(duì)象雨涛,觀察源碼可以發(fā)現(xiàn) get() 被重寫為永遠(yuǎn)返回 null枢舶。
那虛引用到底有什么作用?其實(shí)虛引用主要被用來(lái) 跟蹤對(duì)象被垃圾回收的狀態(tài)镜悉,通過查看引用隊(duì)列中是否包含對(duì)象所對(duì)應(yīng)的虛引用來(lái)判斷它是否 即將被垃圾回收祟辟,從而采取行動(dòng)。它并不被期待用來(lái)取得目標(biāo)對(duì)象的引用侣肄,而目標(biāo)對(duì)象被回收前旧困,它的引用會(huì)被放入一個(gè) ReferenceQueue 對(duì)象中,從而達(dá)到跟蹤對(duì)象垃圾回收的作用。
關(guān)于java引用類型的實(shí)現(xiàn)和原理可以閱讀之前的文章Reference 吼具、ReferenceQueue 詳解 和Java 引用類型簡(jiǎn)述
關(guān)于linux的內(nèi)核態(tài)和用戶態(tài)
- 內(nèi)核態(tài):控制計(jì)算機(jī)的硬件資源僚纷,并提供上層應(yīng)用程序運(yùn)行的環(huán)境。比如socket I/0操作或者文件的讀寫操作等
- 用戶態(tài):上層應(yīng)用程序的活動(dòng)空間拗盒,應(yīng)用程序的執(zhí)行必須依托于內(nèi)核提供的資源怖竭。
- 系統(tǒng)調(diào)用:為了使上層應(yīng)用能夠訪問到這些資源,內(nèi)核為上層應(yīng)用提供訪問的接口陡蝇。
因此我們可以得知當(dāng)我們通過JNI調(diào)用的native方法實(shí)際上就是從用戶態(tài)切換到了內(nèi)核態(tài)的一種方式痊臭。并且通過該系統(tǒng)調(diào)用使用操作系統(tǒng)所提供的功能。
Q:為什么需要用戶進(jìn)程(位于用戶態(tài)中)要通過系統(tǒng)調(diào)用(Java中即使JNI)來(lái)調(diào)用內(nèi)核態(tài)中的資源登夫,或者說(shuō)調(diào)用操作系統(tǒng)的服務(wù)了广匙?
A:intel cpu提供Ring0-Ring3四種級(jí)別的運(yùn)行模式,Ring0級(jí)別最高恼策,Ring3最低鸦致。Linux使用了Ring3級(jí)別運(yùn)行用戶態(tài),Ring0作為內(nèi)核態(tài)涣楷。Ring3狀態(tài)不能訪問Ring0的地址空間分唾,包括代碼和數(shù)據(jù)。因此用戶態(tài)是沒有權(quán)限去操作內(nèi)核態(tài)的資源的狮斗,它只能通過系統(tǒng)調(diào)用外完成用戶態(tài)到內(nèi)核態(tài)的切換绽乔,然后在完成相關(guān)操作后再有內(nèi)核態(tài)切換回用戶態(tài)。
DirectByteBuffer ———— 直接緩沖
DirectByteBuffer是Java用于實(shí)現(xiàn)堆外內(nèi)存的一個(gè)重要類情龄,我們可以通過該類實(shí)現(xiàn)堆外內(nèi)存的創(chuàng)建迄汛、使用和銷毀捍壤。
DirectByteBuffer該類本身還是位于Java內(nèi)存模型的堆中骤视。堆內(nèi)內(nèi)存是JVM可以直接管控、操縱鹃觉。
而DirectByteBuffer中的unsafe.allocateMemory(size);是個(gè)一個(gè)native方法专酗,這個(gè)方法分配的是堆外內(nèi)存,通過C的malloc來(lái)進(jìn)行分配的盗扇。分配的內(nèi)存是系統(tǒng)本地的內(nèi)存祷肯,并不在Java的內(nèi)存中,也不屬于JVM管控范圍疗隶,所以在DirectByteBuffer一定會(huì)存在某種方式來(lái)操縱堆外內(nèi)存佑笋。
在DirectByteBuffer的父類Buffer中有個(gè)address屬性:
// Used only by direct buffers
// NOTE: hoisted here for speed in JNI GetDirectBufferAddress
long address;
address只會(huì)被直接緩存給使用到。之所以將address屬性升級(jí)放在Buffer中斑鼻,是為了在JNI調(diào)用GetDirectBufferAddress時(shí)提升它調(diào)用的速率蒋纬。
address表示分配的堆外內(nèi)存的地址。
unsafe.allocateMemory(size);分配完堆外內(nèi)存后就會(huì)返回分配的堆外內(nèi)存基地址,并將這個(gè)地址賦值給了address屬性蜀备。這樣我們后面通過JNI對(duì)這個(gè)堆外內(nèi)存操作時(shí)都是通過這個(gè)address來(lái)實(shí)現(xiàn)的了关摇。
在前面我們說(shuō)過,在linux中內(nèi)核態(tài)的權(quán)限是最高的碾阁,那么在內(nèi)核態(tài)的場(chǎng)景下输虱,操作系統(tǒng)是可以訪問任何一個(gè)內(nèi)存區(qū)域的,所以操作系統(tǒng)是可以訪問到Java堆的這個(gè)內(nèi)存區(qū)域的脂凶。
Q:那為什么操作系統(tǒng)不直接訪問Java堆內(nèi)的內(nèi)存區(qū)域了宪睹?
A:這是因?yàn)镴NI方法訪問的內(nèi)存區(qū)域是一個(gè)已經(jīng)確定了的內(nèi)存區(qū)域地質(zhì),那么該內(nèi)存地址指向的是Java堆內(nèi)內(nèi)存的話蚕钦,那么如果在操作系統(tǒng)正在訪問這個(gè)內(nèi)存地址的時(shí)候横堡,Java在這個(gè)時(shí)候進(jìn)行了GC操作,而GC操作會(huì)涉及到數(shù)據(jù)的移動(dòng)操作[GC經(jīng)常會(huì)進(jìn)行先標(biāo)志在壓縮的操作冠桃。即命贴,將可回收的空間做標(biāo)志,然后清空標(biāo)志位置的內(nèi)存食听,然后會(huì)進(jìn)行一個(gè)壓縮胸蛛,壓縮就會(huì)涉及到對(duì)象的移動(dòng),移動(dòng)的目的是為了騰出一塊更加完整樱报、連續(xù)的內(nèi)存空間葬项,以容納更大的新對(duì)象],數(shù)據(jù)的移動(dòng)會(huì)使JNI調(diào)用的數(shù)據(jù)錯(cuò)亂迹蛤。所以JNI調(diào)用的內(nèi)存是不能進(jìn)行GC操作的民珍。
Q:如上面所說(shuō),JNI調(diào)用的內(nèi)存是不能進(jìn)行GC操作的盗飒,那該如何解決了嚷量?
A:①堆內(nèi)內(nèi)存與堆外內(nèi)存之間數(shù)據(jù)拷貝的方式(并且在將堆內(nèi)內(nèi)存拷貝到堆外內(nèi)存的過程JVM會(huì)保證不會(huì)進(jìn)行GC操作):比如我們要完成一個(gè)從文件中讀數(shù)據(jù)到堆內(nèi)內(nèi)存的操作,即FileChannelImpl.read(HeapByteBuffer)逆趣。這里實(shí)際上File I/O會(huì)將數(shù)據(jù)讀到堆外內(nèi)存中蝶溶,然后堆外內(nèi)存再講數(shù)據(jù)拷貝到堆內(nèi)內(nèi)存,這樣我們就讀到了文件中的內(nèi)存宣渗。
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 {
// 分配臨時(shí)的堆外內(nèi)存
ByteBuffer var5 = Util.getTemporaryDirectBuffer(var1.remaining());
int var7;
try {
// File I/O 操作會(huì)將數(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 {
// 里面會(huì)調(diào)用DirectBuffer.cleaner().clean()來(lái)釋放臨時(shí)的堆外內(nèi)存
Util.offerFirstTemporaryDirectBuffer(var5);
}
return var7;
}
}
而寫操作則反之抖所,我們會(huì)將堆內(nèi)內(nèi)存的數(shù)據(jù)線寫到對(duì)堆外內(nèi)存中,然后操作系統(tǒng)會(huì)將堆外內(nèi)存的數(shù)據(jù)寫入到文件中痕囱。
② 直接使用堆外內(nèi)存田轧,如DirectByteBuffer:這種方式是直接在堆外分配一個(gè)內(nèi)存(即,native memory)來(lái)存儲(chǔ)數(shù)據(jù)鞍恢,程序通過JNI直接將數(shù)據(jù)讀/寫到堆外內(nèi)存中傻粘。因?yàn)閿?shù)據(jù)直接寫入到了堆外內(nèi)存中巷查,所以這種方式就不會(huì)再在JVM管控的堆內(nèi)再分配內(nèi)存來(lái)存儲(chǔ)數(shù)據(jù)了,也就不存在堆內(nèi)內(nèi)存和堆外內(nèi)存數(shù)據(jù)拷貝的操作了抹腿。這樣在進(jìn)行I/O操作時(shí)岛请,只需要將這個(gè)堆外內(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)存(按頁(yè)分配)的大小和實(shí)際內(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;
}
// 構(gòu)建Cleaner對(duì)象用于跟蹤DirectByteBuffer對(duì)象的垃圾回收崇败,以實(shí)現(xiàn)當(dāng)DirectByteBuffer被垃圾回收時(shí),堆外內(nèi)存也會(huì)被釋放
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)存(按頁(yè)分配)的大小和實(shí)際內(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()會(huì)觸發(fā)一次非堵塞的Reference#tryHandlePending(false)混狠。該方法會(huì)將已經(jīng)被JVM垃圾回收的DirectBuffer對(duì)象的堆外內(nèi)存釋放岸霹。
因?yàn)樵赗eference的靜態(tài)代碼塊中定義了:
SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
@Override
public boolean tryHandlePendingReference() {
return tryHandlePending(false);
}
});
如果在進(jìn)行一次堆外內(nèi)存資源回收后,還不夠進(jìn)行本次堆外內(nèi)存分配的話将饺,則
// trigger VM's Reference processing
System.gc();
System.gc()會(huì)觸發(fā)一個(gè)full gc贡避,當(dāng)然前提是你沒有顯示的設(shè)置-XX:+DisableExplicitGC來(lái)禁用顯式GC。并且你需要知道予弧,調(diào)用System.gc()并不能夠保證full gc馬上就能被執(zhí)行刮吧。
所以在后面打代碼中,會(huì)進(jìn)行最多9次嘗試掖蛤,看是否有足夠的可用堆外內(nèi)存來(lái)分配堆外內(nèi)存杀捻。并且每次嘗試之前,都對(duì)延遲等待時(shí)間蚓庭,已給JVM足夠的時(shí)間去完成full gc操作致讥。如果9次嘗試后依舊沒有足夠的可用堆外內(nèi)存來(lái)分配本次堆外內(nèi)存,則拋出OutOfMemoryError("Direct buffer memory”)異常器赞。
注意垢袱,這里之所以用使用full gc的很重要的一個(gè)原因是:System.gc()會(huì)對(duì)新生代的老生代都會(huì)進(jìn)行內(nèi)存回收,這樣會(huì)比較徹底地回收DirectByteBuffer對(duì)象以及他們關(guān)聯(lián)的堆外內(nèi)存.
DirectByteBuffer對(duì)象本身其實(shí)是很小的拳魁,但是它后面可能關(guān)聯(lián)了一個(gè)非常大的堆外內(nèi)存惶桐,因此我們通常稱之為冰山對(duì)象.
我們做ygc的時(shí)候會(huì)將新生代里的不可達(dá)的DirectByteBuffer對(duì)象及其堆外內(nèi)存回收了,但是無(wú)法對(duì)old里的DirectByteBuffer對(duì)象及其堆外內(nèi)存進(jìn)行回收潘懊,這也是我們通常碰到的最大的問題。( 并且堆外內(nèi)存多用于生命期中等或較長(zhǎng)的對(duì)象 )
如果有大量的DirectByteBuffer對(duì)象移到了old贿衍,但是又一直沒有做cms gc或者full gc授舟,而只進(jìn)行ygc,那么我們的物理內(nèi)存可能被慢慢耗光贸辈,但是我們還不知道發(fā)生了什么释树,因?yàn)閔eap明明剩余的內(nèi)存還很多(前提是我們禁用了System.gc – JVM參數(shù)DisableExplicitGC)。
總的來(lái)說(shuō),Bits.reserveMemory(size, cap)方法在可用堆外內(nèi)存不足以分配給當(dāng)前要?jiǎng)?chuàng)建的堆外內(nèi)存大小時(shí)奢啥,會(huì)實(shí)現(xiàn)以下的步驟來(lái)嘗試完成本次堆外內(nèi)存的創(chuàng)建:
① 觸發(fā)一次非堵塞的Reference#tryHandlePending(false)秸仙。該方法會(huì)將已經(jīng)被JVM垃圾回收的DirectBuffer對(duì)象的堆外內(nèi)存釋放。
② 如果進(jìn)行一次堆外內(nèi)存資源回收后桩盲,還不夠進(jìn)行本次堆外內(nèi)存分配的話寂纪,則進(jìn)行 System.gc()。System.gc()會(huì)觸發(fā)一個(gè)full gc赌结,但你需要知道捞蛋,調(diào)用System.gc()并不能夠保證full gc馬上就能被執(zhí)行。所以在后面打代碼中柬姚,會(huì)進(jìn)行最多9次嘗試拟杉,看是否有足夠的可用堆外內(nèi)存來(lái)分配堆外內(nèi)存。并且每次嘗試之前量承,都對(duì)延遲等待時(shí)間搬设,已給JVM足夠的時(shí)間去完成full gc操作。
注意撕捍,如果你設(shè)置了-XX:+DisableExplicitGC焕梅,將會(huì)禁用顯示GC,這會(huì)使System.gc()調(diào)用無(wú)效卦洽。
③ 如果9次嘗試后依舊沒有足夠的可用堆外內(nèi)存來(lái)分配本次堆外內(nèi)存贞言,則拋出OutOfMemoryError("Direct buffer memory”)異常。
那么可用堆外內(nèi)存到底是多少了阀蒂?该窗,即默認(rèn)堆外存內(nèi)存有多大:
① 如果我們沒有通過-XX:MaxDirectMemorySize來(lái)指定最大的堆外內(nèi)存。則??
② 如果我們沒通過-Dsun.nio.MaxDirectMemorySize指定了這個(gè)屬性蚤霞,且它不等于-1酗失。則??
③ 那么最大堆外內(nèi)存的值來(lái)自于directMemory = Runtime.getRuntime().maxMemory(),這是一個(gè)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的情況下也就是我們?cè)O(shè)置的-Xmx的值里除去一個(gè)survivor的大小就是默認(rèn)的堆外內(nèi)存的大小了昧绣。
堆外內(nèi)存回收
Cleaner是PhantomReference的子類规肴,并通過自身的next和prev字段維護(hù)的一個(gè)雙向鏈表。PhantomReference的作用在于跟蹤垃圾回收過程夜畴,并不會(huì)對(duì)對(duì)象的垃圾回收過程造成任何的影響拖刃。
所以cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); 用于對(duì)當(dāng)前構(gòu)造的DirectByteBuffer對(duì)象的垃圾回收過程進(jìn)行跟蹤。
當(dāng)DirectByteBuffer對(duì)象從pending狀態(tài) ——> enqueue狀態(tài)時(shí)贪绘,會(huì)觸發(fā)Cleaner的clean()兑牡,而Cleaner的clean()的方法會(huì)實(shí)現(xiàn)通過unsafe對(duì)堆外內(nèi)存的釋放。
??雖然Cleaner不會(huì)調(diào)用到Reference.clear()税灌,但Cleaner的clean()方法調(diào)用了remove(this)均函,即將當(dāng)前Cleaner從Cleaner鏈表中移除亿虽,這樣當(dāng)clean()執(zhí)行完后,Cleaner就是一個(gè)無(wú)引用指向的對(duì)象了苞也,也就是可被GC回收的對(duì)象洛勉。
thunk方法:
通過配置參數(shù)的方式來(lái)回收堆外內(nèi)存
同時(shí)我們可以通過-XX:MaxDirectMemorySize來(lái)指定最大的堆外內(nèi)存大小,當(dāng)使用達(dá)到了閾值的時(shí)候?qū)⒄{(diào)用System.gc()來(lái)做一次full gc如迟,以此來(lái)回收掉沒有被使用的堆外內(nèi)存收毫。
堆外內(nèi)存那些事
使用堆外內(nèi)存的原因
- 對(duì)垃圾回收停頓的改善
因?yàn)閒ull gc 意味著徹底回收,徹底回收時(shí)氓涣,垃圾收集器會(huì)對(duì)所有分配的堆內(nèi)內(nèi)存進(jìn)行完整的掃描牛哺,這意味著一個(gè)重要的事實(shí)——這樣一次垃圾收集對(duì)Java應(yīng)用造成的影響,跟堆的大小是成正比的劳吠。過大的堆會(huì)影響Java應(yīng)用的性能引润。如果使用堆外內(nèi)存的話,堆外內(nèi)存是直接受操作系統(tǒng)管理( 而不是虛擬機(jī) )痒玩。這樣做的結(jié)果就是能保持一個(gè)較小的堆內(nèi)內(nèi)存淳附,以減少垃圾收集對(duì)應(yīng)用的影響。 - 在某些場(chǎng)景下可以提升程序I/O操縱的性能蠢古。少去了將數(shù)據(jù)從堆內(nèi)內(nèi)存拷貝到堆外內(nèi)存的步驟奴曙。
什么情況下使用堆外內(nèi)存
- 堆外內(nèi)存適用于生命周期中等或較長(zhǎng)的對(duì)象。( 如果是生命周期較短的對(duì)象草讶,在YGC的時(shí)候就被回收了洽糟,就不存在大內(nèi)存且生命周期較長(zhǎng)的對(duì)象在FGC對(duì)應(yīng)用造成的性能影響 )。
- 直接的文件拷貝操作堕战,或者I/O操作坤溃。直接使用堆外內(nèi)存就能少去內(nèi)存從用戶內(nèi)存拷貝到系統(tǒng)內(nèi)存的操作,因?yàn)镮/O操作是系統(tǒng)內(nèi)核內(nèi)存和設(shè)備間的通信嘱丢,而不是通過程序直接和外設(shè)通信的薪介。
- 同時(shí),還可以使用 池+堆外內(nèi)存 的組合方式越驻,來(lái)對(duì)生命周期較短汁政,但涉及到I/O操作的對(duì)象進(jìn)行堆外內(nèi)存的再使用。( Netty中就使用了該方式 )
堆外內(nèi)存 VS 內(nèi)存池
- 內(nèi)存池:主要用于兩類對(duì)象:①生命周期較短缀旁,且結(jié)構(gòu)簡(jiǎn)單的對(duì)象记劈,在內(nèi)存池中重復(fù)利用這些對(duì)象能增加CPU緩存的命中率,從而提高性能诵棵;②加載含有大量重復(fù)對(duì)象的大片數(shù)據(jù)抠蚣,此時(shí)使用內(nèi)存池能減少垃圾回收的時(shí)間。
- 堆外內(nèi)存:它和內(nèi)存池一樣履澳,也能縮短垃圾回收時(shí)間嘶窄,但是它適用的對(duì)象和內(nèi)存池完全相反。內(nèi)存池往往適用于生命期較短的可變對(duì)象距贷,而生命期中等或較長(zhǎng)的對(duì)象柄冲,正是堆外內(nèi)存要解決的。
堆外內(nèi)存的特點(diǎn)
- 對(duì)于大內(nèi)存有良好的伸縮性
- 對(duì)垃圾回收停頓的改善可以明顯感覺到
- 在進(jìn)程間可以共享忠蝗,減少虛擬機(jī)間的復(fù)制
堆外內(nèi)存的一些問題
- 堆外內(nèi)存回收問題现横,以及堆外內(nèi)存的泄漏問題。這個(gè)在上面的源碼解析已經(jīng)提到了
- 堆外內(nèi)存的數(shù)據(jù)結(jié)構(gòu)問題:堆外內(nèi)存最大的問題就是你的數(shù)據(jù)結(jié)構(gòu)變得不那么直觀阁最,如果數(shù)據(jù)結(jié)構(gòu)比較復(fù)雜戒祠,就要對(duì)它進(jìn)行串行化(serialization),而串行化本身也會(huì)影響性能速种。另一個(gè)問題是由于你可以使用更大的內(nèi)存姜盈,你可能開始擔(dān)心虛擬內(nèi)存(即硬盤)的速度對(duì)你的影響了。
參考文章
http://lovestblog.cn/blog/2015/05/12/direct-buffer/
http://www.infoq.com/cn/news/2014/12/external-memory-heap-memory
http://www.reibang.com/p/85e931636f27
圣思園《并發(fā)與Netty》課程