结论:堆外内存只有在fullgc的时候才能够回收掉。
先看普通对象回收,这里设置jvm参数,便于触发和观察gc
-verbose:gc -XX:+PrintGCDetails -server -Xms20m -Xmx20m
实例代码
public class Test3 { public static final ReferenceQueueQUEUE = new ReferenceQueue<>(); public static final List 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 ReferenceQueueQUEUE = new ReferenceQueue<>(); public static final List 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 ReferenceQueueQUEUE = new ReferenceQueue<>(); public static final List 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
补充 如果对象重写了析构方法(和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线程会处理这个队列,将里面的对象逐个d出,并调用它们的finalize()方法。
- finalize()方法调用完后,Finalizer线程会将引用从Finalizer类中去掉,因此在下一轮GC中,这些对象就可以被回收了。
- Finalizer线程会和我们的主线程进行竞争,不过由于它的优先级较低,获取到的CPU时间较少,因此它永远也赶不上主线程的步伐。
- 程序消耗了所有的可用资源,最后抛出OutOfMemoryError异常。
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)