Java 堆外内存操作和使用场景

JDK5 之后才出现了堆外内存得API给到开发进行调用,那么我们为什么要使用堆外内存呢?

  • 加速GC回收,大量对象产生在堆内,GC回收得压力是很大得
  • 更自由、更高效得使用整个计算机内存
  • 更高性能得跨进程数据通信,避免了主内存得多次copy

使用堆外内存需要考虑哪些问题呢?

  • 准确得时间释放无需在使用得堆外内存

带着上面得问题我们接着往下看

Java 堆外内存得操作方式

  • 利用unsafe直接操作(危险性比较高,官方不推荐)
  • 利用NIO得ByteBuffer,JVM会进行堆外内存管理,当堆内得引用对象被回收时会自动回收相关得堆外内存(推荐)

DirectByteBuffer 对象作为这块内存的引用进行操作,ByteBuffer 提供了如下常用方法来跟堆外内存打交道:

  • public static ByteBuffer allocateDirect(int capacity)
    • 分配堆外内存,返回一个 DirectByteBuffer 堆外内存对象 return new DirectByteBuffer(capacity);
  • public abstract ByteBuffer put(byte b);
    • 向堆外内存中存放一个字节
  • public abstract byte get();
    • 从堆外内存中读取一个字节
  • public final ByteBuffer put(byte[] src)
    • 向堆外内存中存放一个字节数组
  • public ByteBuffer get(byte[] dst)
    • 从堆外内存中读取一个字节数组
  • public abstract ByteBuffer putInt(int value);
    • 向堆外内存中存放一个 int
  • public abstract int getInt();
    • 从堆外内存中读取一个 int
  • public abstract IntBuffer asIntBuffer()
    • 转换为一个 IntBuffer
  • public abstract ByteBuffer putLong(long value); 同上,以此类推
  • public abstract boolean isDirect();
    • 判断是否为堆外内存

ByteBuffer 包含了如下的几个属性:

  • private int mark = -1;:标记位置,记录当前 position 的值
  • private int position = 0;:当前位置
  • private int limit;:限制大小
  • private int capacity;:空间容量
  • 基本关系 mark <= position <= limit <= capacity
public class ByteBufferTest {
    public static void main(String[] args){
        ByteBuffer byteBuffer = ByteBuffer.allocateDirect( 1024 );
        byteBuffer.putChar( 'A' );
        byteBuffer.putLong( 333l );
        byteBuffer.position(0);
        System.out.println(byteBuffer.getChar());
        System.out.println(byteBuffer.getLong());
        System.out.println(byteBuffer.limit());
        System.out.println(byteBuffer.position());
    }
}

堆外内存的设置

堆外内存的限额默认与堆内内存(由-XMX 设定)相仿,可用 -XX:MaxDirectMemorySize 重新设定。
当使用达到了阈值的时候将调用 System.gc 来做一次 Full GC,以此来回收掉没有被使用的堆外内存。

堆外内存的分配

在 DirectByteBuffer 中,首先向 Bits 类申请额度,Bits 类有一个全局的 totalCapacity 变量,记录着全部 DirectByteBuffer 的总大小,每次申请,都先看看是否超限:

  • 如果已经超限,会主动执行 Sytem.gc(),期待能主动回收一点堆外内存。然后休眠一百毫秒,看看 totalCapacity 降下来没有,如果内存还是不足,就抛出大家最头痛的 OOM 异常。
  • 如果额度被批准,就调用大名鼎鼎的 sun.misc.Unsafe 去分配内存,返回内存基地址,Unsafe 的 C++实现,标准的 malloc。然后再调一次 Unsafe 把这段内存给清零。

堆外内存基于 GC 的回收

存在于堆内的 DirectByteBuffer 对象很小,只存着基地址和大小等几个属性,和一个 Cleaner,但它代表着后面所分配的一大段内存,是所谓的冰山对象。
通过前面说的 Cleaner,堆内的 DirectByteBuffer 对象被 GC 时,它背后的堆外内存也会被回收。
这里可以看到一种尴尬的情况,因为 DirectByteBuffer 本身的个头很小,只要熬过了 Young GC,即使已经失效了也能在老生代里舒服的呆着,不容易把老生代撑爆触发 Full GC,如果没有别的大块头进入老生代触发Full GC,就一直在那耗着,占着一大片堆外内存不释放。
这时,就只能靠前面提到的申请额度超限时触发的 System.gc()来救场了。

猜你喜欢

转载自blog.csdn.net/wmq880204/article/details/115177734