對於JVM的內存規則,應該是老生常談的東西了,這裏我就簡單的說下:html
新生代:通常來講新建立的對象都分配在這裏。java
年老代:通過幾回垃圾回收,新生代的對象就會放在年老代裏面。年老代中的對象保存的時間更久。數據庫
永久代:這裏面存放的是class相關的信息,通常是不會進行垃圾回收的。數組
因爲JVM會替咱們執行垃圾回收,所以開發者根本不須要關心對象的釋放。可是若是不瞭解其中的原委,很容易內存泄漏,只能兩眼望天了!緩存
垃圾回收,大體能夠分爲下面幾種:ruby
Minor GC:當新建立對象,內存空間不夠的時候,就會執行這個垃圾回收。因爲執行最頻繁,所以通常採用複製回收機制。框架
Major GC:清理年老代的內存,這裏通常採用的是標記清除+標記整理機制。eclipse
Full GC:有的說與Major GC差很少,有的說至關於執行minor+major回收,那麼咱們暫且能夠認爲Full GC就是全面的垃圾回收吧。jvm
從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的版本看可否解決問題。
由前面的文章可知,堆外內存分配很簡單,直接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對象是否有引用,如沒有則同時也會回收其佔用的堆外內存。
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 方法來作清理工做。
根據上面的源碼分析,咱們能夠想到堆外內存回收的幾張方法:
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種狀況的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