本文以直接内存为引子,会涉及到NIO,零拷贝,OS等内容,具体相关内容读者感兴趣的话可自行查阅资料。同样欢迎各位大佬在评论区指正。
直接内存概述
《深入理解Java虚拟机》:直接内存(Direct Memory
) 并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。
- 直接内存是由本地内存分配的,因此直接内存受本机总内存大小的限制。
- 堆外内存:把内存对象分配在Java虚拟机堆以外的内存
- 堆外内存直接受操作系统管理(而不是虚拟机),这样做的结果就是能够在一定程度上减少垃圾回收(GC)对应用程序造成的影响。
- 在一些文章中将直接内存等价于堆外内存。
直接内存&堆外内存&本地内存的关系【仅供参考】
操作直接内存
堆外内存优势在 IO
操作上,对于网络 IO
,使用 Socket 发送数据时,能够节省堆内内存到堆外内存的数据拷贝
- 在 NIO[jdk1.4] 中引入了一种基于通道和缓冲的
IO
方式。,它可以使用本地函数库直接分配堆外内存,然后通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作,如下图所示【图片来源】: - 直接内存的回收需要做进一步的手动清理,一般配合虚引用完成。
1)分配直接内存
// java.nio.ByteBuffer
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
DirectByteBuffer底层通过Unsafe
去实现内存分配的,对堆内存的分配、读写、回收都做了封装。
DirectByteBuffer(int cap) { // package-private
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
//预分配内存,确认是否有足够的堆外内存,没有的话会触发GC回收一部分
Bits.reserveMemory(size, cap);
long base = 0;
try {
//分配堆外内存
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
// 将刚分配的内存空间初始化为0
unsafe.setMemory(base, size, (byte) 0);
//为address赋值,address就是分配内存的起始地址
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
//创建一个Cleaner
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
- allocateMemory是一个native方法,并不是jvm能够控制的内存区域,通常称为堆外内存,一般是通过c/c++分配的内存(malloc)。
- 上图中提到的HeapByteBuffer,数据的分配存储都在java堆上。
- 对于DirectByteBuffer所生成的ByteBuffer对象,对象存在Java堆上,而申请的堆外空间位于OS中的堆内存中,总的来说,就是通过一个存储在Java堆里面的DirectByteBuffer对象作为这块内存的引用进行操作。
- 那么为了操作堆外内存,在java堆上的对象有一个堆外内存的引用(在DirectByteBuffer的父类Buffer中有一个全局变量
address
,这个就是表示堆外内存所分配对象的地址)
当需要和io设备打交道的时候,HeapByteBuffer会将jvm堆上所维护的byte[]拷贝至堆外内存,然后堆外内存直接和io设备交互。如果直接使用DirectByteBuffer,那么就不需要拷贝这一步,将大大提升io的效率,这种称之为零拷贝(zero-copy
)。
2)前置知识:虚引用
虚引用
- 顾名思义,就是形同虚设。与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么他就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。
应用场景
-
虚引用主要用来跟踪对象被垃圾回收器回收的活动。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要进行垃圾回收。如果程序发现某个虚引用已经加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动(调用Cleaner的clean方法)。
-
虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
String str = new String("abc"); ReferenceQueue queue = new ReferenceQueue(); // 创建虚引用,要求必须与一个引用队列关联 PhantomReference pr = new PhantomReference(str, queue);
当你想取出其中的值时(get),得到的却总是null。
-
在hotspot的jvm中,有一个叫做Cleaner的类,其实就是虚引用典型的应用。可以看到Cleaner是直接简单粗暴的继承了PhantomReference,所以它本质上就是一个虚引用,只不过多了一些便捷的操作。
public class Cleaner extends PhantomReference<Object>
DirectByteBuffer中用到了Cleaner中的create方法
create方法
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
-----------------------------------------------------------------
public static Cleaner create(Object var0, Runnable var1) {
return var1 == null ? null : add(new Cleaner(var0, var1));
}
clean方法
public void clean() {
if (remove(this)) {
try {
this.thunk.run();
} catch (final Throwable var2) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (System.err != null) {
(new Error("Cleaner terminated abnormally", var2)).printStackTrace();
}
System.exit(1);
return null;
}
});
}
}
}
3)回收直接内存
自动回收
-
GC 时会扫描 DirectByteBuffer 对象是否有有效引用指向该对象,如没有,在回收 DirectByteBuffer 对象的同时且会回收其占用的堆外内存
-
GC过程中如果发现某个对象除了只有虚引用引用它之外,并没有其他的地方引用它了,那将会把这个引用放到java.lang.ref.Reference.pending队列里,ReferenceHandler这个守护线程会处理pending队列里,执行一些后置处理,这里是调用Cleaner的clean方法
-
DirectByteBuffer构造方法内创建了一个Cleaner对象, Cleaner继承了PhantomReference,其referent为DirectByteBuffer,也是通过Cleaner调用unsafe.freeMemory(address)来释放直接内存
public void run() { if (address == 0) { // Paranoia return; } unsafe.freeMemory(address); address = 0; Bits.unreserveMemory(size, capacity); }
手动回收
- DirectByteBuffer 实现了 DirectBuffer 接口,这个接口有 cleaner 方法可以获取 cleaner 对象。
public static void clean(final ByteBuffer byteBuffer) {
if (byteBuffer.isDirect()) {
((DirectBuffer)byteBuffer).cleaner().clean();
}
}
4)代码示例
public static void main(String[] args) throws InterruptedException{
//分配1G直接缓存
ByteBuffer buffer = ByteBuffer.allocateDirect(1024*1024*1024);
TimeUnit.SECONDS.sleep(10);
//清除直接缓存
((DirectBuffer)buffer).cleaner().clean();
TimeUnit.SECONDS.sleep(10);
System.out.println("测试完毕");
}
5)OOM
-XX:MaxDirectMemorySize
来指定最大的堆外内存大小。如果不指定,默认与堆的最大值-Xmx参数保持一致- 服务器管理员在配置虚拟机参数时,会根据实际内存设置
-Xmx
等参数信息,但经常忽略直接内存,使得各个内存区域总和大于物理内存限制,从而导致动态扩展时出现OutOfMemoryError
异常。