谈谈JDK和Netty 堆外内存的申请和释放

JDK的堆外内存申请

ByteBuffer byteBuffer = ByteBuffer.allocateDirect(1024)

跟踪源码这里的ByteBuffer的实现类实际上是DirectByteBuffer

	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内部维护了
        //已申请内存大小totalCapacity 
        //最大内存大小maxMemory,该值可以通过-XX:MaxDirectMemorySize设置
        //Bits的作用就是在真正申请内存之前做验证,如果内存空间不够提前抛出异常,当然在抛异常之前也会尝试做一些挣扎
        //例如主动调用System.gc()等,看能否释放一些已有的堆外内存
        Bits.reserveMemory(size, cap);

        long base = 0;
        try {
            //真正的申请操作在这里,是个native方法
            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类继承了PhantomReference 也就是幽灵引用
        //当DirectByteBuffer被GC的时候 cleaner会触发内存回收的动作
        //这里的base就是本次申请的内存地址,释放的时候需要
        cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
        att = null;



    }

以上是JDK的堆外内存申请过程, 通过Bits对申请做验证以及存储大小的统计,调用unsafe.allocateMemory申请堆外内存,最后 再将自己注入到Cleaner中,在gc之后通过Cleaner完成后续的内存释放

JDK的堆外内存释放
先看Cleaner类被DirectByteBuffer调用的部分

//Deallocator就是 DirectByteBuffer的内部类,真正释放内存的操作就由这个类完成
public static Cleaner create(DirectByteBuffer var0, Deallocator var1) {
        //调用add方法,是为了将自己被一个静态变量引用,防止被GC
        return var1 == null ? null : add(new Cleaner(var0, var1));
 }

Cleaner的构造方法

private Cleaner(DirectByteBuffer var1, Deallocator var2) {
        //调用Reference的构造方法, 接下来就由Reference来控制Cleaner
        super(var1, dummyQueue);
        this.thunk = var2;
}

接下来继续看Reference,首先关注的是 static代码块

static {
        ThreadGroup tg = Thread.currentThread().getThreadGroup();
        for (ThreadGroup tgn = tg;
             tgn != null;
             tg = tgn, tgn = tg.getParent());
        //重点在这里,这里开启了一个线程,实际上就是运行一个循环,不停的调用tryHandlePending(true)     
        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();

        //这里主要是为了Bits类在挣扎的时候能够主动调用tryHandlePending方法,看看能否释放一些内存空间
        // provide access in SharedSecrets
        SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
            @Override
            public boolean tryHandlePendingReference() {
                return tryHandlePending(false);
            }
        });
    }

接下来就重点看看tryHandlePending的方法,这里我把jdk的注释也拿了过来


    /* Object used to synchronize with the garbage collector.  The collector
     * must acquire this lock at the beginning of each collection cycle.  It is
     * therefore critical that any code holding this lock complete as quickly
     * as possible, allocate no new objects, and avoid calling user code.
     */
     //通过注释可以了解到,这个lock是垃圾收集器和code代码共用的
    static private class Lock { }
    private static Lock lock = new Lock();


    /* List of References waiting to be enqueued.  The collector adds
     * References to this list, while the Reference-handler thread removes
     * them.  This list is protected by the above lock object. The
     * list uses the discovered field to link its elements.
     */
    //这里是重点,垃圾收集器在对象被回收后 会将相应的Reference放到这里,例如前面提到的Cleaner
    private static Reference<Object> pending = null;

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
        //对Cleaner进行特殊处理 调用clean 执行内存释放的操作
        if (c != null) {
            c.clean();
            return true;
        }
        
        ReferenceQueue<? super Object> q = r.queue;
        //相应的例如 WeakReference、softReference 等都是在这个时候被放入队列的
        if (q != ReferenceQueue.NULL) q.enqueue(r);
        return true;
    }

接下来我们主要看Cleaner.clean()的逻辑

public void clean() {
        if (remove(this)) {
            try {
                //这里的thunk就是之前传入进来的Deallocator,这里开始调用它的run方法
                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 修改相应的统计数据
            Bits.unreserveMemory(size, capacity);
        }

以上就是jdk的堆外内存申请以及释放, 整体来说并不那么可靠,因为java的gc我们是没办法准确控制的,一个不小心就有可能导致内存泄漏, 而且申请内存的过程需要额外做不少操作,性能难免也有影响

netty的堆外内存申请
为了更好的比较这里只讲 UnpooledByteBufAllocator

UnpooledByteBufAllocator.DEFAULT.buffer(1024)

追踪源码得到

//io.netty.buffer.UnpooledByteBufAllocator#newDirectBuffer
//这里根据是否能获取 sun.misc.Unsafe 和 noCleaner参数来决定使用哪个类
protected ByteBuf newDirectBuffer(int initialCapacity, int maxCapacity) {
        final ByteBuf buf;
        if (PlatformDependent.hasUnsafe()) {
            buf = noCleaner ? new InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf(this, initialCapacity, maxCapacity) :
                    new InstrumentedUnpooledUnsafeDirectByteBuf(this, initialCapacity, maxCapacity);
        } else {
            buf = new InstrumentedUnpooledDirectByteBuf(this, initialCapacity, maxCapacity);
        }
        return disableLeakDetector ? buf : toLeakAwareBuffer(buf);
    }

分别是
InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf
InstrumentedUnpooledUnsafeDirectByteBuf
其中 InstrumentedUnpooledUnsafeDirectByteBuf就和jdk的申请套路是一样的
这里主要分析 InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf 这也是大部分情况下的选择

继续跟踪代码

//io.netty.buffer.UnpooledDirectByteBuf#UnpooledDirectByteBuf(io.netty.buffer.ByteBufAllocator, int, int)
public UnpooledDirectByteBuf(ByteBufAllocator alloc, int initialCapacity, int maxCapacity) {
        super(maxCapacity);
        if (alloc == null) {
            throw new NullPointerException("alloc");
        }
        checkPositiveOrZero(initialCapacity, "initialCapacity");
        checkPositiveOrZero(maxCapacity, "maxCapacity");
        if (initialCapacity > maxCapacity) {
            throw new IllegalArgumentException(String.format(
                    "initialCapacity(%d) > maxCapacity(%d)", initialCapacity, maxCapacity));
        }

        this.alloc = alloc;
        //重点在allocateDirect方法上
        setByteBuffer(allocateDirect(initialCapacity), false);
    }
//io.netty.buffer.UnpooledByteBufAllocator.InstrumentedUnpooledUnsafeNoCleanerDirectByteBuf#allocateDirect
protected ByteBuffer allocateDirect(int initialCapacity) {
            ByteBuffer buffer = 
//调用父类io.netty.buffer.UnpooledUnsafeNoCleanerDirectByteBuf的方法
super.allocateDirect(initialCapacity);
            ((UnpooledByteBufAllocator) alloc()).incrementDirect(buffer.capacity());
            return buffer;
        }
protected ByteBuffer allocateDirect(int initialCapacity) {
        //找到最终的入口
        return  PlatformDependent.allocateDirectNoCleaner(initialCapacity);
    }

接着继续看PlatformDependent类

static ByteBuffer allocateDirectNoCleaner(int capacity) {
        // Calling malloc with capacity of 0 may return a null ptr or a memory address that can be used.
        // Just use 1 to make it safe to use in all cases:
        // See: http://pubs.opengroup.org/onlinepubs/009695399/functions/malloc.html
        return newDirectBuffer(UNSAFE.allocateMemory(Math.max(1, capacity)), capacity);
    }

看到这里想必大家明白为什么取名叫NoCleaner了
这里的申请直接调用了native方法 也就是 UNSAFE.allocateMemory,没有jdk繁琐的过程,也没有创建相应的cleaner,换句话说该内存的释放只能手动来操作了

接下来的操作就是创建DirectByteBuffer了

static ByteBuffer newDirectBuffer(long address, int capacity) {
        ObjectUtil.checkPositiveOrZero(capacity, "capacity");

        try {
        //因为DirectByteBuffer类 不是public 所以只能通过反射来创建,然后传入申请的内存地址
            return (ByteBuffer) DIRECT_BUFFER_CONSTRUCTOR.newInstance(address, capacity);
        } catch (Throwable cause) {
            // Not expected to ever throw!
            if (cause instanceof Error) {
                throw (Error) cause;
            }
            throw new Error(cause);
        }
    }

netty的堆外内存释放

这里从release()方法入手

ByteBuf byteBuf1 = UnpooledByteBufAllocator.DEFAULT.buffer(1024);
byteBuf1.release();
//io.netty.buffer.AbstractReferenceCountedByteBuf#release()
public boolean release() {
     //这里的原理是对ByteBuf的引用做个计数,release表示引用减1,当引用为0时就可以进行释放
     return handleRelease(updater.release(this));
}

继续追踪代码

//io.netty.buffer.UnpooledUnsafeNoCleanerDirectByteBuf#freeDirect
protected void freeDirect(ByteBuffer buffer) {
        PlatformDependent.freeDirectNoCleaner(buffer);
    }

PlatformDependent这个类是不是很熟悉,申请内存也是这个类

public static void freeDirectNoCleaner(ByteBuffer buffer) {
        assert USE_DIRECT_BUFFER_NO_CLEANER;

        int capacity = buffer.capacity();
 //这里PlatformDependent0.directBufferAddress(buffer)获取的就是buffer对应的堆外内存地址了
//然后调用UNSAFE.freeMemory(address) 进行释放
PlatformDependent0.freeMemory(PlatformDependent0.directBufferAddress(buffer));
        decrementMemoryCounter(capacity);
    }

对比明显发现netty的代码更加的简洁,虽然不能通过gc来自动释放内存,但是针对于这种特殊需求,手动控制反而更加合理

除此之外netty还包括内存的度量工具类 例如UnpooledByteBufAllocatorMetric、PooledByteBufAllocatorMetric

为了防止忘记释放内存,还有对应的检测工具 ResourceLeakDetector

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值