更多 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去分配内存,返回内存基地址,Unsafe的 C++实现在此,标准的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起步-跟我进入堆外内存的奇妙世界