NIO中如何使用虚引用管理堆外内存原理
Posted wen-pan
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了NIO中如何使用虚引用管理堆外内存原理相关的知识,希望对你有一定的参考价值。
虚引用是最弱的引用,弱到什么地步呢?也就是你定义了虚引用根本无法通过虚引用获取到这个对象,更别谈影响这个对象的生命周期了。在虚引用中唯一的作用就是用队列接收对象即将死亡的通知。
1、虚引用的特点
-
虚引用必须与ReferenceQueue一起使用,当GC准备回收一个对象,如果发现它还有虚引用,就会在回收之前,把这个虚引用加入到与之关联的ReferenceQueue中。
-
无法通过虚引用来获取被虚引用对象引用的真实对象!!!!
ReferenceQueue queue = new ReferenceQueue(); PhantomReference<byte[]> reference = new PhantomReference<byte[]>(new byte[1], queue); // 调用reference.get(),试图获取 【被虚引用对象引用的真实对象】 这里会返回null System.out.println(reference.get());
原因是因为引用类重写了get方法
public class PhantomReference<T> extends Reference<T> { // 重写get方法,直接返回空 public T get() { return null; } }
2、虚引用的作用
利用虚引用管理堆外内存是虚引用的典型用法,比如JDK的NIO分配堆外内存的时候就是使用的虚引用的特性来管理的堆外内存。
a)、前景引入
问题1:为什么Java不用自己手动释放内存
我们知道在Java里我们不用像C++那样自己手动去释放内存,因为我们的内存是分配在JVM堆空间的,会由GC垃圾回收器帮我们自动管理并回收垃圾内存。但是如果我们能不能操作堆外内存呢?
问题2:Java中有没有办法操作堆外内存
答案是:有的
- 方法一:比如在JDK的NIO中我们就可以通过
ByteBuffer.allocateDirect(size)
去手动的分配一块堆外内存 - 方法二:比如我们可以通过反射拿到
Unsafe
对象,然后通过调用unsafe.allocateMemory(size)
方法去开辟一块堆外内存
问题3:Java中如何释放堆外内存
从上面我们已经看到我们可以通过ByteBuffer.allocateDirect
和unsafe.allocateMemory
两种方式分配堆外内存。但是当分配的堆外内存使用完了以后我们该怎么释放呢?
- 堆外内存不受JVM垃圾收集器管理,所以垃圾收集器无法回收堆外内存。所以堆外内存需要我们手动释放。我们可以通过unsafe对象的
unsafe.freeMemory(address)
方法手动释放指定位置的堆外内存。
b)、通过unsafe来操作堆外内存示例
- 通过unsafe对象来操作堆外内存,需要自己手动释放
/**
* 直接内存分配的底层原理:Unsafe
*/
public class Demo1_27 {
static int _1Gb = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException {
Unsafe unsafe = getUnsafe();
// 分配内存
long base = unsafe.allocateMemory(_1Gb);
unsafe.setMemory(base, _1Gb, (byte) 0);
System.in.read();
// 释放内存
unsafe.freeMemory(base);
System.in.read();
}
/**
* 通过反射获取Unsafe对象
*/
public static Unsafe getUnsafe() {
try {
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
return unsafe;
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException(e);
}
}
}
c)、通过ByteBuffer来使用堆外内存示例
/**
* 添加JVM运行时参数:-XX:+DisableExplicitGC,禁用显式回收对直接内存的影响
*/
public class Demo1_26 {
static int _1Gb = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException {
// ①、分配1G内存
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
System.out.println("分配完毕...");
System.in.read();
System.out.println("开始释放...");
// ②、被虚引用引用的对象 的 强引用被释放掉
byteBuffer = null;
System.gc(); // 显式的垃圾回收,Full GC
// ③、在这里阻塞住,通过内存分析工具分析当前JVM进程的内存消耗。观察上面分配的1G内存是否被回收
System.in.read();
}
}
通过内存分析工具可以看到这块1gb的内存已经被回收了。可是我们并没有手动释放堆外内存呀。这是什么情况?
内存释放前
内存释放后
3、使用虚引用管理堆外内存分析
- 上面介绍了两种使用堆外内存的方法,一种是依靠unsafe对象,另一种是NIO中的ByteBuffer,直接使用unsafe对象来操作内存,对于一般开发者来说难度很大,并且如果内存管理不当,容易造成内存泄漏。所以不推荐。推荐使用的是ByteBuffer来操作堆外内存。
- 在上面的
ByteBuffer
案例中,我们并没有手动释放内存,但是最终当byteBuffer
对象被垃圾回收时,堆外内存仍然被释放掉了,这是什么原因?是垃圾回收器回收的堆外内存吗?显然不是,因为堆外内存不受JVM垃圾收集器管理。
对于ByteBuffer这块的源码,我开始的时候尝试把它贴出来讲,但是发现这么讲的话需要的篇幅太长了。这里就以将流程的方式来聊聊ByteBuffer中是如何释放的堆外内存的吧!!!
①、测试代码回顾
/**
* 添加JVM运行时参数:-XX:+DisableExplicitGC,禁用显式回收对直接内存的影响
*/
public class Demo1_26 {
static int _1Gb = 1024 * 1024 * 1024;
public static void main(String[] args) throws IOException {
// ①、分配1G内存
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(_1Gb);
System.out.println("分配完毕...");
System.in.read();
System.out.println("开始释放...");
// ②、被虚引用引用的对象 的 强引用被释放掉
byteBuffer = null;
System.gc(); // 显式的垃圾回收,Full GC
// ③、在这里阻塞住,通过内存分析工具分析当前JVM进程的内存消耗。观察上面分配的1G内存是否被回收
System.in.read();
}
}
②、流程分析
1、byteBuffer未被回收前内存图示
即在源码中的②还没有被执行的时候,内存图示如下
- 可以看到被虚引用引用的对象其实就是这个byteBuffer对象。所以说需要重点关注的是这个byteBuffer对象被回收了以后会触发什么操作。
2、byteBuffer被回收后内存图示
- 当byteBuffer被回收后,在进行GC垃圾回收的时候,发现
虚引用对象Cleaner
是PhantomReference
类型的对象,并且被该对象引用的对象(ByteBuffer对象)
已经被回收了 - 那么他就将将这个对象放入到(
ReferenceQueue
)队列中 - JVM中会有一个优先级很低的线程会去将该队列中的
虚引用对象
取出来,然后回调clean()
方法 - 在
clean()
方法里做的工作其实就是根据内存地址
去释放这块内存(内部还是通过unsafe对象去释放的内存)。
3、部分源码展示
a)、ByteBuffer创建源码
// 创建堆外内存
public static ByteBuffer allocateDirect(int capacity) {
return new DirectByteBuffer(capacity);
}
// 这就是一个虚引用对象
private final Cleaner cleaner;
// 具体创建堆外内存逻辑
DirectByteBuffer(int cap) {
super(-1, 0, cap, cap);
boolean pa = VM.isDirectMemoryPageAligned();
int ps = Bits.pageSize();
long size = Math.max(1L, (long)cap + (pa ? ps : 0));
Bits.reserveMemory(size, cap);
long base = 0;
try {
// 使用unsafe对象分配一块堆外内存
base = unsafe.allocateMemory(size);
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap);
throw x;
}
unsafe.setMemory(base, size, (byte) 0);
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
// 【重点】创建一个虚引用对象,这个虚引用对象指向this对象(也就是指向创建的byteBuffer对象)
// 这里的 Deallocator 可以重点探究。
// 其实这里就是将Deallocator保存到了虚引用对象cleaner上。当虚引用对象被放入队列后,就会执行Deallocator对象的clean() 方法来清除堆外内存。看下面源码体现
// 可以看到这里将分配的堆外内存地址传递给了Deallocator对象保存。到时候释放堆外内存的时候也就要依靠这个地址
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
b)、Deallocator如何释放内存源码
private static class Deallocator implements Runnable{
private static Unsafe unsafe = Unsafe.getUnsafe();
private long address;
private long size;
private int capacity;
private Deallocator(long address, long size, int capacity) {
assert (address != 0);
this.address = address;
this.size = size;
this.capacity = capacity;
}
// 当虚引用对象被放入队列后,JVM中会有一个线程去取这个队列中的元素。然后就会执行这个方法释放内存
public void run() {
if (address == 0) {
// Paranoia
return;
}
// 通过分配堆外内存时保存的地址来释放堆外内存
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
}
以上是关于NIO中如何使用虚引用管理堆外内存原理的主要内容,如果未能解决你的问题,请参考以下文章