Java網(wǎng)絡(luò)編程與NIO詳解8:淺析mmap和Direct Buffer

本文轉(zhuǎn)自:https://www.cnblogs.com/huxiao-tee/p/4660352.html

本系列文章將整理到我在GitHub上的《Java面試指南》倉(cāng)庫(kù),更多精彩內(nèi)容請(qǐng)到我的倉(cāng)庫(kù)里查看

https://github.com/h2pl/Java-Tutorial

喜歡的話麻煩點(diǎn)下Star哈

文章將同步到我的個(gè)人博客:

www.how2playlife.com

本文是微信公眾號(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)存映射文件的方法,即將一個(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》課程

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末配阵,一起剝皮案震驚了整個(gè)濱河市馏颂,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌棋傍,老刑警劉巖救拉,帶你破解...
    沈念sama閱讀 218,755評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蔼囊,死亡現(xiàn)場(chǎng)離奇詭異歌径,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)岭粤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,305評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門麸拄,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)派昧,“玉大人,你說(shuō)我怎么就攤上這事感帅《范В” “怎么了?”我有些...
    開封第一講書人閱讀 165,138評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵失球,是天一觀的道長(zhǎng)岖是。 經(jīng)常有香客問我,道長(zhǎng)实苞,這世上最難降的妖魔是什么豺撑? 我笑而不...
    開封第一講書人閱讀 58,791評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮黔牵,結(jié)果婚禮上聪轿,老公的妹妹穿的比我還像新娘。我一直安慰自己猾浦,他們只是感情好陆错,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,794評(píng)論 6 392
  • 文/花漫 我一把揭開白布灯抛。 她就那樣靜靜地躺著,像睡著了一般音瓷。 火紅的嫁衣襯著肌膚如雪对嚼。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,631評(píng)論 1 305
  • 那天绳慎,我揣著相機(jī)與錄音纵竖,去河邊找鬼。 笑死杏愤,一個(gè)胖子當(dāng)著我的面吹牛靡砌,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播珊楼,決...
    沈念sama閱讀 40,362評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼通殃,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了亥曹?” 一聲冷哼從身側(cè)響起邓了,我...
    開封第一講書人閱讀 39,264評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎媳瞪,沒想到半個(gè)月后骗炉,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,724評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蛇受,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,900評(píng)論 3 336
  • 正文 我和宋清朗相戀三年句葵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片兢仰。...
    茶點(diǎn)故事閱讀 40,040評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡乍丈,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出把将,到底是詐尸還是另有隱情轻专,我是刑警寧澤,帶...
    沈念sama閱讀 35,742評(píng)論 5 346
  • 正文 年R本政府宣布察蹲,位于F島的核電站请垛,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏洽议。R本人自食惡果不足惜宗收,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,364評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望亚兄。 院中可真熱鬧混稽,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,944評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至颓影,卻和暖如春各淀,著一層夾襖步出監(jiān)牢的瞬間懒鉴,已是汗流浹背诡挂。 一陣腳步聲響...
    開封第一講書人閱讀 33,060評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留临谱,地道東北人璃俗。 一個(gè)月前我還...
    沈念sama閱讀 48,247評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像悉默,于是被迫代替她去往敵國(guó)和親城豁。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,979評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容