JDK1.4中新加入了NIO,引入了一種基于通道(Channel)和緩存區(qū)(Buffer)的I/O方式,它可以使用Native函數(shù)庫(kù)直接分配堆外內(nèi)存(native堆),然后通過一個(gè)存儲(chǔ)在Java堆中的DirectByteBuffer對(duì)象作為這塊內(nèi)存的引用進(jìn)行操作。這樣能在一些場(chǎng)景中顯著提高性能,因?yàn)楸苊饬嗽贘ava堆和Native堆中來回復(fù)制數(shù)據(jù)。
直接內(nèi)存(Direct Memory),直接內(nèi)存并不是運(yùn)行時(shí)數(shù)據(jù)區(qū)的一部分,也不是Java虛擬機(jī)規(guī)范中定義的內(nèi)在區(qū)域。它通過Unsafe類的allocateMemory()方法申請(qǐng)分配內(nèi)存,底層會(huì)調(diào)用操作系統(tǒng)的的malloc函數(shù)。
關(guān)于Direct Buffers中的問題涉及到許多...
下面我將這些問題的答案羅列如下,回答全部來自互聯(lián)網(wǎng),外加自己的一些理解,如果有不到位的地方,敬請(qǐng)批評(píng)指正!
1.1 NIO 是如何分配Native Memory的
NIO 使用java.nio.ByteBuffer.allocateDirect()方法分配內(nèi)存, 這種方式也就是通常所說的NIO direct memory 。
ByteBuffer.allocateDirect()分配的內(nèi)存使用的是本機(jī)內(nèi)存而不是Java 堆上的內(nèi)存,這也進(jìn)一步說明每次分配內(nèi)存時(shí)會(huì)調(diào)用操作系統(tǒng)的malloc()函數(shù)。
malloc函數(shù)分配內(nèi)存主要是使用brk和mmap系統(tǒng)調(diào)用,brk是_edata指針堆中的地址往高地址推,mmap是在堆和棧之間找分配一塊空閑的虛擬內(nèi)存。brk和mmap分配的都不是物理內(nèi)存,當(dāng)第一次訪問分配的虛擬內(nèi)存的時(shí)候,通過查詢頁(yè)表,發(fā)現(xiàn)這一段地址并不在物理頁(yè)面上。因?yàn)槟壳爸唤⒘说刂酚成?,真正的硬盤數(shù)據(jù)還沒有拷貝到內(nèi)存中,因此引發(fā)缺頁(yè)異常。這時(shí)操作系統(tǒng)再負(fù)責(zé)分配物理內(nèi)存,從磁盤空間加載數(shù)據(jù)到該物理內(nèi)存中,建立虛擬內(nèi)存和直接內(nèi)存的映射關(guān)系。
1.2 bck和mmap的使用場(chǎng)景
當(dāng)使用malloc分配的字節(jié)數(shù)小于128k的時(shí)候,調(diào)用brk分配虛擬內(nèi)存
當(dāng)malloc分配的字節(jié)數(shù)大于128的時(shí)候,使用過mmap分配虛擬內(nèi)存
1.3 為什么等到第一次訪問虛擬內(nèi)存的時(shí)候才分配物理內(nèi)存呢?
因?yàn)樯暾?qǐng)的內(nèi)存不一定馬上使用,推遲分配可以系統(tǒng)擁有更多的空閑物理內(nèi)存去出來其他事,從而提高系統(tǒng)的吞吐量。
1.4 內(nèi)核空間與用戶空間的概念?
為了保證用戶進(jìn)程不能直接操作內(nèi)核,保證內(nèi)核的安全,操心系統(tǒng)將虛擬空間劃分為兩部分,一部分為內(nèi)核空間,一部分為用戶空間。內(nèi)核空間主要是指操作系統(tǒng)運(yùn)行時(shí)所使用的用于程序調(diào)度、虛擬內(nèi)存的使用或者連接硬件資源等的程序邏輯。
1.5 用戶空間和內(nèi)核空間的區(qū)分?

用戶空間和內(nèi)核空間的區(qū)分一般采用虛擬內(nèi)存來實(shí)現(xiàn),因此用戶空間和內(nèi)核空間都是在虛擬內(nèi)存中。
使用虛擬內(nèi)存無(wú)非是因?yàn)槠鋬纱髢?yōu)勢(shì):
一是它可以使多個(gè)虛擬內(nèi)存地址指向同一個(gè)物理內(nèi)存;
二是虛擬內(nèi)存的空間可以大于物理內(nèi)存的空間。
1.6 為何需要內(nèi)存空間和用戶空間的劃分呢?
很顯然和前面所說的每個(gè)進(jìn)程都獨(dú)立使用屬于自己的內(nèi)存一樣,為了保證操作系統(tǒng)的穩(wěn)定性,運(yùn)行在操作系統(tǒng)中的用戶程序不能訪問操作系統(tǒng)所使用的內(nèi)存空間。這也是從安全性上考慮的,如訪問硬件資源只能由操作系統(tǒng)來發(fā)起,用戶程序不允許直接訪問硬件資源。如果用戶程序需要訪問硬件資源,如網(wǎng)絡(luò)連接等,可以調(diào)用操作系統(tǒng)提供的接口來實(shí)現(xiàn),這個(gè)調(diào)用接口的過程也
就是系統(tǒng)調(diào)用。每一次系統(tǒng)調(diào)用都會(huì)存在兩個(gè)內(nèi)存空間的切換,通常的網(wǎng)絡(luò)傳輸也是一次系統(tǒng)調(diào)用,通過網(wǎng)絡(luò)傳輸?shù)臄?shù)據(jù)先是從內(nèi)核空間接收到遠(yuǎn)程主機(jī)的數(shù)據(jù),然后再?gòu)膬?nèi)核空間復(fù)制到用戶空間,供用戶程序使用。這種從內(nèi)核空間到用戶空間的數(shù)據(jù)復(fù)制很費(fèi)時(shí),雖然保住了程序運(yùn)行的安全性和穩(wěn)定性,但是也犧牲了一部分效率。但是現(xiàn)在已經(jīng)出現(xiàn)了很多其他技術(shù)能夠減少這種從內(nèi)核空間到用戶空間的數(shù)據(jù)復(fù)制的方式,如Linux系統(tǒng)提供了sendfile 文件傳輸方式。
注意,只有內(nèi)核空間的內(nèi)存才能被DMA引擎獨(dú)立異步地存取。
1.7 DirectBuffer的開辟的堆外內(nèi)存解決了什么問題?
HeapByteBuffer是在jvm的內(nèi)存范圍之內(nèi),然后在調(diào)io的操作時(shí)會(huì)將數(shù)據(jù)區(qū)域拷貝一份到os的內(nèi)存區(qū)域,這樣造成了不必要的性能上的降低,這樣做是有原因的,試想假設(shè)如果os和jvm都是用jvm里邊的數(shù)據(jù)區(qū)域, 但是jvm會(huì)對(duì)這塊內(nèi)存區(qū)域進(jìn)行GC回收,可能會(huì)對(duì)這塊內(nèi)存的數(shù)據(jù)進(jìn)行更改,根據(jù)我們的假設(shè),由于這塊區(qū)域os也在使用,jvm對(duì)這塊共享數(shù)據(jù)發(fā)生了變更,os那邊就會(huì)出現(xiàn)數(shù)據(jù)錯(cuò)亂的情況。那么如果不讓jvm對(duì)這塊共享區(qū)域進(jìn)行GC是不是可以避免這個(gè)問題呢?
答案是不行的,也會(huì)存在問題,如果jvm不對(duì)其進(jìn)行GC回收,jvm這邊可能會(huì)出現(xiàn)OOM的內(nèi)存溢出。因此,最后這個(gè)地方非常尷尬,只能拷貝jvm的那一份到os的內(nèi)存空間,即使jvm那邊的數(shù)據(jù)區(qū)域被改變,但是os里邊的不會(huì)受到影響,等os使用io結(jié)束后會(huì)對(duì)這塊區(qū)域進(jìn)行回收,因?yàn)檫@是os的管理范圍之內(nèi)。
1.8 DirectByteBuffer 的原理?
其中 DirectByteBuffer 自身是一個(gè)Java對(duì)象,在Java堆中;而這個(gè)對(duì)象中有個(gè)long類型字段address,記錄著一塊調(diào)用 malloc() 申請(qǐng)到的native memory。
IO的時(shí)候只需要把 DirectByteBuffer 背后的native memory地址傳給真正做I/O的函數(shù)。這邊就不需要再去訪問Java對(duì)象去讀寫要做I/O的數(shù)據(jù)了。
這塊內(nèi)存直接與io設(shè)備進(jìn)行交互,當(dāng)jvm對(duì)DirectByteBuffer內(nèi)存垃圾回收的時(shí)候,會(huì)通過address調(diào)os,os將address對(duì)應(yīng)的區(qū)域回收。
1.9.DirectBuffer 屬于堆外存,那應(yīng)該還是屬于用戶內(nèi)存,還是內(nèi)核內(nèi)存?
DirectByteBuffer 自身是(Java)堆內(nèi)的,它背后真正承載數(shù)據(jù)的buffer是在(Java)堆外——native memory中的。這是 malloc() 分配出來的內(nèi)存,是用戶態(tài)的。
1.10 如果是DirectBuffer 指向的內(nèi)存在用戶空間,是不是還存在I/O的數(shù)據(jù)從內(nèi)核空間到用戶空間(native堆)的數(shù)據(jù)拷貝?
首先,訪問硬件資源只能由操作系統(tǒng)來發(fā)起,用戶程序不允許直接訪問硬件資源。所以真實(shí)的IO操作,需要調(diào)用操作系統(tǒng)提供的接口來實(shí)現(xiàn),這個(gè)調(diào)用接口的過程也就是系統(tǒng)調(diào)用。
采用JVM內(nèi)存的方式:
如果采用read/write的方式,那么必然產(chǎn)生內(nèi)核緩沖區(qū)到用戶緩沖區(qū)的過程。拿網(wǎng)絡(luò)傳輸來說,read的過程如下:
協(xié)議引擎—>內(nèi)核socket緩沖區(qū)—>內(nèi)核緩沖區(qū)—>用戶地址空間的堆外內(nèi)存—>Java堆內(nèi)存—>處理
這個(gè)過程的堆外內(nèi)存存在的意義見問題1.7。
采用堆外內(nèi)存的方式:
用堆外內(nèi)存只需拷貝一次,而用堆內(nèi)存是要拷貝兩次(內(nèi)核->堆外->堆)
協(xié)議引擎—>內(nèi)核socket緩沖區(qū)—>內(nèi)核緩沖區(qū)—>用戶地址空間的堆外內(nèi)存—>處理
