对于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