聊聊Java中引用类型-引用类型应用与内存泄漏

本文将接着上一篇文章内容,聊聊Java中引用使用以及可能产生的内存泄漏。

Java程序员是幸福的,不用过多考虑内存申请和释放,Jvm在Java与C++之间构建一堵由内存动态分配和垃圾收集技术所围成的高墙,是的Java程序员能全身心投入到实际开发当中,是否会有墙外面人想进去,墙里面的人却想出来呢?

内存溢出和内存泄漏:

  • 内存溢出:俗称OOM,指JVM无法申请到足够内存空间或者GC失败,而抛出的Error,OOM造成的后果十分严重,使得应用无法对外提供服务。
  • 内存泄漏:部分内存已经没有用了,但是却没有被回收,对于Java而言,就是GC无法回收这部分本应该被回收的内存。

Obeject 的 finalize

Object 的finalize方法,当对象即将被回收时,可以被执行finalize方法,但是并不能依赖这个方法来清除,否则将造成内存泄漏。
例如当进行socket编程时,需要在finally块中执行close方法,从而释放资源,如果忘记释放了,则可能会造成内存泄漏,例如在 重写了finalize方法:

    /**
     * Cleans up if the user forgets to close it.
     */
    protected void finalize() throws IOException {
        close();
    }

方法注释也很简洁明了,所以在释放这一类工具类时,一定要手动执行close方法,而不是交给finalize去替我们释放,这样容易引发内存泄漏。

ThreadLocal

ThreadLocal很常见,原理不难理解,在Thread类中有两个ThreadLocalMap变量,维护着多个本地线程变量:
Thread:

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

ThreadLocalMap结构:

        private static final int INITIAL_CAPACITY = 16;
        private Entry[] table;   // 存放元素的数组
        private int size = 0;   // 大小
        private int threshold; // Default to 0

其Entry为一个WeakReference子类,reference为对应的ThreadLocal 实例,即如果没有其他引用,就会被回收:

        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

Entry 由于没有使用引用队列,故一旦没有引用,则会直接变为inactive状态,从而被gc回收。
但是另一方面,ThreadLocalMap 中 Entry[] 为一个引用,所以事实上,就算ThreadLocal没有其他地方使用,也会被Thread引用,所以只要Thread不销毁,Entry 并不会因WeakReference特性而销毁。
另一方面,由于Entry 中key(ThreadLocal)是弱引用类型,所以一旦ThreadLocal没有被引用,那么ThreadLocal将会在下次gc被回收,造成的效果为:

  1. Entry 不为null。
  2. Entry实例的get方法获取为null,因为ThreadLocal已经被回收了,但是value仍然存在,这就造成了泄漏。

在ThreadLocalMap的set操作过程,虽然在检查到上述第二种情况,并且会尝试将数组内所有满足该情况的元素节点的value都设置为null。
这也是为啥追求极致性能的netty会自己造一个FastThreadLocal来取代TheadLocal:https://blog.csdn.net/anLA_/article/details/110777608
所以在使用ThreadLocal时,在用完时,一定要执行remove方法,清除对应引用。

Netty中内存泄漏检测

netty中通过BufAllocator使用ByteBuf时,都会包装一层校验内存泄漏的逻辑:

    protected static ByteBuf toLeakAwareBuffer(ByteBuf buf) {
        ResourceLeakTracker<ByteBuf> leak;
        switch (ResourceLeakDetector.getLevel()) {
            case SIMPLE:
                leak = AbstractByteBuf.leakDetector.track(buf);
                if (leak != null) {
                    buf = new SimpleLeakAwareByteBuf(buf, leak);
                }
                break;
            case ADVANCED:
            case PARANOID:
                leak = AbstractByteBuf.leakDetector.track(buf);
                if (leak != null) {
                    buf = new AdvancedLeakAwareByteBuf(buf, leak);
                }
                break;
            default:
                break;
        }
        return buf;
    }

上述会将ByteBuf封装一层 DefaultResourceLeak,而DefaultResourceLeak 则是一个WeakReference对象:

        DefaultResourceLeak(
                Object referent,
                ReferenceQueue<Object> refQueue,
                Set<DefaultResourceLeak<?>> allLeaks) {
            super(referent, refQueue);

            assert referent != null;

            // Store the hash of the tracked object to later assert it in the close(...) method.
            // It's important that we not store a reference to the referent as this would disallow it from
            // be collected via the WeakReference.
            trackedHash = System.identityHashCode(referent);
            allLeaks.add(this);
            // Create a new Record so we always have the creation stacktrace included.
            headUpdater.set(this, new Record(Record.BOTTOM));
            this.allLeaks = allLeaks;
        }

在应用中,对返回的ByteBuf对象进行操作时,都会间接调用 ResourceLeakDetectorreportLeak 方法:

    private void reportLeak() {
        if (!logger.isErrorEnabled()) {
            clearRefQueue();
            return;
        }
        // Detect and report previous leaks.
        for (;;) {
            @SuppressWarnings("unchecked")
            DefaultResourceLeak ref = (DefaultResourceLeak) refQueue.poll();  // 如果有弱引用被回收,则会进入队列
            if (ref == null) {
                break;
            }
            if (!ref.dispose()) {   // 如果已经释放,则不是泄漏
                continue;
            }
            String records = ref.toString();
            if (reportedLeaks.putIfAbsent(records, Boolean.TRUE) == null) {
                if (records.isEmpty()) {
                    reportUntracedLeak(resourceType);
                } else {
                    reportTracedLeak(resourceType, records);
                }
            }
        }
    }

上述方法有以下要点:

  1. 如果进入refQueue,则说明有弱引用被回收,如果dispose了,则说明回收之前执行release,释放了资源,否则没有释放。
  2. 报告泄漏最近调用路径

使用MAT进行堆dump分析

Java引用类型,还有一个用途点,就是在使用eclipse mat工具时,由于Java对象通过引用链接,所以当找到一个大对象时,可以过滤其他引用类型,直接选择强引用,从而一步一步最终拿到当时调用链路栈:
在这里插入图片描述
这样就能轻而易举解决多数OOM问题的根源。

总结

  1. Java引用类型很强大,可以来实现一些高校的应用内缓存,可以监听gc动作等。
  2. 根据部分类文档及结合代码来确定 使用时要注意是否需要手动释放。

觉得对你有帮助?不如关注博主公众号: 六点A君
在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值