1. 程式人生 > >Java網路程式設計與NIO詳解8:淺析mmap和Direct Buffer

Java網路程式設計與NIO詳解8:淺析mmap和Direct Buffer

微信公眾號【黃小斜】作者是螞蟻金服 JAVA 工程師,目前在螞蟻財富負責後端開發工作,專注於 JAVA 後端技術棧,同時也懂點投資理財,堅持學習和寫作,用大廠程式設計師的視角解讀技術與網際網路,我的世界裡不只有 coding!關注公眾號後回覆”架構師“即可領取 Java基礎、進階、專案和架構師等免費學習資料,更有資料庫、分散式、微服務等熱門技術學習視訊,內容豐富,兼顧原理和實踐,另外也將贈送作者原創的Java學習指南、Java程式設計師面試指南等乾貨資源

之前看到一篇文章說epoll中在維護epoll控制代碼資料結構時使用到了mmap的技術,但是後來看了其他文章以及epoll原始碼後發現好像並沒有用到這個技術。

轉自知乎:

epoll_wait的實現~有關從核心態拷貝到使用者態程式碼.可以看到__put_user這個函式就是核心拷貝到使用者空間.分析完整個linux 2.6版本的epoll實現沒有發現使用了mmap系統呼叫,根本不存在共享記憶體在epoll的實現

if (revents) {
            /* 將當前的事件和使用者傳入的資料都copy給使用者空間,
             * 就是epoll_wait()後應用程式能讀到的那一堆資料. */
            if (__put_user(revents, &uevent->events) ||
                __put_user(epi->event.data, &uevent->data)) {
                /* 如果copy過程中發生錯誤, 會中斷連結串列的掃描,
                 * 並把當前發生錯誤的epitem重新插入到ready list.
                 * 剩下的沒處理的epitem也不會丟棄, 在ep_scan_ready_list()
                 * 中它們也會被重新插入到ready list */
                list_add(&epi->rdllink, head);
                return eventcnt ? eventcnt : -EFAULT;
            }

那麼既然提到了,就讓我們看看mmap到底是什麼吧

mmap:記憶體對映檔案

轉自:https://www.cnblogs.com/huxiao-tee/p/4660352.html

  • mmap基礎概念
  • mmap記憶體對映原理
  • mmap和常規檔案操作的區別
  • mmap優點總結

回到頂部

mmap基礎概念

mmap是一種記憶體對映檔案的方法,即將一個檔案或者其它物件對映到程序的地址空間,實現檔案磁碟地址和程序虛擬地址空間中一段虛擬地址的一一對映關係。實現這樣的對映關係後,程序就可以採用指標的方式讀寫操作這一段記憶體,而系統會自動回寫髒頁面到對應的檔案磁碟上,即完成了對檔案的操作而不必再呼叫read,write等系統呼叫函式。相反,核心空間對這段區域的修改也直接反映使用者空間,從而可以實現不同程序間的檔案共享。如下圖所示:

          

由上圖可以看出,程序的虛擬地址空間,由多個虛擬記憶體區域構成。虛擬記憶體區域是程序的虛擬地址空間中的一個同質區間,即具有同樣特性的連續地址範圍。上圖中所示的text資料段(程式碼段)、初始資料段、BSS資料段、堆、棧和記憶體對映,都是一個獨立的虛擬記憶體區域。而為記憶體對映服務的地址空間處在堆疊之間的空餘部分。

linux核心使用vm_area_struct結構來表示一個獨立的虛擬記憶體區域,由於每個不同質的虛擬記憶體區域功能和內部機制都不同,因此一個程序使用多個vm_area_struct結構來分別表示不同型別的虛擬記憶體區域。各個vm_area_struct結構使用連結串列或者樹形結構連結,方便程序快速訪問,如下圖所示:

         

vm_area_struct結構中包含區域起始和終止地址以及其他相關資訊,同時也包含一個vm_ops指標,其內部可引出所有針對這個區域可以使用的系統呼叫函式。這樣,程序對某一虛擬記憶體區域的任何操作需要用要的資訊,都可以從vm_area_struct中獲得。mmap函式就是要建立一個新的vm_area_struct結構,並將其與檔案的物理磁碟地址相連。具體步驟請看下一節。

回到頂部

mmap記憶體對映原理

mmap記憶體對映的實現過程,總的來說可以分為三個階段:

(一)程序啟動對映過程,並在虛擬地址空間中為對映建立虛擬對映區域

1、程序在使用者空間呼叫庫函式mmap,原型:void mmap(void start, size_t length, int prot, int flags, int fd, off_t offset);

2、在當前程序的虛擬地址空間中,尋找一段空閒的滿足要求的連續的虛擬地址

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

4、將新建的虛擬區結構(vm_area_struct)插入程序的虛擬地址區域連結串列或樹中

(二)呼叫核心空間的系統呼叫函式mmap(不同於使用者空間函式),實現檔案實體地址和程序虛擬地址的一一對映關係

5、為對映分配了新的虛擬地址區域後,通過待對映的檔案指標,在檔案描述符表中找到對應的檔案描述符,通過檔案描述符,連結到核心“已開啟檔案集”中該檔案的檔案結構體(struct file),每個檔案結構體維護著和這個已開啟檔案相關各項資訊。

6、通過該檔案的檔案結構體,連結到file_operations模組,呼叫核心函式mmap,其原型為:int mmap(struct file filp, struct vm_area_struct vma),不同於使用者空間庫函式。

7、核心mmap函式通過虛擬檔案系統inode模組定位到檔案磁碟實體地址。

8、通過remap_pfn_range函式建立頁表,即實現了檔案地址和虛擬地址區域的對映關係。此時,這片虛擬地址並沒有任何資料關聯到主存中。

(三)程序發起對這片對映空間的訪問,引發缺頁異常,實現檔案內容到實體記憶體(主存)的拷貝

注:前兩個階段僅在於建立虛擬區間並完成地址對映,但是並沒有將任何檔案資料的拷貝至主存。真正的檔案讀取是當程序發起讀或寫操作時。

9、程序的讀或寫操作訪問虛擬地址空間這一段對映地址,通過查詢頁表,發現這一段地址並不在物理頁面上。因為目前只建立了地址對映,真正的硬碟資料還沒有拷貝到記憶體中,因此引發缺頁異常。

10、缺頁異常進行一系列判斷,確定無非法操作後,核心發起請求調頁過程。

11、調頁過程先在交換快取空間(swap cache)中尋找需要訪問的記憶體頁,如果沒有則呼叫nopage函式把所缺的頁從磁碟裝入到主存中。

12、之後程序即可對這片主存進行讀或者寫的操作,如果寫操作改變了其內容,一定時間後系統會自動回寫髒頁面到對應磁碟地址,也即完成了寫入到檔案的過程。

注:修改過的髒頁面並不會立即更新迴文件中,而是有一段時間的延遲,可以呼叫msync()來強制同步, 這樣所寫的內容就能立即儲存到檔案裡了。

回到頂部

mmap和常規檔案操作的區別

對linux檔案系統不瞭解的朋友,請參閱我之前寫的博文《從核心檔案系統看檔案讀寫過程》,我們首先簡單的回顧一下常規檔案系統操作(呼叫read/fread等類函式)中,函式的呼叫過程:

1、程序發起讀檔案請求。

2、核心通過查詢程序檔案符表,定位到核心已開啟檔案集上的檔案資訊,從而找到此檔案的inode。

3、inode在address_space上查詢要請求的檔案頁是否已經快取在頁快取中。如果存在,則直接返回這片檔案頁的內容。

4、如果不存在,則通過inode定位到檔案磁碟地址,將資料從磁碟複製到頁快取。之後再次發起讀頁面過程,進而將頁快取中的資料發給使用者程序。

總結來說,常規檔案操作為了提高讀寫效率和保護磁碟,使用了頁快取機制。這樣造成讀檔案時需要先將檔案頁從磁碟拷貝到頁快取中,由於頁快取處在核心空間,不能被使用者程序直接定址,所以還需要將頁快取中資料頁再次拷貝到記憶體對應的使用者空間中。這樣,通過了兩次資料拷貝過程,才能完成程序對檔案內容的獲取任務。寫操作也是一樣,待寫入的buffer在核心空間不能直接訪問,必須要先拷貝至核心空間對應的主存,再寫回磁碟中(延遲寫回),也是需要兩次資料拷貝。

而使用mmap操作檔案中,建立新的虛擬記憶體區域和建立檔案磁碟地址和虛擬記憶體區域對映這兩步,沒有任何檔案拷貝操作。而之後訪問資料時發現記憶體中並無資料而發起的缺頁異常過程,可以通過已經建立好的對映關係,只使用一次資料拷貝,就從磁碟中將資料傳入記憶體的使用者空間中,供程序使用。

總而言之,常規檔案操作需要從磁碟到頁快取再到使用者主存的兩次資料拷貝。而mmap操控檔案,只需要從磁碟到使用者主存的一次資料拷貝過程。說白了,mmap的關鍵點是實現了使用者空間和核心空間的資料直接互動而省去了空間不同資料不通的繁瑣過程。因此mmap效率更高。

回到頂部

mmap優點總結

由上文討論可知,mmap優點共有一下幾點:

1、對檔案的讀取操作跨過了頁快取,減少了資料的拷貝次數,用記憶體讀寫取代I/O讀寫,提高了檔案讀取效率。

2、實現了使用者空間和核心空間的高效互動方式。兩空間的各自修改操作可以直接反映在對映的區域內,從而被對方空間及時捕捉。

3、提供程序間共享記憶體及相互通訊的方式。不管是父子程序還是無親緣關係的程序,都可以將自身使用者空間對映到同一個檔案或匿名對映到同一片區域。從而通過各自對對映區域的改動,達到程序間通訊和程序間共享的目的。

     同時,如果程序A和程序B都映射了區域C,當A第一次讀取C時通過缺頁從磁碟複製檔案頁到記憶體中;但當B再讀C的相同頁面時,雖然也會產生缺頁異常,但是不再需要從磁碟中複製檔案過來,而可直接使用已經儲存在記憶體中的檔案資料。

4、可用於實現高效的大規模資料傳輸。記憶體空間不足,是制約大資料操作的一個方面,解決方案往往是藉助硬碟空間協助操作,補充記憶體的不足。但是進一步會造成大量的檔案I/O操作,極大影響效率。這個問題可以通過mmap對映很好的解決。換句話說,但凡是需要用磁碟空間代替記憶體的時候,mmap都可以發揮其功效。

堆外記憶體之 DirectByteBuffer 詳解

原文出處: tomas家的小撥浪鼓

堆外記憶體

堆外記憶體是相對於堆內記憶體的一個概念。堆內記憶體是由JVM所管控的Java程序記憶體,我們平時在Java中建立的物件都處於堆內記憶體中,並且它們遵循JVM的記憶體管理機制,JVM會採用垃圾回收機制統一管理它們的記憶體。那麼堆外記憶體就是存在於JVM管控之外的一塊記憶體區域,因此它是不受JVM的管控。

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

java引用型別,因為DirectByteBuffer是通過虛引用(Phantom Reference)來實現堆外記憶體的釋放的。

PhantomReference 是所有“弱引用”中最弱的引用型別。不同於軟引用和弱引用,虛引用無法通過 get() 方法來取得目標物件的強引用從而使用目標物件,觀察原始碼可以發現 get() 被重寫為永遠返回 null。

那虛引用到底有什麼作用?其實虛引用主要被用來 跟蹤物件被垃圾回收的狀態,通過檢視引用佇列中是否包含物件所對應的虛引用來判斷它是否 即將被垃圾回收,從而採取行動。它並不被期待用來取得目標物件的引用,而目標物件被回收前,它的引用會被放入一個 ReferenceQueue 物件中,從而達到跟蹤物件垃圾回收的作用。

關於java引用型別的實現和原理可以閱讀之前的文章Reference 、ReferenceQueue 詳解 和 Java 引用型別簡述。

關於linux的核心態和使用者態

  • 核心態:控制計算機的硬體資源,並提供上層應用程式執行的環境。比如socket I/0操作或者檔案的讀寫操作等
  • 使用者態:上層應用程式的活動空間,應用程式的執行必須依託於核心提供的資源。
  • 系統呼叫:為了使上層應用能夠訪問到這些資源,核心為上層應用提供訪問的介面。

因此我們可以得知當我們通過JNI呼叫的native方法實際上就是從使用者態切換到了核心態的一種方式。並且通過該系統呼叫使用作業系統所提供的功能。

Q:為什麼需要使用者程序(位於使用者態中)要通過系統呼叫(Java中即使JNI)來呼叫核心態中的資源,或者說呼叫作業系統的服務了?
A:intel cpu提供Ring0-Ring3四種級別的執行模式,Ring0級別最高,Ring3最低。Linux使用了Ring3級別執行使用者態,Ring0作為核心態。Ring3狀態不能訪問Ring0的地址空間,包括程式碼和資料。因此使用者態是沒有許可權去操作核心態的資源的,它只能通過系統呼叫外完成使用者態到核心態的切換,然後在完成相關操作後再有核心態切換回使用者態。

DirectByteBuffer ———— 直接緩衝

DirectByteBuffer是Java用於實現堆外記憶體的一個重要類,我們可以通過該類實現堆外記憶體的建立、使用和銷燬。

DirectByteBuffer該類本身還是位於Java記憶體模型的堆中。堆內記憶體是JVM可以直接管控、操縱。

而DirectByteBuffer中的unsafe.allocateMemory(size);是個一個native方法,這個方法分配的是堆外記憶體,通過C的malloc來進行分配的。分配的記憶體是系統本地的記憶體,並不在Java的記憶體中,也不屬於JVM管控範圍,所以在DirectByteBuffer一定會存在某種方式來操縱堆外記憶體。

在DirectByteBuffer的父類Buffer中有個address屬性:

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

address只會被直接快取給使用到。之所以將address屬性升級放在Buffer中,是為了在JNI呼叫GetDirectBufferAddress時提升它呼叫的速率。
address表示分配的堆外記憶體的地址。

unsafe.allocateMemory(size);分配完堆外記憶體後就會返回分配的堆外記憶體基地址,並將這個地址賦值給了address屬性。這樣我們後面通過JNI對這個堆外記憶體操作時都是通過這個address來實現的了。

在前面我們說過,在linux中核心態的許可權是最高的,那麼在核心態的場景下,作業系統是可以訪問任何一個記憶體區域的,所以作業系統是可以訪問到Java堆的這個記憶體區域的。

Q:那為什麼作業系統不直接訪問Java堆內的記憶體區域了?
A:這是因為JNI方法訪問的記憶體區域是一個已經確定了的記憶體區域地質,那麼該記憶體地址指向的是Java堆內記憶體的話,那麼如果在作業系統正在訪問這個記憶體地址的時候,Java在這個時候進行了GC操作,而GC操作會涉及到資料的移動操作[GC經常會進行先標誌在壓縮的操作。即,將可回收的空間做標誌,然後清空標誌位置的記憶體,然後會進行一個壓縮,壓縮就會涉及到物件的移動,移動的目的是為了騰出一塊更加完整、連續的記憶體空間,以容納更大的新物件],資料的移動會使JNI呼叫的資料錯亂。所以JNI呼叫的記憶體是不能進行GC操作的。

Q:如上面所說,JNI呼叫的記憶體是不能進行GC操作的,那該如何解決了?
A:①堆內記憶體與堆外記憶體之間資料拷貝的方式(並且在將堆內記憶體拷貝到堆外記憶體的過程JVM會保證不會進行GC操作):比如我們要完成一個從檔案中讀資料到堆內記憶體的操作,即FileChannelImpl.read(HeapByteBuffer)。這裡實際上File I/O會將資料讀到堆外記憶體中,然後堆外記憶體再講資料拷貝到堆內記憶體,這樣我們就讀到了檔案中的記憶體。

12345678910111213141516171819202122232425262728 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 {``// 分配臨時的堆外記憶體``ByteBuffer var5 = Util.getTemporaryDirectBuffer(var1.remaining());``int var7;``try {``// File I/O 操作會將資料讀入到堆外記憶體中``int var6 = readIntoNativeBuffer(var0, var5, var2, var4);``var5.flip();``if (var6 > 0``) {``// 將堆外記憶體的資料拷貝到堆外記憶體中``var1.put(var5);``}``var7 = var6;``} finally {``// 裡面會呼叫DirectBuffer.cleaner().clean()來釋放臨時的堆外記憶體``Util.offerFirstTemporaryDirectBuffer(var5);``}``return var7;``}``}

而寫操作則反之,我們會將堆內記憶體的資料線寫到對堆外記憶體中,然後作業系統會將堆外記憶體的資料寫入到檔案中。

② 直接使用堆外記憶體,如DirectByteBuffer:這種方式是直接在堆外分配一個記憶體(即,native memory)來儲存資料,程式通過JNI直接將資料讀/寫到堆外記憶體中。因為資料直接寫入到了堆外記憶體中,所以這種方式就不會再在JVM管控的堆內再分配記憶體來儲存資料了,也就不存在堆內記憶體和堆外記憶體資料拷貝的操作了。這樣在進行I/O操作時,只需要將這個堆外記憶體地址傳給JNI的I/O的函式就好了。

DirectByteBuffer堆外記憶體的建立和回收的原始碼解讀

堆外記憶體分配

123456789101112131415161718192021222324252627 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``));``// 保留總分配記憶體(按頁分配)的大小和實際記憶體的大小``Bits.reserveMemory(size, cap);``long base = 0``;``try {``// 通過unsafe.allocateMemory分配堆外記憶體,並返回堆外記憶體的基地址``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物件的垃圾回收,以實現當DirectByteBuffer被垃圾回收時,堆外記憶體也會被釋放``cleaner = Cleaner.create(``this``, new Deallocator(base, size, cap));``att = null``;``}

Bits.reserveMemory(size, cap) 方法

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960 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();``}``}``}

該方法用於在系統中儲存總分配記憶體(按頁分配)的大小和實際記憶體的大小。

其中,如果系統中記憶體( 即,堆外記憶體 )不夠的話:

12345678910 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()會觸發一次非堵塞的Reference#tryHandlePending(false)。該方法會將已經被JVM垃圾回收的DirectBuffer物件的堆外記憶體釋放。
因為在Reference的靜態程式碼塊中定義了:

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

如果在進行一次堆外記憶體資源回收後,還不夠進行本次堆外記憶體分配的話,則

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

System.gc()會觸發一個full gc,當然前提是你沒有顯示的設定-XX:+DisableExplicitGC來禁用顯式GC。並且你需要知道,呼叫System.gc()並不能夠保證full gc馬上就能被執行。

所以在後面打程式碼中,會進行最多9次嘗試,看是否有足夠的可用堆外記憶體來分配堆外記憶體。並且每次嘗試之前,都對延遲等待時間,已給JVM足夠的時間去完成full gc操作。如果9次嘗試後依舊沒有足夠的可用堆外記憶體來分配本次堆外記憶體,則丟擲OutOfMemoryError(“Direct buffer memory”)異常。

注意,這裡之所以用使用full gc的很重要的一個原因是:System.gc()會對新生代的老生代都會進行記憶體回收,這樣會比較徹底地回收DirectByteBuffer物件以及他們關聯的堆外記憶體.
DirectByteBuffer物件本身其實是很小的,但是它後面可能關聯了一個非常大的堆外記憶體,因此我們通常稱之為冰山物件.

我們做ygc的時候會將新生代裡的不可達的DirectByteBuffer物件及其堆外記憶體回收了,但是無法對old裡的DirectByteBuffer物件及其堆外記憶體進行回收,這也是我們通常碰到的最大的問題。( 並且堆外記憶體多用於生命期中等或較長的物件 )
如果有大量的DirectByteBuffer物件移到了old,但是又一直沒有做cms gc或者full gc,而只進行ygc,那麼我們的實體記憶體可能被慢慢耗光,但是我們還不知道發生了什麼,因為heap明明剩餘的記憶體還很多(前提是我們禁用了System.gc – JVM引數DisableExplicitGC)。

總的來說,Bits.reserveMemory(size, cap)方法在可用堆外記憶體不足以分配給當前要建立的堆外記憶體大小時,會實現以下的步驟來嘗試完成本次堆外記憶體的建立:

① 觸發一次非堵塞的Reference#tryHandlePending(false)。該方法會將已經被JVM垃圾回收的DirectBuffer物件的堆外記憶體釋放。
② 如果進行一次堆外記憶體資源回收後,還不夠進行本次堆外記憶體分配的話,則進行 System.gc()。System.gc()會觸發一個full gc,但你需要知道,呼叫System.gc()並不能夠保證full gc馬上就能被執行。所以在後面打程式碼中,會進行最多9次嘗試,看是否有足夠的可用堆外記憶體來分配堆外記憶體。並且每次嘗試之前,都對延遲等待時間,已給JVM足夠的時間去完成full gc操作。
注意,如果你設定了-XX:+DisableExplicitGC,將會禁用顯示GC,這會使System.gc()呼叫無效。
③ 如果9次嘗試後依舊沒有足夠的可用堆外記憶體來分配本次堆外記憶體,則丟擲OutOfMemoryError(“Direct buffer memory”)異常。

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

1234567891011 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的大小就是預設的堆外記憶體的大小了。

堆外記憶體回收

Cleaner是PhantomReference的子類,並通過自身的next和prev欄位維護的一個雙向連結串列。PhantomReference的作用在於跟蹤垃圾回收過程,並不會對物件的垃圾回收過程造成任何的影響。
所以cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); 用於對當前構造的DirectByteBuffer物件的垃圾回收過程進行跟蹤。
當DirectByteBuffer物件從pending狀態 ——> enqueue狀態時,會觸發Cleaner的clean(),而Cleaner的clean()的方法會實現通過unsafe對堆外記憶體的釋放。

 

雖然Cleaner不會呼叫到Reference.clear(),但Cleaner的clean()方法呼叫了remove(this),即將當前Cleaner從Cleaner連結串列中移除,這樣當clean()執行完後,Cleaner就是一個無引用指向的物件了,也就是可被GC回收的物件。

thunk方法:

通過配置引數的方式來回收堆外記憶體

同時我們可以通過-XX:MaxDirectMemorySize來指定最大的堆外記憶體大小,當使用達到了閾值的時候將呼叫System.gc()來做一次full gc,以此來回收掉沒有被使用的堆外記憶體。

堆外記憶體那些事

使用堆外記憶體的原因

  • 對垃圾回收停頓的改善

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

  • 在某些場景下可以提升程式I/O操縱的效能。少去了將資料從堆內記憶體拷貝到堆外記憶體的步驟。

什麼情況下使用堆外記憶體

  • 堆外記憶體適用於生命週期中等或較長的物件。( 如果是生命週期較短的物件,在YGC的時候就被回收了,就不存在大記憶體且生命週期較長的物件在FGC對應用造成的效能影響 )。
  • 直接的檔案拷貝操作,或者I/O操作。直接使用堆外記憶體就能少去記憶體從使用者記憶體拷貝到系統記憶體的操作,因為I/O操作是系統核心記憶體和裝置間的通訊,而不是通過程式直接和外設通訊的。
  • 同時,還可以使用 池+堆外記憶體 的組合方式,來對生命週期較短,但涉及到I/O操作的物件進行堆外記憶體的再使用。( Netty中就使用了該方式 )

堆外記憶體 VS 記憶體池

  • 記憶體池:主要用於兩類物件:①生命週期較短,且結構簡單的物件,在記憶體池中重複利用這些物件能增加CPU快取的命中率,從而提高效能;②載入含有大量重複物件的大片資料,此時使用記憶體池能減少垃圾回收的時間。
  • 堆外記憶體:它和記憶體池一樣,也能縮短垃圾回收時間,但是它適用的物件和記憶體池完全相反。記憶體池往往適用於生命期較短的可變物件,而生命期中等或較長的物件,正是堆外記憶體要解決的。

堆外記憶體的特點

  • 對於大記憶體有良好的伸縮性
  • 對垃圾回收停頓的改善可以明顯感覺到
  • 在程序間可以共享,減少虛擬機器間的複製

堆外記憶體的一些問題

  • 堆外記憶體回收問題,以及堆外記憶體的洩漏問題。這個在上面的原始碼解析已經提到了
  • 堆外記憶體的資料結構問題:堆外記憶體最大的問題就是你的資料結構變得不那麼直觀,如果資料結構比較複雜,就要對它進行序列化(serialization),而序列化本身也會影響效能。另一個問題是由於你可以使用更大的記憶體,你可能開始擔心虛擬記憶體(即硬碟)的速度對你的影響了。

參考

  • http://lovestblog.cn/blog/2015/05/12/direct-buffer/
  • http://www.infoq.com/cn/news/2014/12/external-memory-heap-memory
  • http://www.jianshu.com/p/85e931636f27
  • 聖思園《併發與Netty》課程

微信公眾號【Java技術江湖】一位阿里 Java 工程師的技術小站。(關注公眾號後回覆”Java“即可領取 Java基礎、進階、專案和架構師等免費學習資料,更有資料庫、分散式、微服務等熱門技術學習視訊,內容豐富,兼顧原理和實踐,另外也將贈送作者原創的Java學習指南、Java程式設計師面試指南等乾貨資源)

相關推薦

Java網路程式設計NIO8淺析mmapDirect Buffer

微信公眾號【黃小斜】作者是螞蟻金服 JAVA 工程師,目前在螞蟻財富負責後端開發工作,專注於 JAVA 後端技術棧,同時也懂點投資理財,堅持學習和寫作,用大廠程式設計師的視角解讀技術與網際網路,我的世界裡不只有 coding!關注公眾號後回覆”架構師“即可領取 Java基礎、進階、專案和架構師等免費學習資

Java網絡編程NIO8淺析mmapDirect Buffer

temp 行動 訪問 objects swa oca 空閑 long 內存操作 Java網絡編程與NIO詳解8:淺析mmap和Direct Buffer 本系列文章首發於我的個人博客:https://h2pl.github.io/ 歡迎閱覽我的CSDN專欄:Java網絡編程

Java網路程式設計NIO4淺析NIO包中的Buffer、Channel Selector

Java NIO:Buffer、Channel 和 Selector轉自https://www.javadoop.com/post/nio-and-aio本文將介紹 Java NIO 中三大元件 Buffer、Channel、Selector 的使用。本來要一起介紹非阻塞 I

Java網路程式設計NIO2JAVA NIO 一步步構建I/O多路複用的請求模型

微信公眾號【黃小斜】作者是螞蟻金服 JAVA 工程師,專注於 JAVA 後端技術棧:SpringBoot、SSM全家桶、MySQL、分散式、中介軟體、微服務,同時也懂點投資理財,堅持學習和寫作,相信終身學習的力量!關注公眾號後回覆”架構師“即可領取 Java基礎、進階、專案和架構師等免費學習資料,更有資料

深入Java網路程式設計NIO(二)

Java NIO 與 Netty NIO NIO的特性/NIO與IO區別: 1)IO是面向流的,NIO是面向緩衝區的; 2)IO流是阻塞的,NIO流是不阻塞的; 3)NIO有選擇器,而IO沒有。 讀資料和寫資料方式: 從通道進行資料讀取 :建立一個緩衝區,然後請求通道讀取資料。

Java網路程式設計NIO

1. 計算機網路程式設計基礎 1.七層模型 七層模型(OSI,Open System Interconnection參考模型),是參考是國際標準化組織制定的一個用於計算機或通訊系統間互聯的標準體系。它是一個七層抽象的模型,不僅包括一系列抽象的術語和概念,也包括具體的協議。 經典的描述如下:

Java網路程式設計NIO3IO模型Java網路程式設計模型

微信公眾號【Java技術江湖】一位阿里 Java 工程師的技術小站。(關注公眾號後回覆”Java“即可領取 Java基礎、進階、專案和架構師等免費學習資料,更有資料庫、分散式、微服務等熱門技術學習視訊,內容豐富,兼顧原理和實踐,另外也將贈送作者原創的Java學習指南、Java程式設計師面試指南等乾貨資源)

Java網路程式設計NIO開篇Java網路程式設計基礎

老曹眼中的網路程式設計基礎 轉自:https://mp.weixin.qq.com/s/XXMz5uAFSsPdg38bth2jAA 我們是幸運的,因為我們擁有網路。網路是一個神奇的東西,它改變了你和我的生活方式,改變了整個世界。 然而,網路的無標度和

Visual C++網路程式設計經典案例 第3章 多執行緒非同步套接字程式設計 實現執行緒同步 互斥物件 使用API函式操作互斥物件

互斥物件和臨界區物件和事件物件作用一樣 用於實現執行緒同步 互斥物件可以線上程中使用 CreateMutex()建立並返回互斥物件 原型如下 HANDLE CreateMutex(   LPSECURITY_ATTIRIBUTES lpMutexAttributes,  

Visual C++網路程式設計經典案例 第3章 多執行緒非同步套接字程式設計 實現執行緒同步 互斥物件 程式的唯一執行

互斥物件可在程序中使用 使用者在程序建立互斥物件實現程式例項唯一執行 建立控制檯工程 #include<windows.h>                                //包含標頭檔案 #include<stdio.h> in

Visual C++網路程式設計經典案例 第3章 多執行緒非同步套接字程式設計 程序間通訊 命名管道 命名管道例項

vc新增控制檯工程 名字命名管道例項 新增原始檔 名字 伺服器 #include<windows.h>                                //包含標頭檔案 #include<stdio.h> int main() {  

Java web程式設計之基礎理論(計算機網路基礎,HTTP請求的完成過程)

1.計算機網路基礎知識 首先我們需明確通訊系統互聯參考模型: OSI/RM模型與TCP/IP模型:                  OSI/RM模型是一種事實上被TCP/IP模型淘汰的模型,在當今世界上沒有大規模使用。當發生HTTP請求時,傳送方傳送的資料是由最頂

Java網絡編程NIO3IO模型Java網絡編程模型

用戶 分組 重新 spa 編譯 linux操作 計算 再次 簡化 Java網絡編程和NIO詳解3:IO模型與Java網絡編程模型 基本概念說明 用戶空間與內核空間 現在操作系統都是采用虛擬存儲器,那麽對32位操作系統而言,它的尋址空間(虛擬存儲空間)為4G(2的32次方)。

Visual C++網路程式設計經典案例 第2章 Winsock網路程式開發流程 基於UDP的Sockets程式設計 UDP伺服器

vc建立控制檯程式視窗的應用程式 命名為 UDP伺服器UDPSever.cpp #include<winsock2.h> #include<stdio.h> #include<windows.h> #pragma comment(lib,

Visual C++網路程式設計經典案例 第2章 Winsock網路程式開發流程 基於UDP的Sockets程式設計 UDP客戶端

在VC中建立UDP客戶端程式 控制檯 #include<winsock2.h> #include<stdio.h> #include<windows.h> #pragma comment(lib,"WS2_32.lib") int mai

Visual C++網路程式設計經典案例 第2章 Winsock網路程式開發流程 TCP伺服器程式 介面初始化

與TCP客戶端一樣 伺服器程式啟動時需要介面初始化 不管伺服器在初始化時 還應該同時完成套接字的建立以及地址繫結等處理工作 class CTCPDlg : public CDialog { // Construction public:     CTCPDlg(CWnd*

Visual C++網路程式設計經典案例 第4章 FTP瀏覽器 登入FTP伺服器 連線FTP伺服器

在對FTP檔案進行相關處理之前 必須在成功連線 登入伺服器以後 才可以執行相關的操作 因為FTP連線是基於Windows套接字程式設計的 所以FTP的連線過程和Socket連線一樣 也就是客戶端建立連線套接字以後 呼叫函式Connect()向伺服器連線請求 使用者需要特別

Visual C++網路程式設計經典案例 第5章 網頁瀏覽器 製作個性化介面 工具欄程式設計 新增工具欄按鈕

製作網頁瀏覽器時 使用者還需要新增一些功能。 例如 使用者重新整理上一步下一步 和瀏覽記錄等。 這些操作在VC中實現非常簡單。 首先在資源管理器中,展開Toolbar項 新增四個工具欄按鈕 按鈕的ID分別為 ID_VIEWRECORD、ID_PRE、ID_NEXT、ID_REFR

Visual C++網路程式設計經典案例 第5章 網頁瀏覽器 製作個性化介面 工具欄程式設計 OnCreate()函式

在這裡 使用者需要用到MFC中CReBar類 該類相當於一個容器 可以將多個控制元件組合在一起 程式碼如下 int CMainFrame::OnCreate(LPCREATESTRUCT lpCreateStruct) { ...//省略部分程式碼 if(dlg.m_hWnd==NU

Visual C++網路程式設計經典案例 第5章 網頁瀏覽器 製作個性化介面 工具欄程式設計 新增對話方塊到工具欄

將ID為ID_DIALOG 的對話方塊新增到工具欄中, 使用者需要將類物件設定為CMainFrame類的成員變數 首先,在CMainFrame類的標頭檔案"MainFrm.h"開頭處 新增CTooldlg類的標頭檔案“Tooldlg.h” ... //省略部分程式碼 #includ