结论:堆外内存只有在fullgc的时候才能够回收掉。
先看普通对象回收,这里设置jvm参数,便于触发和观察gc
-verbose:gc -XX:+PrintGCDetails -server -Xms20m -Xmx20m
实例代码
public class Test3 {
public static final ReferenceQueue<Bu> QUEUE = new ReferenceQueue<>();
public static final List<PhantomReference> list = new ArrayList<>();
public static void main(String[] args) throws Exception {
for (int i = 0; i < 12; i++) {
// 里面包含一个1M的字节数组
Entti ti = new Entti();
PhantomReference phantomReference = new PhantomReference(ti, QUEUE);
ti = null;
System.out.println(1);
}
System.gc();
// 保证gc触发
Thread.sleep(1000);
int i = 0;
Reference t;
while ((t = QUEUE.poll()) != null) {
i++;
}
System.out.println("size" + i);
}
}
结果
分析:虚引用不是再对象回收后会加入队列嘛?gc日志显示对象确实被回收了,为何queue里面没有数据?在对象回收的时候,如果发现对象还有虚引用,就把虚引用加入其队列,但是这里的虚引用已经不存活了,所以队列为空。
我们把虚引用保存起来,保证其存活:
public class Test3 {
public static final ReferenceQueue<Bu> QUEUE = new ReferenceQueue<>();
public static final List<PhantomReference> list = new ArrayList<>();
public static void main(String[] args) throws Exception {
for (int i = 0; i < 12; i++) {
// 里面包含一个1M的字节数组
Entti ti = new Entti();
PhantomReference phantomReference = new PhantomReference(ti, QUEUE);
ti = null;
list.add(phantomReference);
System.out.println(1);
}
System.gc();
// 保证gc触发
Thread.sleep(1000);
int i = 0;
Reference t;
while ((t = QUEUE.poll()) != null) {
i++;
}
System.out.println("size" + i);
}
}
结果
分析:队列的数量确实对了,但是发现堆的占用空间基本没有释放,虚引用不是不影响对象回收吗?虚引用只是不影响对象回收的判断,现在jvm确实认定这是可以回收的对象,但是因为list->phantomReference->对象空间。jvm不能把这块对象释放,但是jvm把虚引用放入了队列里,通知用户这块空间要回收。
我们把列表的虚引用去掉,再gc
public class Test3 {
public static final ReferenceQueue<Bu> QUEUE = new ReferenceQueue<>();
public static final List<PhantomReference> list = new ArrayList<>();
public static void main(String[] args) throws Exception {
for (int i = 0; i < 12; i++) {
// 里面包含一个1M的字节数组
Entti ti = new Entti();
PhantomReference phantomReference = new PhantomReference(ti, QUEUE);
ti = null;
list.add(phantomReference);
System.out.println(1);
}
System.gc();
// 保证gc触发
Thread.sleep(1000);
int i = 0;
Reference t;
while ((t = QUEUE.poll()) != null) {
i++;
}
System.out.println("size" + i);
list.clear();
System.gc();
}
结果
分析:果然空间释放了,如果对象在回收的时候有虚引用,yanggc无法回收这块空间,如果虚引用一直不释放,jvm无法释放这块空间,只会把虚引用放入对应队列中。
如果只清理list,不清理QUEUE
1
结果
分析:必须把所有的引用都清理掉,不然无法回收空间
堆外内存释放的原理就是虚引用的实现,只不过这个虚引用队列jvm会管理。
堆外内存构造函数:堆外内存在构造的时候包含一个cleaner(包含一个runable),cleaner.create会把这个clear对象加入到一个单向的列表中,保证clear对象不会再yanggc的时候回收掉。
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));
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;
}
fullgc的时候jvm会进入这段逻辑:如果这个引用是cleaner类型,就调用cleaner的clean方法(最终调用注册的runable释放堆外内存)。
static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
synchronized (lock) {
if (pending != null) {
r = pending;
// 'instanceof' might throw OutOfMemoryError sometimes
// so do this before un-linking 'r' from the 'pending' chain...
c = r instanceof Cleaner ? (Cleaner) r : null;
// unlink 'r' from 'pending' chain
pending = r.discovered;
r.discovered = null;
} else {
// The waiting on the lock may cause an OutOfMemoryError
// because it may try to allocate exception objects.
if (waitForNotify) {
lock.wait();
}
// retry if waited
return waitForNotify;
}
}
} catch (OutOfMemoryError x) {
// Give other threads CPU time so they hopefully drop some live references
// and GC reclaims some space.
// Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
// persistently throws OOME for some time...
Thread.yield();
// retry
return true;
} catch (InterruptedException x) {
// retry
return true;
}
// Fast path for cleaners
if (c != null) {
c.clean();
return true;
}
ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
补充 如果对象重写了析构方法(和cleaner类似):
- JVM创建Finalizable对象
- JVM创建 java.lang.ref.Finalizer实例,指向刚创建的对象。
- java.lang.ref.Finalizer类持有新创建的java.lang.ref.Finalizer的实例。这使得下一次新生代GC无法回收这些对象。
- 新生代GC无法清空Eden区,因此会将这些对象移到老年代。
- 垃圾回收器发现这些对象实现了finalize()方法。因为会把它们添加到java.lang.ref.Finalizer.ReferenceQueue队列中。
- Finalizer线程会处理这个队列,将里面的对象逐个弹出,并调用它们的finalize()方法。
- finalize()方法调用完后,Finalizer线程会将引用从Finalizer类中去掉,因此在下一轮GC中,这些对象就可以被回收了。
- Finalizer线程会和我们的主线程进行竞争,不过由于它的优先级较低,获取到的CPU时间较少,因此它永远也赶不上主线程的步伐。
- 程序消耗了所有的可用资源,最后抛出OutOfMemoryError异常。