这是我参与8月更文挑战的第22天,活动详情查看:8月更文挑战
JVM内存划分
-
java中的对象都是在堆内分配的,即,我们常说的JVM内存,JVM内存完全被java虚拟机管理,所以,对于java程序员来说,完全不用关系对象的回收问题,java虚拟机通过对内存进行划分成年轻代和老年代,并使用不同的垃圾回收算法对我们废弃的java对象进行回收。
-
除了JVM内存外,还有一种内存即为JVM堆外内存。堆外内存的使用和释放都要通过调用操作系统API,堆外内存不受java虚拟机管理,所以我们使用后,需要进行手动的释放,否则会造成内存泄漏问题。
堆外内存的作用
-
两个进程的堆外内存指向同一块地址即可实现进程间通讯。
-
当进行网络IO操作,文件读写时,如果使用堆内内存,那么首先要将堆内内存拷贝到堆外,然后操作系统才能对数据进行读写,但是使用堆外内存的话,操作系统可以直接操作,减少了一次内存拷贝,增加我们程序的性能。
-
因为堆外内存使我们编程人员手动进行申请和释放的,所以,在一定程度上也可以减轻JVM的GC压力。
Unsafe类申请/释放内存
我们可以通过调用Unsafe的API对堆外内存进行申请和释放
- Unsafe构造方法私有化,我们不能直接创建Unsafe实例,只能通过反射获取
Class<Unsafe> unsafeClass = Unsafe.class;
Field unsafeField = unsafeClass.getDeclaredField("theUnsafe");
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
- 申请/回收内存
// 分配一个10M大小的堆外内存,并返回内存的起始地址
long address = unsafe.allocateMemory(1024 * 1024 * 10);
// 手动调用释放内存API
unsafe.freeMemory(address);
一般情况下,不会直接使用这个API进行操作,而是使用java的NIO中自带的ByteBuffer帮助我们操作堆外内存。
ByteBuffer堆外内存管理
java中可以通过使用ByteBuffer申请堆外内存
申请一个10M大小的堆外内存
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024 * 1024 * 10);
通过查看该方法的源码可以看出,内部是调用了Unsafe类,除了申请了一块堆外内存外,该堆外内存还关联了一个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 {
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;
}
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
堆外内存和Cleaner对象,ByteBuffer对象的关系
ByteBuffer对堆外内存的回收策略
-
我们创建的ByteBuffer对象可能有时候会常驻内存,这样在经过若干次GC后,分代年龄到了晋升到老年代的条件,就会被转移到老年代,以为老年代很少进行
FUllGC,所以会导致堆外内存迟迟不回被回收。进而出现类似内存泄漏的问题。 -
所以,我们尽量在启动JVM实例时,通过配置
-XX:MaxDirectMemorySize指定堆外堆内存大小,防止堆外内存无限使用,当堆外内存分配不足时,就会触发一次FUllGC进行垃圾清理。如果FUllGC后,还是无法满足需要新分配的内存大小,程序就会抛OOM -
在
ByteBuffer.allocateDirect执行过程中,也会如果堆外内存不足,也会通过调用Bits.unreserveMemory(size, cap);内部会调用System.gc()强制执行FUllGC,但是生产环境中,一般都会配置-XX:+DisableExplicitGC关闭System.gc()的作用。
Cleaner对象回收堆外内存原理
Java对象中,有四种引用关系,分别是
强引用,软引用,弱引用和虚引用。Cleaner对象就是一个虚引用对象。 虚引用的对象不能单独使用,需要配合引用队列使用,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。要注意的是,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。
public class Cleaner extends PhantomReference<Object> {
private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();
private static Cleaner first = null;
private Cleaner next = null;
private Cleaner prev = null;
private final Runnable thunk;
}
- first 是 Cleaner 类中的静态变量,Cleaner 对象在初始化时会加入 Cleaner 链表中。
- DirectByteBuffer 对象包含堆外内存的地址、大小以及 Cleaner 对象的引用。
- ReferenceQueue 用于保存需要回收的 Cleaner 对象。
当ByteBuffer被回收后,Cleaner对象不再有任何强引用,此时,Cleaner对象会被加入到引用队列中,执行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;
}
});
}
}
}
public void run() {
if (address == 0) {
// Paranoia
return;
}
unsafe.freeMemory(address);
address = 0;
Bits.unreserveMemory(size, capacity);
}
clean方法主要做了两件事情
- 将自己从链表中移除。
- 调用unsafe方法,回收堆外内存。这时,堆外内存就被成功回收了。