神刀安全网

从0到1起步-跟我进入堆外内存的奇妙世界

堆外内存一直是Java业务开发人员难以企及的隐藏领域,究竟他是干什么的,以及如何更好的使用呢?那就请跟着我进入这个世界吧。

一、什么是堆外内存

1、广义的堆外内存

说到堆外内存,那大家肯定想到堆内内存,这也是我们大家接触最多的,我们在jvm参数里通常设置-Xmx来指定我们的堆的最大值,不过这还不是我们理解的Java堆,-Xmx的值是新生代和老生代的和的最大值,我们在jvm参数里通常还会加一个参数-XX:MaxPermSize来指定持久代的最大值,那么我们认识的Java堆的最大值其实是-Xmx和-XX:MaxPermSize的总和,在分代算法下,新生代,老生代和持久代是连续的虚拟地址,因为它们是一起分配的,那么剩下的都可以认为是堆外内存(广义的)了,这些包括了jvm本身在运行过程中分配的内存,codecache,jni里分配的内存,DirectByteBuffer分配的内存等等。

2、狭义的堆外内存作为java开发者,我们常说的堆外内存溢出了,其实是狭义的堆外内存,这个主要是指java.nio.DirectByteBuffer在创建的时候分配内存,我们这篇文章里也主要是讲狭义的堆外内存,因为它和我们平时碰到的问题比较密切。

Java中分配的非空对象都是由Java虚拟机的垃圾收集器管理的,也称为堆内内存(on-heap memory)。虚拟机会定期对垃圾内存进行回收,在某些特定的时间点,它会进行一次彻底的回收(full gc)。彻底回收时,垃圾收集器会对所有分配的堆内内存进行完整的扫描,这意味着一个重要的事实——这样一次垃圾收集对Java应用造成的影响,跟堆的大小是成正比的。过大的堆会影响Java应用的性能。

对于这个问题,一种解决方案就是使用堆外内存(off-heap memory)。堆外内存意味着把内存对象分配在Java虚拟机的堆以外的内存,这些内存直接受操作系统管理(而不是虚拟机)。这样做的结果就是能保持一个较小的堆,以减少垃圾收集对应用的影响。

DirectByteBuffer类是在Java Heap外分配内存,对堆外内存的申请主要是通过成员变量unsafe来操作,下面介绍构造方法

DirectByteBuffer(int cap) {                           super(-1, 0, cap, cap);         //内存是否按页分配对齐         boolean pa = VM.isDirectMemoryPageAligned();         //获取每页内存大小         int ps = Bits.pageSize();         //分配内存的大小,如果是按页对齐方式,需要再加一页内存的容量         long size = Math.max(1L, (long)cap + (pa ? ps : 0));         //用Bits类保存总分配内存(按页分配)的大小和实际内存的大小         Bits.reserveMemory(size, cap);          long base = 0;         try {            //在堆外内存的基地址,指定内存大小             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 = Cleaner.create(this, new Deallocator(base, size, cap));         att = null;     }

注:在Cleaner 内部中通过一个列表,维护了一个针对每一个 directBuffer 的一个回收堆外内存的 线程对象(Runnable),回收操作是发生在 Cleaner 的 clean() 方法中。

private static class Deallocator implements Runnable  {     private static Unsafe unsafe = Unsafe.getUnsafe();     private long address;     private long size;     private int capacity;     private Deallocator(long address, long size, int capacity) {         assert (address != 0);         this.address = address;         this.size = size;         this.capacity = capacity;     }      public void run() {         if (address == 0) {             // Paranoia             return;         }         unsafe.freeMemory(address);         address = 0;         Bits.unreserveMemory(size, capacity);     } }

二、使用堆外内存的优点

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,以此来回收掉没有被使用的堆外内存。

四、DirectByteBuffer使用测试

我们在写NIO程序经常使用ByteBuffer来读取或者写入数据,那么使用ByteBuffer.allocate(capability)还是使用ByteBuffer.allocteDirect(capability)来分配缓存了?第一种方式是分配JVM堆内存,属于GC管辖范围,由于需要拷贝所以速度相对较慢;第二种方式是分配OS本地内存,不属于GC管辖范围,由于不需要内存拷贝所以速度相对较快。

代码如下:

package com.stevex.app.nio;  import java.nio.ByteBuffer; import java.util.concurrent.TimeUnit;  public class DirectByteBufferTest {     public static void main(String[] args) throws InterruptedException{             //分配128MB直接内存         ByteBuffer bb = ByteBuffer.allocateDirect(1024*1024*128);          TimeUnit.SECONDS.sleep(10);         System.out.println("ok");     } }

测试用例1:设置JVM参数-Xmx100m,运行异常,因为如果没设置-XX:MaxDirectMemorySize,则默认与-Xmx参数值相同,分配128M直接内存超出限制范围。

Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory     at java.nio.Bits.reserveMemory(Bits.java:658)     at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)     at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:306)     at com.stevex.app.nio.DirectByteBufferTest.main(DirectByteBufferTest.java:8)

测试用例2:设置JVM参数-Xmx256m,运行正常,因为128M小于256M,属于范围内分配。

测试用例3:设置JVM参数-Xmx256m -XX:MaxDirectMemorySize=100M,运行异常,分配的直接内存128M超过限定的100M。

Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory     at java.nio.Bits.reserveMemory(Bits.java:658)     at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:123)     at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:306)     at com.stevex.app.nio.DirectByteBufferTest.main(DirectByteBufferTest.java:8)

测试用例4:设置JVM参数-Xmx768m,运行程序观察内存使用变化,会发现clean()后内存马上下降,说明使用clean()方法能有效及时回收直接缓存。
代码如下:

package com.stevex.app.nio;  import java.nio.ByteBuffer; import java.util.concurrent.TimeUnit; import sun.nio.ch.DirectBuffer;  public class DirectByteBufferTest {     public static void main(String[] args) throws InterruptedException{         //分配512MB直接缓存         ByteBuffer bb = ByteBuffer.allocateDirect(1024*1024*512);          TimeUnit.SECONDS.sleep(10);          //清除直接缓存         ((DirectBuffer)bb).cleaner().clean();          TimeUnit.SECONDS.sleep(10);          System.out.println("ok");     } }

五、细说System.gc方法

1、JDK里的System.gc的实现

/**  * Runs the garbage collector.  * <p>  * Calling the <code>gc</code> method suggests that the Java Virtual  * Machine expend effort toward recycling unused objects in order to  * make the memory they currently occupy available for quick reuse.  * When control returns from the method call, the Java Virtual  * Machine has made a best effort to reclaim space from all discarded  * objects.  * <p>  * The call <code>System.gc()</code> is effectively equivalent to the  * call:  * <blockquote><pre>  * Runtime.getRuntime().gc()  * </pre></blockquote>  *  * @see     java.lang.Runtime#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下的堆外内存分配等

注:
如果我们使用了堆外内存,并且用了DisableExplicitGC设置为true,那么就是禁止使用System.gc,这样堆外内存将无从触发极有可能造成内存溢出错误,在这种情况下可以考虑使用ExplicitGCInvokesConcurrent参数。
说起Full gc我们最先想到的就是stop thd world,这里要先提到VMThread,在jvm里有这么一个线程不断轮询它的队列,这个队列里主要是存一些VM_operation的动作,比如最常见的就是内存分配失败要求做GC操作的请求等,在对gc这些操作执行的时候会先将其他业务线程都进入到安全点,也就是这些线程从此不再执行任何字节码指令,只有当出了安全点的时候才让他们继续执行原来的指令,因此这其实就是我们说的stop the world(STW),整个进程相当于静止了

转载本站任何文章请注明:转载至神刀安全网,谢谢神刀安全网 » 从0到1起步-跟我进入堆外内存的奇妙世界

分享到:更多 ()

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址