Java 堆外内存的使用

更多 Java 虚拟机方面的文章,请参见文集《Java 虚拟机》


为什么需要使用堆外内存

  • 将长期存活的对象(如 Local Cache )移入堆外内存( off-heap,又名直接内存 direct-memory),从而减少 CMS 管理的对象数量, 以降低 Full GC 的次数和频率,达到提高系统响应速度的目的。
  • 加快了复制的速度:堆内在 flush 到远程时,会先复制到直接内存,然后在发送;而堆外内存相当于省略掉了这个工作。

堆外内存不是 JVM 运行时数据区 Runtime Data Area 的一部分,这部分内存区域直接被操作系统管理,JVM 通过 JNI 本地接口操作堆外内存。

堆外内存的使用

在 JDK 1.4以前,对这部分内存访问没有光明正大的做法:只能通过反射拿到 Unsafe 类,然后调用allocateMemory()/freeMemory()来申请/释放这块内存。
1.4 开始新加入了 NIO,它引入了一种基于 Channel 与 Buffer 的 I/O 方式,可以使用 Native 函数库直接分配堆外内存,然后通过一个存储在 Java 堆里面的 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 static void main(String[] args) {
    ByteBuffer bb = ByteBuffer.allocateDirect(1024);
    bb.putChar('A');
    bb.putInt(123);

    System.out.println("capacity: " + bb.capacity());
    System.out.println("limit: " + bb.limit());
    System.out.println("position: " + bb.position());

    bb.position(0);
    System.out.println(bb.getChar());
    System.out.println(bb.getInt());
}

输出:

capacity: 1024
limit: 1024
position: 6
A
123

堆外内存的设置

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

堆外内存的分配

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

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

堆外内存的回收

堆外内存基于 GC 的回收

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

堆外内存的主动回收

对于 Sun 的 JDK 这其实很简单,只要从 DirectByteBuffer 里取出那个 sun.misc.Cleaner,然后调用它的 clean() 就行。
例如:
((DirectBuffer)bb).cleaner().clean();


引用:
JVM初探——使用堆外内存减少Full GC
Netty之Java堆外内存扫盲贴
从0到1起步-跟我进入堆外内存的奇妙世界

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
【社区内容提示】社区部分内容疑似由AI辅助生成,浏览时请结合常识与多方信息审慎甄别。
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • 堆外内存, JDK 1.4 nio引进了ByteBuffer.allocateDirect()分配堆外内存 Byt...
    andersonoy阅读 6,248评论 0 1
  • 1. Java基础部分 基础部分的顺序:基本语法,类相关的语法,内部类的语法,继承相关的语法,异常的语法,线程的语...
    子非鱼_t_阅读 32,153评论 18 399
  • 堆外内存 堆外内存是相对于堆内内存的一个概念。堆内内存是由JVM所管控的Java进程内存,我们平时在Java中创建...
    tomas家的小拨浪鼓阅读 41,602评论 19 71
  • 占小狼转载请注明原创出处,谢谢! 堆外内存 JVM启动时分配的内存,称为堆内存,与之相对的,在代码中还可以使用堆外...
    美团Java阅读 27,448评论 15 60
  • 从三月份找实习到现在,面了一些公司,挂了不少,但最终还是拿到小米、百度、阿里、京东、新浪、CVTE、乐视家的研发岗...
    时芥蓝阅读 42,473评论 11 349