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()
来救场了。
扫描二维码关注公众号,回复:
13116321 查看本文章
