JVM笔记(六)堆外内存导致OOM

58 篇文章 0 订阅
4 篇文章 0 订阅

在垃圾回收时,对于Direct Memory虚拟机虽然会执行回收,但是不受JVM直接管理,不能发现空间不足了就通知收集器回收,而是等待FullGC时,JVM会顺带清除直接内存的废弃对象。

写个代码来看直接内存的分配

    public static void main(String[] args) {
        while(true) {
            // 每次100M
            ByteBuffer.allocateDirect(100 * 1024 * 1024);
        }
    }

增加参数-XX:+PrintGCDetails 打印GC日志,执行程序会发现一直在执行,但是会频繁的GC/FullGC,申请的是堆外内存,堆内并不占多少空间,为什么会频繁触发GC呢?

来看DirectByteBuffer源码

内部会有一个Bits类来记录本应用已经分配了多少的直接内存空间,下边是方法中的代码,删除了很多,保留了我想说的。

  static void reserveMemory(long size, int cap) {

        if (!memoryLimitSet && VM.isBooted()) {
            // 获取虚拟机配置的最大允许的直接内存参数值-XX:MaxDirectMemorySize,默认和 MaxHeapSize差不多
            maxMemory = VM.maxDirectMemory();
            memoryLimitSet = true;
        }

        // 记录本次分配的内存大小,检查已分配的内存是否已经超过最大允许的直接内存大小
        if (tryReserveMemory(size, cap)) {
            // 内存足够返回
            return;
        }

        // trigger VM's Reference processing
        // 内存不足,触发一个fullgc
        System.gc();

        // 检查是否进行了回收,并支持分配
        while (true) {
             if (tryReserveMemory(size, cap)) {
                return;
             }
            // 不能无限次的死循环检查,设置了各最大次数
             if (sleeps >= MAX_SLEEPS) {
                    break;
            }
 
        }
         // 内存不足报错oom
        throw new OutOfMemoryError("Direct buffer memory");
}

在分配直接内存超过最大限制时,会代码方式触发FullGC,所以会频繁GC且一直执行下去。

但是假若我们程序中设置了-XX:+DisableExplicitGC参数,参数表示禁用System.gc(),那么程序中的System.gc()无效,就会报错OOM。

DirectByteBuffer对象是怎么回收的呢

DirectByteBuffer buffer = new DirectByteBuffer();

当然这样子代码是不对的,我只是表达个意思。

buffer对象中保存的是堆外内存的地址,buffer这个对象还是在堆内的,这部分内存会在gc的时候检查到没有任何引用,然后垃圾回收掉,而堆外内存在buffer这个对象被回收之后,再由unsafe类回收堆外内存,下边来看具体情况。

继续来看DirectByteBuffer源码,在构造函数中初始化内存后其中有一个Cleaner对象,对象关联了当前的ByteBuffer对象和一个Runnable,Runnable中是释放内存操作,那看起来必然是在cleaner中调用的堆外内存释放


class DirectByteBuffer extends MappedByteBuffer implements DirectBuffer {

    private final Cleaner cleaner;

     DirectByteBuffer(int cap) {        
        // 忽略分配内存相关代码
     
        // 初始化cleaner对象
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
    }

    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;
        }

        public void run() {
            if (address == 0) {
                // Paranoia
                return;
            }
            unsafe.freeMemory(address);
            address = 0;
            Bits.unreserveMemory(size, capacity);
        }

    }

    public Cleaner cleaner() { return cleaner; }
}

继续跟踪Cleaner对象

public class Cleaner extends PhantomReference<Object> {

   private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();
   private final Runnable thunk;// 释放内存runnable对象

   // 这个var0就是DirectByteBuffer对象, var1就是unsafe清理操作
   public static Cleaner create(Object var0, Runnable var1) {
        return var1 == null ? null : add(new Cleaner(var0, var1));
    }

   private Cleaner(Object var1, Runnable var2) {
        super(var1, dummyQueue);
        this.thunk = var2;
    }

    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;
                    }
                });
            }

        }
    }
}

发现Cleaner是个虚引用,关联着DirectByteBuffer,在顶层父类Reference中会发现,静态块中启动了一个ReferenceHandler 线程死循环执行tryHandlePending方法,而方法内部的pending变量是个链表,gc时会将引用对象不可达的引用对象放入pending链表(这一步我看别的博客说的),在本例中,即DirectByteBuffer大对象被回收时,gc会将虚引用Cleaner对象放入pending链表,线程运行时,判断pending中的对象是不是一个Cleaner,如果是调用其clean()方法,本例中即unsafe释放内存操作。

public abstract class Reference<T> {
    private static Reference<Object> pending = null;

 static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        Thread handler = new ReferenceHandler(tg, "Reference Handler");
        /* If there were a special system-only priority greater than
         * MAX_PRIORITY, it would be used here
         */
        handler.setPriority(Thread.MAX_PRIORITY);
        handler.setDaemon(true);
        handler.start();

        // provide access in SharedSecrets
        SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            @Override
            public boolean tryHandlePendingReference() {
                return tryHandlePending(false);
            }
        });
    }

  private static class ReferenceHandler extends Thread {

        private static void ensureClassInitialized(Class<?> clazz) {
            try {
                Class.forName(clazz.getName(), true, clazz.getClassLoader());
            } catch (ClassNotFoundException e) {
                throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
            }
        }

        static {
            // pre-load and initialize InterruptedException and Cleaner classes
            // so that we don't get into trouble later in the run loop if there's
            // memory shortage while loading/initializing them lazily.
            ensureClassInitialized(InterruptedException.class);
            ensureClassInitialized(Cleaner.class);
        }

        ReferenceHandler(ThreadGroup g, String name) {
            super(g, name);
        }

        public void run() {
            while (true) {
                tryHandlePending(true);
            }
        }
    }

   static boolean tryHandlePending(boolean waitForNotify) {
        Reference<Object> r;
        Cleaner c;
        try {
            synchronized (lock) {
                if (pending != null) {
                    r = pending;
    
                    c = r instanceof Cleaner ? (Cleaner) r : null;
                   
                    pending = r.discovered;
                    r.discovered = null;
                } else {
                    if (waitForNotify) {
                        lock.wait();
                    }
                   
                    return waitForNotify;
                }
            }
        } catch (OutOfMemoryError x) {
            Thread.yield();
            return true;
        } catch (InterruptedException x) {
            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;
    }

}

这个Reference类是弱引用WeakReference、软引用SoftReference、虚引用PhantomReference的共同父类,三种引用中的构造方法中都会有一个ReferenceQueue,在Reference的tryHandlePending方法最后也可以看到若当前pending中的Reference引用对象构造时的队列不为null,会将当前引用对象入队。程序中可以关注这个队列来获取一个通知,即这个Reference对象所关联的对象被回收的通知

 

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值