DirectByteBuffer堆外内存申请、回收

JVM中对象在内存中的分布如下:

  • 新生代:一般来说新创建的对象都分配在这里;
  • 年老代:经过几次垃圾回收,新生代的对象就会放在年老代里面。年老代中的对象保存的时间更久。
  • 永久代:这里面存放的是class相关的信息,一般是不会进行垃圾回收的。

JVM会替我们执行垃圾回收,主要包括young gc和full gc。jvm内存溢出可以通过jmap -heap或者jstat -gcutil工具来诊断。

1、ByteBuffer堆外内存介绍

ByteBuffer堆外内存使用:

  1. 从nio时代开始,可以使用ByteBuffer等类来操纵堆外内存了,使用ByteBuffer分配本地内存则非常简单,直接:ByteBuffer buffer = ByteBuffer.allocateDirect(10 * 1024 * 1024);
  2. 可以通过指定JVM参数来确定堆外内存大小限制:-XX:MaxDirectMemorySize=512m
  3. 对于这种direct buffer内存不够的时候会抛出错误: java.lang.OutOfMemoryError: Direct buffer memory

堆外内存泄露的问题定位通常比较麻烦,可以借助google-perftools这个工具,它可以输出不同方法申请堆外内存的数量。最后,JDK存在一些direct buffer的bug(比如这个这个),可能引发OOM,所以也不妨升级JDK的版本看能否解决问题。

 

在C语言的内存分配和释放函数malloc/free,必须要一一对应,否则就会出现内存泄露或者是野指针的非法访问。java中ByteBuffer申请的堆外内存需要手动释放吗?ByteBuffer申请的堆外内存也是由GC负责回收的,Hotspot在GC时会扫描Direct ByteBuffer对象是否有引用,如没有,当堆内的引用被gc回收时通过虚拟引用回收其占用的堆外内存!(前提是没有关闭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);
        }
    }

1)-XX:MaxDirectMemorySize=40m -verbose:gc -XX:+PrintGCDetails

代码会一直运行下午,同时会看到系统频繁的进行垃圾回收;

2)-XX:MaxDirectMemorySize=40m -verbose:gc -XX:+PrintGCDetails -XX:+DisableExplicitGC

增加了-XX:+DisableExplicitGC,这个参数作用是禁止显示调用GC。代码如何显示调用GC呢,通过System.gc()函数调用。如果加上了这个JVM启动参数,那么代码中调用System.gc()没有任何效果,相当于是没有这行代码一样。

显然堆内存(包括新生代和老年代)内存很充足,但是堆外内存溢出了。也就是说使用了java nio中的direct memory,那么-XX:+DisableExplicitGC一定要小心,存在潜在的内存泄露风险。(后面会讲到Direct Memory的申请时会使用System.gc()

3)-XX:MaxDirectMemorySize=40m -verbose:gc -XX:+PrintGCDetails

JVM参数把-XX:+DisableExplicitGC去掉,代码将list.add(buffer); 注释放开再次运行,仍然会报java.lang.OutOfMemoryError: Direct buffer memory

原因是堆内list把堆外内存对象的引用一直持有,导致堆内的引用无法被gc,从而堆外内存也无法回收

2、ByteBuffer堆外内存回收的矛盾点:

我们知道java代码无法强制JVM何时进行垃圾回收,也就是说垃圾回收这个动作的触发,完全由JVM自己控制,它会挑选合适的时机回收堆内存中的无用java对象。代码中显示调用System.gc(),只是建议JVM进行垃圾回收,但是到底会不会执行垃圾回收是不确定的,一般来说是,系统比较空闲的时候(比如JVM中活动的线程很少的时候),或是内存不足,才进行垃圾回收。使用ByteBuffer堆外内存回收的矛盾在于:堆内存由JVM自己管理,堆外内存必须要由我们自己释放;堆内存的消耗速度远远小于堆外内存的消耗,但要命的是必须先释放堆内存中的对象(引用),才能释放堆外内存,但是我们又不能强制JVM释放堆内存。例如:ByteBuffer bb = ByteBuffer.allocateDirect(1024),这段代码的执行会在堆外占用1k的内存,Java堆内只会占用一个对象的指针引用的大小,堆外的这1k的空间只有当bb对象被回收时,才会被回收,这里会发现一个明显的不对称现象,就是堆外可能占用了很多,而堆内没占用多少,导致还没触发GC,那就很容易出现Direct Memory造成物理内存耗光。

再者,假设堆内 ByteBuffer对象的引用升级到了老年代,导致这个引用会长期存在无法回收,这时堆外的内存将长期无法得到回收。

 

3、ByteBuffer源码分析,堆外内存申请:

ByteBuffer.allocateDirect(cap);

进行内存申请的时候,会调用:DirectByteBuffer(int cap)构造函数,如下:

DirectByteBuffer(int cap) {                   // package-private
    // 初始化Buffer的四个核心属性
    super(-1, 0, cap, cap);
    // 判断是否需要页面对齐,通过参数-XX:+PageAlignDirectMemory控制,默认为false
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    // 确保有足够内存
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    Bits.reserveMemory(size, cap);
 
    long base = 0;
    try {
        // 调用unsafe方法分配内存
        base = unsafe.allocateMemory(size);
    } catch (OutOfMemoryError x) {
        // 分配失败,释放内存
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    // 初始化内存空间为0
    unsafe.setMemory(base, size, (byte) 0);
    // 设置内存起始地址
    if (pa && (base % ps != 0)) {
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    // 使用Cleaner机制注册内存回收处理函数
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    att = null;
}

 

 

首先,这个构造函数里的Bits.reserveMemory(size, cap)方法判断是否有足够的空间可供申请:

// 该方法主要用于判断申请的堆外内存是否超过了用例指定的最大值
// 如果还有足够空间可以申请,则更新对应的变量
// 如果已经没有空间可以申请,则抛出OOME
// 参数解释:
//     size:根据是否按页对齐,得到的真实需要申请的内存大小
//     cap:用户指定需要的内存大小(<=size)
static void reserveMemory(long size, int cap) {
    // 因为涉及到更新多个静态统计变量,这里需要Bits类锁
    synchronized (Bits.class) {
        // 获取最大可以申请的对外内存大小,默认值是64MB
        // 可以通过参数-XX:MaxDirectMemorySize=<size>设置这个大小
        if (!memoryLimitSet && VM.isBooted()) {
            maxMemory = VM.maxDirectMemory();
            memoryLimitSet = true;
        }
        // -XX:MaxDirectMemorySize限制的是用户申请的大小,而不考虑对齐情况
        // 所以使用两个变量来统计:
        //     reservedMemory:真实的目前保留的空间
        //     totalCapacity:目前用户申请的空间
        if (cap <= maxMemory - totalCapacity) {
            reservedMemory += size;
            totalCapacity += cap;
            count++;
            return; // 如果空间足够,更新统计变量后直接返回
        }
    }
 
    // 如果已经没有足够空间,则尝试GC
    System.gc();
    try {
        Thread.sleep(100);
    } catch (InterruptedException x) {
        // Restore interrupt status
        Thread.currentThread().interrupt();
    }
    synchronized (Bits.class) {
        // GC后再次判断,如果还是没有足够空间,则抛出OOME
        if (totalCapacity + cap > maxMemory)
            throw new OutOfMemoryError("Direct buffer memory");
        reservedMemory += size;
        totalCapacity += cap;
        count++;
    }
}

 

可以看到:

  • 如果空间不足,会调用System.gc()尝试释放内存,然后再进行判断,如果还是没有足够的空间,抛出OOME。
  • 确定有足够的空间后,使用sun.misc.Unsafe#allocateMemory申请内存;
  • 最后,DirectByteBuffer使用Cleaner机制进行空间回收

说明:

  1. sun.misc.Unsafe.allocateMemory这个函数是通过JNI调用C的malloc来申请内存;
  2. 申请内存时,可以通过-XX:+PageAlignDirectMemory:指定申请的内存是否需要按页对齐,默认不对其;
  3. 默认堆外内存大小为可用的最大Java堆大小(后面会讲到)

4、ByteBuffer源码分析,堆外内存回收:

上面我们可以看到,当使用ByteBuffer申请时,在DirectByteBuffer构造函数最后,会注册一个Cleaner的内存回收函数。

堆内的DirectByteBuffer对象本身会被垃圾回收正常的处理,但是堆外的内存就不会被GC回收了,所以需要一个机制,在DirectByteBuffer回收时,同时回收其堆外申请的内存。

Java中可选的特性有finalize函数(对象被gc回收前的准备工作),但是finalize机制是Java官方不推荐的,官方推荐的做法是使用虚引用来处理对象被回收时的后续处理工作。同时Java提供了Cleaner类来简化这个实现,Cleaner是PhantomReference的子类,可以在PhantomReference被加入ReferenceQueue时触发对应的Runnable回调。

DirectByteBuffer就是使用Cleaner机制来实现本身被GC时,回收堆外内存的能力。

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方法释放内存
            unsafe.freeMemory(address);
            address = 0;
            // 更新统计变量
            Bits.unreserveMemory(size, capacity);
        }
 
    }

 

说明:sun.misc.Unsafe.freeMemory方法使用C标准库的free函数释放内存空间。

5、DirectByteBuffer读写逻辑

 

public ByteBuffer put(int i, byte x) {
    unsafe.putByte(ix(checkIndex(i)), ((x)));
    return this;
}
 
public byte get(int i) {
    return ((unsafe.getByte(ix(checkIndex(i)))));
}
 
private long ix(int i) {
    return address + (i << 0);
}

 

DirectByteBuffer使用sun.misc.Unsafe.getByte(long)和sun.misc.Unsafe.putByte(long, byte)这两个方法来读写堆外内存空间的指定位置的字节数据。不过这两个方法本地实现比较复杂,这里就不分析了。

6、默认可以申请的堆外内存大小:

用户可以通过-XX:MaxDirectMemorySize=<size>这个参数来控制可以申请多大的DirectByteBuffer内存。但是默认情况下这个大小是多少呢?

// A user-settable upper limit on the maximum amount of allocatable direct
// buffer memory.  This value may be changed during VM initialization if
// "java" is launched with "-XX:MaxDirectMemorySize=<size>".
//
// The initial value of this field is arbitrary; during JRE initialization
// it will be reset to the value specified on the command line, if any,
// otherwise to Runtime.getRuntime().maxMemory().
//
private static long directMemory = 64 * 1024 * 1024;
 
// Returns the maximum amount of allocatable direct buffer memory.
// The directMemory variable is initialized during system initialization
// in the saveAndRemoveProperties method.
//
public static long maxDirectMemory() {
    return directMemory;
}

这里directMemory默认赋值为64MB,那对外内存的默认大小是64MB吗?不是,仔细看注释,注释中说,这个值会在JRE启动过程中被重新设置为用户指定的值,如果用户没有指定,则会设置为Runtime.getRuntime().maxMemory()。

这个过程发生在sun.misc.VM#saveAndRemoveProperties函数中,这个函数会被java.lang.System#initializeSystemClass调用:

public static void saveAndRemoveProperties(Properties props) {
    if (booted)
        throw new IllegalStateException("System initialization has completed");
 
    savedProps.putAll(props);
 
    // Set the maximum amount of direct memory.  This value is controlled
    // by the vm option -XX:MaxDirectMemorySize=<size>.
    // The maximum amount of allocatable direct buffer memory (in bytes)
    // from the system property sun.nio.MaxDirectMemorySize set by the VM.
    // The system property will be removed.
    String s = (String)props.remove("sun.nio.MaxDirectMemorySize");
    if (s != null) {
        if (s.equals("-1")) {
            // -XX:MaxDirectMemorySize not given, take default
            directMemory = Runtime.getRuntime().maxMemory();
        } else {
            long l = Long.parseLong(s);
            if (l > -1)
                directMemory = l;
        }
    }
 
    //...
}

所以默认情况下,可以申请的DirectByteBuffer大小为Runtime.getRuntime().maxMemory(),而这个值等于可用的最大Java堆大小,也就是我们-Xmx参数指定的值。

参考:http://www.importnew.com/29817.html

 

 

 

 

  • 5
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
DirectByteBuffer 是 Java NIO 中提供的一种用于操作堆外内存的缓冲区类型。DirectByteBuffer 持有一个堆外内存的引用,当 DirectByteBuffer 对象被垃圾回收时,它所持有的堆外内存并不会自动被回收,这可能会导致内存泄露。 为了避免内存泄露,需要手动释放 DirectByteBuffer 所持有的堆外内存。一种常见的方法是通过调用 DirectByteBuffer 的 clean() 方法来释放堆外内存。但是,clean() 方法并不能保证立即释放堆外内存,而且它只能在 JDK 9 之前的版本中使用。 另外一种更可靠的方法是使用反射来调用 DirectByteBuffer 内部的 Cleaner 对象的 clean() 方法,这样可以保证及时释放堆外内存。示例代码如下: ``` import sun.misc.Cleaner; import java.lang.reflect.Field; import java.nio.ByteBuffer; public class DirectByteBufferUtil { private static final sun.misc.Unsafe UNSAFE; private static final long ADDRESS_OFFSET; private static final long CLEANER_OFFSET; static { try { Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe"); field.setAccessible(true); UNSAFE = (sun.misc.Unsafe) field.get(null); ADDRESS_OFFSET = UNSAFE.objectFieldOffset(Buffer.class.getDeclaredField("address")); CLEANER_OFFSET = UNSAFE.objectFieldOffset(ByteBuffer.class.getDeclaredField("cleaner")); } catch (Exception ex) { throw new RuntimeException(ex); } } public static void free(ByteBuffer buffer) { long address = UNSAFE.getLong(buffer, ADDRESS_OFFSET); if (address != 0) { UNSAFE.putLong(buffer, ADDRESS_OFFSET, 0); Cleaner cleaner = (Cleaner) UNSAFE.getObject(buffer, CLEANER_OFFSET); if (cleaner != null) { cleaner.clean(); } } } } ``` 使用上述代码中的 free() 方法来释放 DirectByteBuffer 所持有的堆外内存,例如: ``` ByteBuffer buffer = ByteBuffer.allocateDirect(1024); // 使用 buffer 操作堆外内存 DirectByteBufferUtil.free(buffer); ``` 这样就可以释放 DirectByteBuffer 所持有的堆外内存了。需要注意的是,由于上述代码使用了 JDK 内部的类和方法,所以可能不具有可移植性,应该根据具体情况谨慎使用。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

赶路人儿

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值