Java堆外內存之三:堆外內存回收方法

1、JVM內存的分配及垃圾回收

  對於JVM的內存規則,應該是老生常談的東西了,這裏我就簡單的說下:html

  新生代:通常來講新建立的對象都分配在這裏。java

  年老代:通過幾回垃圾回收,新生代的對象就會放在年老代裏面。年老代中的對象保存的時間更久。數據庫

  永久代:這裏面存放的是class相關的信息,通常是不會進行垃圾回收的。數組

JVM垃圾回收

  因爲JVM會替咱們執行垃圾回收,所以開發者根本不須要關心對象的釋放。可是若是不瞭解其中的原委,很容易內存泄漏,只能兩眼望天了!緩存

  垃圾回收,大體能夠分爲下面幾種:ruby

  Minor GC:當新建立對象,內存空間不夠的時候,就會執行這個垃圾回收。因爲執行最頻繁,所以通常採用複製回收機制。框架

  Major GC:清理年老代的內存,這裏通常採用的是標記清除+標記整理機制。eclipse

  Full GC:有的說與Major GC差很少,有的說至關於執行minor+major回收,那麼咱們暫且能夠認爲Full GC就是全面的垃圾回收吧。jvm

2、堆外內存溢出

從nio時代開始,可使用ByteBuffer等類來操縱堆外內存了,使用ByteBuffer分配本地內存則很是簡單,直接ByteBuffer.allocateDirect(10 * 1024 * 1024)便可,以下:ide

ByteBuffer buffer = ByteBuffer.allocateDirect(numBytes);

像Memcached等等不少緩存框架都會使用堆外內存,以提升效率,反覆讀寫,去除它的GC的影響。能夠經過指定JVM參數來肯定堆外內存大小限制(有的VM默認是無限的,好比JRocket,JVM默認是64M): 

-XX:MaxDirectMemorySize=512m

對於這種direct buffer內存不夠的時候會拋出錯誤: 

java.lang.OutOfMemoryError: Direct buffer memory

對於heap的OOM咱們能夠經過執行jmap -heap來獲取堆內內存狀況,例如如下輸出取自我上週定位的一個問題: 

using parallel threads in the new generation.
using thread-local object allocation.
Concurrent Mark-Sweep GC
 
Heap Configuration:
   MinHeapFreeRatio = 40
   MaxHeapFreeRatio = 70
   MaxHeapSize      = 2147483648 (2048.0MB)
   NewSize          = 16777216 (16.0MB)
   MaxNewSize       = 33554432 (32.0MB)
   OldSize          = 50331648 (48.0MB)
   NewRatio         = 7
   SurvivorRatio    = 8
   PermSize         = 16777216 (16.0MB)
   MaxPermSize      = 67108864 (64.0MB)
 
Heap Usage:
New Generation (Eden + 1 Survivor Space):
   capacity = 30212096 (28.8125MB)
   used     = 11911048 (11.359260559082031MB)
   free     = 18301048 (17.45323944091797MB)
   39.42476549789859% used
Eden Space:
   capacity = 26869760 (25.625MB)
   used     = 11576296 (11.040016174316406MB)
   free     = 15293464 (14.584983825683594MB)
   43.08298994855183% used
From Space:
   capacity = 3342336 (3.1875MB)
   used     = 334752 (0.319244384765625MB)
   free     = 3007584 (2.868255615234375MB)
   10.015510110294118% used
To Space:
   capacity = 3342336 (3.1875MB)
   used     = 0 (0.0MB)
   free     = 3342336 (3.1875MB)
   0.0% used
concurrent mark-sweep generation:
   capacity = 2113929216 (2016.0MB)
   used     = 546999648 (521.6595153808594MB)
   free     = 1566929568 (1494.3404846191406MB)
   25.875968024844216% used
Perm Generation:
   capacity = 45715456 (43.59765625MB)
   used     = 27495544 (26.22179412841797MB)
   free     = 18219912 (17.37586212158203MB)
   60.144962788952604% used

可見堆內存都是正常的,從新回到業務日誌裏尋找異常,發現出如今堆外內存的分配上: 

java.lang.OutOfMemoryError
 at sun.misc.Unsafe.allocateMemory(Native Method)
 at java.nio.DirectByteBuffer.(DirectByteBuffer.java:101)
 at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:288)
 at com.schooner.MemCached.SchoonerSockIOPool$TCPSockIO.(Unknown Source)

對於這個參數分配太小的狀況下形成OOM,不妨執行jmap -histo:live看看(也能夠用JConsole之類的外部觸發GC),由於它會強制一次full GC,若是堆外內存明顯降低,頗有可能就是堆外內存過大引發的OOM。

BTW,若是在執行jmap命令時遇到:

Error attaching to process: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process

這個算是JDK的一個bug(連接),只要是依賴於SA(Serviceability Agent)的工具,好比jinfo/jstack/jmap都會存在這個問題,可是Oracle說了「won’t fix」……

Ubuntu 10.10 and newer has a new default security policy that affects Serviceability commands. 
This policy prevents a process from attaching to another process owned by the same UID if
the target process is not a descendant of the attaching process.

不過它也是給瞭解決方案的,須要修改/etc/sysctl.d/10-ptrace.conf:

kernel.yama.ptrace_scope = 0

堆外內存泄露的問題定位一般比較麻煩,能夠藉助google-perftools這個工具,它能夠輸出不一樣方法申請堆外內存的數量。固然,若是你是64位系統,你須要先安裝libunwind庫

最後,JDK存在一些direct buffer的bug(好比這個這個),可能引起OOM,因此也不妨升級JDK的版本看可否解決問題。

3、堆外內存回收

3.一、ByteBuffer的堆外內存回收

 由前面的文章可知,堆外內存分配很簡單,直接ByteBuffer.allocateDirect(10 * 1024 * 1024)便可。很像C語言。在C語言的內存分配和釋放函數malloc/free,必需要一一對應,不然就會出現內存泄露或者是野指針的非法訪問。java中咱們須要手動釋放獲取的堆外內存嗎?在談到堆外內存優勢時提到「能夠無限使用到1TB」,既然能夠無限使用,那麼會不會用爆內存呢?這個是頗有可能的...因此堆外內存的垃圾回收也很重要。

因爲堆外內存並不直接控制於JVM,所以只能等到full GC的時候才能垃圾回收!(direct buffer歸屬的的JAVA對象是在堆上且可以被GC回收的,一旦它被回收,JVM將釋放direct buffer的堆外空間。前提是沒有關閉DisableExplicitGC

先看一個示例:(堆外內存回收演示)

/**
     * @VM args:-XX:MaxDirectMemorySize=40m -verbose:gc -XX:+PrintGCDetails
     * -XX:+DisableExplicitGC //增長此參數一下子就會內存溢出java.lang.OutOfMemoryError: Direct buffer memory */
    public static void TestDirectByteBuffer() {
        List<ByteBuffer> list = new ArrayList<ByteBuffer>();
        while(true) {
            ByteBuffer buffer = ByteBuffer.allocateDirect(1 * 1024 * 1024);
            //list.add(buffer);
        }
    }

經過NIO的ByteBuffer使用堆外內存,將堆外內存設置爲40M:

場景一:不由用FullGC下的system.gc

運行這段代碼會發現:程序能夠一直運行下去,不會報OutOfMemoryError。若是使用了-verbose:gc -XX:+PrintGCDetails,會發現程序頻繁的進行垃圾回收活動。

結果省略。

場景二:同時JVM徹底忽略系統的GC調用

與以前的JVM啓動參數相比,增長了-XX:+DisableExplicitGC,這個參數做用是禁止顯示調用GC。代碼如何顯示調用GC呢,經過System.gc()函數調用。若是加上了這個JVM啓動參數,那麼代碼中調用System.gc()沒有任何效果,至關因而沒有這行代碼同樣。結果以下:

顯然堆內存(包括新生代和老年代)內存很充足,可是堆外內存溢出了。也就是說NIO直接內存的回收,須要依賴於System.gc()若是咱們的應用中使用了java nio中的direct memory,那麼使用-XX:+DisableExplicitGC必定要當心,存在潛在的內存泄露風險

  從DirectByteBuffer的源碼也能夠分析出來,ByteBuffer.allocateDirect()會調用Bits.reservedMemory()方法,在該方法中顯示調用了System.gc()用戶內存回收,若是-XX:+DisableExplicitGC打開,則讓System.gc()無效,內存沒法有效回收,致使OOM。

     咱們知道java代碼沒法強制JVM什麼時候進行垃圾回收,也就是說垃圾回收這個動做的觸發,徹底由JVM本身控制,它會挑選合適的時機回收堆內存中的無用java對象。代碼中顯示調用System.gc(),只是建議JVM進行垃圾回收,可是到底會不會執行垃圾回收是不肯定的,可能會進行垃圾回收,也可能不會。何時纔是合適的時機呢?通常來講是,系統比較空閒的時候(好比JVM中活動的線程不多的時候),還有就是內存不足,不得不進行垃圾回收。咱們例子中的根本矛盾在於:堆內存由JVM本身管理,堆外內存必需要由咱們本身釋放;堆內存的消耗速度遠遠小於堆外內存的消耗,但要命的是必須先釋放堆內存中的對象,才能釋放堆外內存,可是咱們又不能強制JVM釋放堆內存。

Direct Memory的回收機制:Direct Memory是受GC控制的,例如ByteBuffer bb = ByteBuffer.allocateDirect(1024),這段代碼的執行會在堆外佔用1k的內存,Java堆內只會佔用一個對象的指針引用的大小,堆外的這1k的空間只有當bb對象被回收時,纔會被回收,這裏會發現一個明顯的不對稱現象,就是堆外可能佔用了不少,而堆內沒佔用多少,致使還沒觸發GC,那就很容易出現Direct Memory形成物理內存耗光。

ByteBuffer與Unsafe使用堆外內存在回收時的不一樣:

Direct ByteBuffer分配出去的直接內存其實也是由GC負責回收的,而不像Unsafe是徹底自行管理的,Hotspot在GC時會掃描Direct ByteBuffer對象是否有引用,如沒有則同時也會回收其佔用的堆外內存

GC是如何回收ByteBuffer分配的「直接內存」的,看下面的源碼

  DirectByteBuffer 類有一個內部的靜態類 Deallocator,這個類實現了 Runnable 接口並在 run() 方法內釋放了內存,源碼以下:

 

那這個 Deallocator 線程是哪裏調用了呢?這裏就用到了 Java 的虛引用(PhantomReference),Java 虛引用容許對象被回收以前作一些清理工做。在 DirectByteBuffer 的構造方法中建立了一個 Cleaner:

cleaner = Cleaner.create(this /* 這個是 DirectByteBuffer 對象的引用 */, 
new Deallocator(address, cap) /* 清理線程 */); 

DirectByteBuffer中Deallocator線程如何建立

 

而 Cleaner 類繼承了 PhantomReference 類,而且在本身的 clean() 方法中啓動了清理線程,當 DirectByteBuffer 被 GC 以前 cleaner 對象會被放入一個引用隊列(ReferenceQueue),JVM 會啓動一個低優先級線程掃描這個隊列,而且執行 Cleaner 的 clean 方法來作清理工做。

根據上面的源碼分析,咱們能夠想到堆外內存回收的幾張方法:

  1. Full GC,通常發生在年老代垃圾回收以及調用System.gc的時候,但這樣不一頂能知足咱們的需求。
  2. 調用ByteBuffer的cleaner的clean(),內部仍是調用System.gc(),因此必定不要-XX:+DisableExplicitGC
package xing.test;

import java.nio.ByteBuffer;
import sun.nio.ch.DirectBuffer;

public class NonHeapTest {
    public static void clean(final ByteBuffer byteBuffer) {  
        if (byteBuffer.isDirect()) {  
           ((DirectBuffer)byteBuffer).cleaner().clean();  
        }  
  }  
    
    public static void sleep(long i) {  
        try {  
              Thread.sleep(i);  
         }catch(Exception e) {  
              /*skip*/  
         }  
    }  
    public static void main(String []args) throws Exception {  
           ByteBuffer buffer = ByteBuffer.allocateDirect(1024 * 1024 * 200);  
           System.out.println("start");  
           sleep(5000);  
           clean(buffer);//執行垃圾回收
//         System.gc();//執行Full gc進行垃圾回收
           System.out.println("end");  
           sleep(5000);  
    }  
}

這樣就能手動的控制回收堆外內存了!其中sun.nio實際上是java.nio的內部實現。因此你可能不能經過eclipse的自動排錯找到這個包,直接複製

import sun.nio.ch.DirectBuffer;

顯然堆內存(包括新生代和老年代)內存很充足,可是堆外內存溢出了。也就是說NIO直接內存的回收,須要依賴於System.gc()。若是咱們的應用中使用了java nio中的direct memory,那麼使用-XX:+DisableExplicitGC必定要當心,存在潛在的內存泄露風險

     咱們知道java代碼沒法強制JVM什麼時候進行垃圾回收,也就是說垃圾回收這個動做的觸發,徹底由JVM本身控制,它會挑選合適的時機回收堆內存中的無用java對象。代碼中顯示調用System.gc(),只是建議JVM進行垃圾回收,可是到底會不會執行垃圾回收是不肯定的,可能會進行垃圾回收,也可能不會。何時纔是合適的時機呢?通常來講是,系統比較空閒的時候(好比JVM中活動的線程不多的時候),還有就是內存不足,不得不進行垃圾回收。咱們例子中的根本矛盾在於:堆內存由JVM本身管理,堆外內存必需要由咱們本身釋放;堆內存的消耗速度遠遠小於堆外內存的消耗,但要命的是必須先釋放堆內存中的對象,才能釋放堆外內存,可是咱們又不能強制JVM釋放堆內存。

Direct Memory的回收機制:Direct Memory是受GC控制的,例如ByteBuffer bb = ByteBuffer.allocateDirect(1024),這段代碼的執行會在堆外佔用1k的內存,Java堆內只會佔用一個對象的指針引用的大小,堆外的這1k的空間只有當bb對象被回收時,纔會被回收,這裏會發現一個明顯的不對稱現象,就是堆外可能佔用了不少,而堆內沒佔用多少,致使還沒觸發GC,那就很容易出現Direct Memory形成物理內存耗光。

Direct ByteBuffer分配出去的內存其實也是由GC負責回收的,而不像Unsafe是徹底自行管理的,Hotspot在GC時會掃描Direct ByteBuffer對象是否有引用,如沒有則同時也會回收其佔用的堆外內存。

3.二、正確釋放Unsafe分配的堆外內存

        雖然第3種狀況的ObjectInHeap存在內存泄露,可是這個類的設計是合理的,它很好的封裝了直接內存,這個類的調用者感覺不到直接內存的存在。那怎麼解決ObjectInHeap中的內存泄露問題呢?能夠覆寫Object.finalize(),當堆中的對象即將被垃圾回收器釋放的時候,會調用該對象的finalize。因爲JVM只會幫助咱們管理內存資源,不會幫助咱們管理數據庫鏈接,文件句柄等資源,因此咱們須要在finalize本身釋放資源。

import sun.misc.Unsafe;

public class RevisedObjectInHeap
{
	private long address = 0;

	private Unsafe unsafe = GetUsafeInstance.getUnsafeInstance();

	// 讓對象佔用堆內存,觸發[Full GC
	private byte[] bytes = null;

	public RevisedObjectInHeap()
	{
		address = unsafe.allocateMemory(2 * 1024 * 1024);
		bytes = new byte[1024 * 1024];
	}

	@Override
	protected void finalize() throws Throwable
	{
		super.finalize();
		System.out.println("finalize." + bytes.length);
		unsafe.freeMemory(address);
	}

	public static void main(String[] args)
	{
		while (true)
		{
			RevisedObjectInHeap heap = new RevisedObjectInHeap();
			System.out.println("memory address=" + heap.address);
		}
	}

}

咱們覆蓋了finalize方法,手動釋放分配的堆外內存。若是堆中的對象被回收,那麼相應的也會釋放佔用的堆外內存。這裏有一點須要注意下

// 讓對象佔用堆內存,觸發[Full GC
private byte[] bytes = null;

這行代碼主要目的是爲了觸發堆內存的垃圾回收行爲,順帶執行對象的finalize釋放堆外內存。若是沒有這行代碼或者是分配的字節數組比較小,程序運行一段時間後仍是會報OutOfMemoryError。這是由於每當建立1個RevisedObjectInHeap對象的時候,佔用的堆內存很小(就幾十個字節左右),可是卻須要佔用2M的堆外內存。這樣堆內存還很充足(這種狀況下不會執行堆內存的垃圾回收),可是堆外內存已經不足,因此就不會報OutOfMemoryError。

參考資料

監控使用的directBuffer大小:http://stackoverflow.com/questions/3908520/looking-up-how-much-direct-buffer-memory-is-available-to-java

《應用DirectBuffer提高系統性能》http://www.tbdata.org/archives/801

《Java 的 DirectBuffer 是什麼東西?》http://www.simaliu.com/archives/274.html

相關文章
相關標籤/搜索