一. 强引用、软引用、弱引用、虚引用
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的情况