reference、referenceQueue、及应用

一. 强引用、软引用、弱引用、虚引用

1.GC时Reference的守护线程tryPending被唤醒时,

a.软引用、弱引用对象的referent会被置空,之后并将弱引用加入引用队列。

b.FinalReference的referent也不会置空。

c.PhantomReference:tryHandlePending的时候referent不为空,是Gc将要释放,还没释放。

2.关于ReferenceQueue

a. WeakReference和SoftReference的ReferenceQueue只是起到监控作用, 被压入到队列中的弱引用指向惹referent已经被释放了。

b.FinalReference的ReferenceQueue起到保存Finalizer的对象的作用,便于FinalizerThread轮询ReferenceQueue从中取出Finalizer。

c. PhantomReference

Finalizer和Cleaner都有一个链表的强引用,Finalizer守护线程会将这个引用remove掉,reference的守护线程会触发clean将强引用也给remove掉。至于在tryHandlepending的时候,referent不为空,就是因为这两个链表导致的。被链表remove之后,自然就可回收了。

二. 应用

1.ThreadLocalMap

         ThreadLocal每个线程有独立的内存空间,ThreadLocal.ThreadLocalMap threadLocals = null;ThreadLocal.ThreadLocalMap是以ThreadLocal为key的map。ThreadLocalMap.Entry继承自WeakReference,初始化的时候,会将Entry的key传入Reference.referent。

        每个线程的threadlocalMap以ThreadLocal为key的原因:每个线程可以有许多个ThreadLocal变量,要不咋取名复数s。

Thread.threadLocals        

 ThreadLocalMap.Entry: 并非是链表(区别于HashMap.Entry)

 ThreadLocalMap(开放寻址的线性探测法)区别于Hashmap(链接法)。它内部结构就是数组来存储的,没有链表。因为其通过线性探测法来处理hash冲突。通过静态变量threadLocalHashCode的nexthashcode算法,斐波那契散列法可控制散列值均匀分布。

模拟hash冲突的方式:重写ThreadLocal,替换其nextHashCode

 

set方法:

 ThreadLocalMap.set

        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            ThreadLocalMap.Entry[] tab = table;
            int len = tab.length;
            ThreadLocalMap.Entry e;

            // Back up to check for prior stale entry in current run.
            // We clean out whole runs at a time to avoid continual
            // incremental rehashing due to garbage collector freeing
            // up refs in bunches (i.e., whenever the collector runs).
            // 往前遍历,遇到空的Entry退出循环,遇到Entry的key == null(失效的slot)
            // 则替换掉slotToExpunge,清理工作就从slotToExpunge开始的
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;

            // Find either the key or trailing null slot of run, whichever
            // occurs first
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                // If we find key, then we need to swap it
                // with the stale entry to maintain hash table order.
                // The newly stale slot, or any other stale slot
                // encountered above it, can then be sent to expungeStaleEntry
                // to remove or rehash all of the other entries in run.
                // k的目标索引下被staleSlot的占用了,后续staleSlot的强应用被null只存在弱引用。
                // 则需将k和staleSlot换位置。否则staleSlot的Entry被置空释放了,后续k再set一下,
                // 就会在staleSlot多出一个Entry,与k对应的Entry一致。因为遇到空则插入。
                if (k == key) {
                    e.value = value;
                    // 交换位置之后,只需将i为索引的Entry清理即可,因是向后遍历的
                    // 所以i一定是在以slotToExpunge为起始位置的清理区间的。
                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    // slotToExpunge == staleSlot(向前遍历没找到失效的slot)
                    // 且staleSlot和i对应的Entry已交换,此时i对应了无效的slot,则需将slotToExpunge置为i
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    // 启发式清理(log2(len)次尝试),连续段的清理slotToExpunge~null的区间。
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // If we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
                // 如果slotToExpunge == staleSlot(向前遍历没找到失效的slot) 且当前k == null
                // 则slotToExpunge置为当前的索引,从此开始清理垃圾
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // If key not found, put new entry in stale slot
            tab[staleSlot].value = null;
            tab[staleSlot] = new ThreadLocalMap.Entry(key, value);

            // If there are any other stale entries in run, expunge them
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

ThreadLocal采用弱引用,针对内存泄漏多层保险,当线程池中线程后续的threadLocals中get、set方法都会自清理失效的slot(Entry.key即ThreadLocal的强引用被释放后)。但是后续如果无get、set操作,还是出现内存泄漏。

2.finalize

对象实现了finalize方法,称f类,即可再创建的时候,被注册到Finalizer的双向队列unfinalized(静态的所有Finalizer对象共用)。这将导致f类多了一层强引用链。导致Gc时不能立即回收。

当f类的强引用被释放之后,只存在Finalizer的引用,jvm gc时会将Reference的pending(静态)置为FinalReference并调用了lock.notify()唤醒ReferenceHandler。

Reference的守护线程ReferenceHandler会不断的轮询tryHandlePending。Finalize的referent即f类处于非空状态,tryHandlePending将pending压入queue(Finalize的ReferenceQueue)。

Finalizer的守护线程FinalizerThread,会轮询其queue并取出Finalizer对象,并调用其referent(f类)的finalize方法。因此线程优先级比较低,中途可能进行了多轮的GC,f类因unfinalized链表引用导致还是堆积在老年代。

 完成以上步骤,f类即摆脱了Finalizer的强引用链,下次Gc即可回收f类。

3.netty Recycler: WeakOrderQueue、Stack、DELAYED_RECYCLED

weakHashMap

 与数据关联的线程,已经被释放只存在弱引用了。则需要将其缓存,满足要求的 转移到生产的原始线程中去(即 new这些变量的线程)

4.Cleaner

5. ResourceLeakDetector,DefaultResourceLeak 内存泄漏探测器

 即使选择了Pool池分配内存,但是如果设置的jvm堆内存太小,就会以非对象池分配了。同样直接内存分配太小,也不会以池分配内存。

1.因netty使用了对象池(预分配大的内存),netty使用引用计数,是因为直接的GC会有延迟,导致从对象池中获取的指定位置内存块,不能够即时的重置,从而会导致短时间内存占用飙升。使用计数器,可以即时的释放该内存块,但是使用不恰当时,就回出现内存泄漏,因GC释放了ByteBuf但是,引用计数不为0,导致内存块一直被标记,后续对象只能重新分配未被标记的块。

directAreana就是依赖上图的DEFAULT_NUM_DIRECTARENA

2.选择池化分配器:directArena不为空就池化,否则非池化。

2.1非池化有以下两种情况(是否选择Cleaner释放):

        当不支持unsafe时

        2.1.1 当USE_DIRECT_BUFFER_NO_CLEANER为真,则创建直接内存只能直接使用unsafe,不能使用cleaner来由GC自动释放。所以GC可以释放掉ByteBuf,以及ByteBuffer的引用,但是却无法触发UNSAFE.freeMemory(address),所以需要手动触发release,以及加上引用计数探测直接内存泄漏;

        2.1.2 当USE_DIRECT_BUFFER_NO_CLEANER为false时,就是Nio的ByteBuffer的直接内存分配,使用Cleaner(PhantomReference)来控制堆外内存释放,此处就不需要使用引用计数来探测直接内存泄漏了。ByteBuf释放后,ByteBuffer因虚引用,触发Cleaner的clean方法,去除CLeaner中的链表强引用,从而待GC回收此片直接内存。

2.2 池化就是情况1了。

3.非池化的堆内存,是不需要通过计数器,完全由GC控制,ByteBuffer伴随ByteBuf的释放而释放。

4.引用计数采用AtomicIntegerFieldUpdater,来减少内存的占用。AtomicInteger被每个BuyteBuf持有下,内存占用还是很多的。

5.内存泄漏检测DefaultResourceLeak采用AtomicReferenceFieldUpdater(任意的类型了)来节省内存。

大致检测流程:

 

 release成功的引用计数为0的情况

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,下面是ReferenceQueue的源码分析: ReferenceQueue是一个抽象类,其定义如下: ``` public abstract class ReferenceQueue<T> { static ReferenceQueue<Object> NULL = new Null<>(); static class Null<T> extends ReferenceQueue<T> { @SuppressWarnings("unchecked") public Reference<? extends T> poll() { return null; } public Reference<? extends T> remove(long timeout) throws IllegalArgumentException { throw new IllegalArgumentException(); } public Reference<? extends T> remove() { return null; } } ... } ``` ReferenceQueue类中只定义了一个静态的内部类Null,同时该类中也没有任何实例变量和实例方法。Null类实现了ReferenceQueue抽象类,并重写了父类中的三个方法。这三个方法分别是: - poll():从队列中获取并移除一个引用对象,如果队列为空则返回null。 - remove(long timeout):从队列中获取并移除一个引用对象,如果队列为空则等待指定的时间,如果在指定的时间内队列仍为空则返回null。 - remove():从队列中获取并移除一个引用对象,如果队列为空则一直等待,直到队列非空。 在Java中,我们可以使用ReferenceQueue来监控一个对象是否被垃圾回收。当一个对象被垃圾回收时,与该对象关联的引用对象将会被加入到ReferenceQueue中。通过使用ReferenceQueue,我们可以在另一个线程中对被回收的对象进行处理,执行一些必要的清理工作或者记录日志等操作。 在使用ReferenceQueue时,我们需要将要被监控的引用对象与ReferenceQueue关联起来,一般使用弱引用、软引用或虚引用来实现。例如,我们可以使用如下代码将一个对象obj与一个ReferenceQueue关联起来: ``` ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>(); WeakReference<Object> weakReference = new WeakReference<>(obj, referenceQueue); ``` 在这个示例中,我们使用了一个WeakReference来关联obj对象和referenceQueue,当obj对象被垃圾回收时,与obj关联的弱引用将会被加入到referenceQueue中。我们可以在另一个线程中通过调用referenceQueue.poll()方法来获取被回收的弱引用对象,从而进行必要的清理工作。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值