一、什么是堆外内存
1、堆内内存(on-heap memory)回顾
堆外内存和堆内内存是相对的二个概念,其中堆内内存是我们平常工作中接触比较多的,我们在jvm参数中只要使用-Xms,-Xmx等参数就可以设置堆的大小和最大值,理解jvm的堆还需要知道下面这个公式:
堆内内存 = 新生代+老年代+持久代
在使用堆内内存(on-heap memory)的时候,完全遵守JVM虚拟机的内存管理机制,采用垃圾回收器(GC)统一进行内存管理,GC会在某些特定的时间点进行一次彻底回收,也就是Full GC,GC会对所有分配的堆内内存进行扫描,在这个过程中会对JAVA应用程序的性能造成一定影响,还可能会产生Stop The World。
常见的垃圾回收算法主要有:
• 引用计数器法(Reference Counting)
• 标记清除法(Mark-Sweep)
• 复制算法(Coping)
• 标记压缩法(Mark-Compact)
• 分代算法(Generational Collecting)
• 分区算法(Region)
注:在这里我们不对各个算法进行深入介绍,感兴趣的同学可以关注我的下一篇关于垃圾回收算法的介绍分享。
2、堆外内存(off-heap memory)介绍
和堆内内存相对应,堆外内存就是把内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收对应用程序造成的影响。
作为JAVA开发者我们经常用java.nio.DirectByteBuffer对象进行堆外内存的管理和使用,它会在对象创建的时候就分配堆外内存。
DirectByteBuffer类是在Java Heap外分配内存,对堆外内存的申请主要是通过成员变量unsafe来操作。
二、使用堆外内存的优点
1、减少了垃圾回收
因为垃圾回收会暂停其他的工作。
2、加快了复制的速度
堆内在flush到远程时,会先复制到直接内存(非堆内存),然后在发送;而堆外内存相当于省略掉了这个工作。
同样任何一个事物使用起来有优点就会有缺点,堆外内存的缺点就是内存难以控制,使用了堆外内存就间接失去了JVM管理内存的可行性,改由自己来管理,当发生内存溢出时排查起来非常困难。
三、使用DirectByteBuffer的注意事项
java.nio.DirectByteBuffer对象在创建过程中会先通过Unsafe接口直接通过os::malloc来分配内存,然后将内存的起始地址和大小存到java.nio.DirectByteBuffer对象里,这样就可以直接操作这些内存。这些内存只有在DirectByteBuffer回收掉之后才有机会被回收,因此如果这些对象大部分都移到了old,但是一直没有触发CMS GC或者Full GC,那么悲剧将会发生,因为你的物理内存被他们耗尽了,因此为了避免这种悲剧的发生,通过-XX:MaxDirectMemorySize来指定最大的堆外内存大小,当使用达到了阈值的时候将调用System.gc来做一次full gc,以此来回收掉没有被使用的堆外内存。
五、细说System.gc方法
1、JDK里的System.gc的实现
public static void gc() {
Runtime.getRuntime().gc();
}
其实发现System.gc方法其实是调用的Runtime.getRuntime.gc(),我们再接着看。
/*
运行垃圾收集器。
调用此方法表明,java虚拟机扩展
努力回收未使用的对象,以便内存可以快速复用,
当控制从方法调用返回的时候,虚拟机尽力回收被丢弃的对象
*/
public native void gc();
这里看到gc方法是native的,在java层面只能到此结束了,代码只有这么多,要了解更多,可以看方法上面的注释,不过我们需要更深层次地来了解其实现,那还是准备好进入到jvm里去看看。
2、System.gc的作用有哪些
说起堆外内存免不了要提及System.gc方法,下面就是使用了System.gc的作用是什么?
• 做一次full gc
• 执行后会暂停整个进程。
• System.gc我们可以禁掉,使用-XX:+DisableExplicitGC,
其实一般在cms gc下我们通过-XX:+ExplicitGCInvokesConcurrent也可以做稍微高效一点的gc,也就是并行gc。
• 最常见的场景是RMI/NIO下的堆外内存分配等
说起Full gc我们最先想到的就是stop thd world,这里要先提到VMThread,在jvm里有这么一个线程不断轮询它的队列,这个队列里主要是存一些VM_operation的动作,比如最常见的就是内存分配失败要求做GC操作的请求等,在对gc这些操作执行的时候会先将其他业务线程都进入到安全点,也就是这些线程从此不再执行任何字节码指令,只有当出了安全点的时候才让他们继续执行原来的指令,因此这其实就是我们说的stop the world(STW),整个进程相当于静止了。