Netty怎么監(jiān)控內(nèi)存泄露

小編給大家分享一下Netty怎么監(jiān)控內(nèi)存泄露,相信大部分人都還不怎么了解,因此分享這篇文章給大家參考一下,希望大家閱讀完這篇文章后大有收獲,下面讓我們一起去了解一下吧!

成都創(chuàng)新互聯(lián)長(zhǎng)期為1000多家客戶提供的網(wǎng)站建設(shè)服務(wù),團(tuán)隊(duì)從業(yè)經(jīng)驗(yàn)10年,關(guān)注不同地域、不同群體,并針對(duì)不同對(duì)象提供差異化的產(chǎn)品和服務(wù);打造開放共贏平臺(tái),與合作伙伴共同營(yíng)造健康的互聯(lián)網(wǎng)生態(tài)環(huán)境。為延長(zhǎng)企業(yè)提供專業(yè)的網(wǎng)站設(shè)計(jì)、做網(wǎng)站,延長(zhǎng)網(wǎng)站改版等技術(shù)服務(wù)。擁有十載豐富建站經(jīng)驗(yàn)和眾多成功案例,為您定制開發(fā)。

Netty如何監(jiān)控內(nèi)存泄露

前言

一般而言,在Netty程序中都會(huì)采用池化的ByteBuf,也就是PooledByteBuf以提高程序性能。但是PooledByteBuf需要在使用完畢后手工釋放,否則就會(huì)因?yàn)?code>PooledByteBuf申請(qǐng)的內(nèi)存空間沒有歸還進(jìn)而造成內(nèi)存泄露,最終OOM。而一旦泄露發(fā)生,在復(fù)雜的應(yīng)用程序中找到未手工釋放的ByteBuf并不是一個(gè)簡(jiǎn)單的活計(jì),在沒有工具輔助的情況只能白盒檢查所有源碼,效率無(wú)疑十分低下。

為了解決這個(gè)問題,Netty設(shè)計(jì)了專門的泄露檢測(cè)接口用于實(shí)現(xiàn)對(duì)需要手動(dòng)釋放的資源對(duì)象的監(jiān)控。

JDK的弱引用和引用隊(duì)列

在分析Netty的泄露監(jiān)控功能之前,先來(lái)復(fù)習(xí)下其中會(huì)用到的JDK知識(shí):引用。

在java中存在4中引用類型,分別是強(qiáng)引用,軟引用,弱引用,虛引用。

強(qiáng)引用

強(qiáng)引用,是我們寫程序最經(jīng)常使用的方式。比如一個(gè)將一個(gè)值賦給一個(gè)變量,那這個(gè)對(duì)象值就被該變量強(qiáng)引用了。除非設(shè)置為null,否則java的內(nèi)存回收不會(huì)回收該對(duì)象。就算是內(nèi)存不足異常發(fā)生也不會(huì)。

軟引用

軟引用所引用的對(duì)象會(huì)在java內(nèi)存不足的時(shí)候,被gc回收。如果gc發(fā)生的時(shí)候,java的內(nèi)存還充足則不會(huì)回收這個(gè)對(duì)象 使用的方式如下

  • SoftReference ref = new SoftReference(new Date());

  • Date tmp = ref.get(); //如果對(duì)象沒有被回收,則這個(gè)get操作會(huì)返回初始化的值。如果被回收了之后,則返回null

弱引用

弱引用則比軟引用更差一些。只要是gc發(fā)生的時(shí)候,弱引用的對(duì)象都會(huì)被回收。使用方式上和軟引用類似,如下

  • WeakReference re = new WeakReference(new Date());

  • re.get();

虛引用

虛引用和前面的軟引用、弱引用不同,它并不影響對(duì)象的生命周期。在java中用java.lang.ref.PhantomReference類表示。如果一個(gè)對(duì)象與虛引用關(guān)聯(lián),則跟沒有引用與之關(guān)聯(lián)一樣,在任何時(shí)候都可能被垃圾回收器回收。

除了強(qiáng)引用之外,其余的引用都有一個(gè)引用隊(duì)列可以與之配合。當(dāng)java清理調(diào)用不必要的引用后,會(huì)將這個(gè)引用本身(不是引用指向的值對(duì)象)添加到隊(duì)列之中。代碼如下

ReferenceQueue<Date> queue = new ReferenceQueue<>();
WeakReference<Date> re = new WeakReference<Date>(new Date(), queue);
Reference<? extends Date> moved = queue.poll();

從上面的介紹可以看出引用隊(duì)列的一個(gè)適用場(chǎng)景:與弱引用或虛引用配合,監(jiān)控一個(gè)對(duì)象是否被GC回收。

Netty的實(shí)現(xiàn)思路

針對(duì)需要手動(dòng)關(guān)閉的資源對(duì)象,Netty設(shè)計(jì)了一個(gè)接口io.netty.util.ResourceLeakTracker來(lái)實(shí)現(xiàn)對(duì)資源對(duì)象的追蹤。該接口提供了一個(gè)release方法。在資源對(duì)象關(guān)閉需要調(diào)用release方法。如果從未調(diào)用release方法則被認(rèn)為存在資源泄露。

該接口只有一個(gè)實(shí)現(xiàn),就是io.netty.util.ResourceLeakDetector.DefaultResourceLeak,該實(shí)現(xiàn)繼承了WeakReference。每一個(gè)DefaultResourceLeak會(huì)與一個(gè)需要監(jiān)控的資源對(duì)象關(guān)聯(lián),同時(shí)關(guān)聯(lián)著一個(gè)引用隊(duì)列。

當(dāng)資源對(duì)象被GC回收后,與之關(guān)聯(lián)的DefaultResourceLeak就會(huì)進(jìn)入引用隊(duì)列。通過(guò)檢查引用隊(duì)列中的DefaultResourceLeak實(shí)例的狀態(tài)(release方法的調(diào)用會(huì)導(dǎo)致狀態(tài)變更),就能確定在資源對(duì)象被GC前,是否執(zhí)行了手動(dòng)關(guān)閉的相關(guān)方法,從而判斷是否存在泄漏可能。

代碼實(shí)現(xiàn)

分配監(jiān)控對(duì)象

當(dāng)進(jìn)行ByteBuf的分配的時(shí)候,比如方法io.netty.buffer.PooledByteBufAllocator#newHeapBuffer,查看代碼如下

protected ByteBuf newHeapBuffer(int initialCapacity, int maxCapacity) {
        PoolThreadCache cache = threadCache.get();
        PoolArena<byte[]> heapArena = cache.heapArena;
        final ByteBuf buf;
        if (heapArena != null) {
            buf = heapArena.allocate(cache, initialCapacity, maxCapacity);
        } else {
            buf = PlatformDependent.hasUnsafe() ?
                    new UnpooledUnsafeHeapByteBuf(this, initialCapacity, maxCapacity) :
                    new UnpooledHeapByteBuf(this, initialCapacity, maxCapacity);
        }
        return toLeakAwareBuffer(buf);
    }

當(dāng)實(shí)際持有內(nèi)存區(qū)域的ByteBuf生成,通過(guò)方法io.netty.buffer.AbstractByteBufAllocator#toLeakAwareBuffer(io.netty.buffer.ByteBuf)加持監(jiān)控泄露的能力。該方法代碼如下

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

根據(jù)不同的監(jiān)控級(jí)別生成不同的監(jiān)控等級(jí)對(duì)象。Netty對(duì)監(jiān)控分為4個(gè)等級(jí):

  1. 關(guān)閉:這種模式下不進(jìn)行泄露監(jiān)控。

  2. 簡(jiǎn)單:這種模式下以1/128的概率抽取ByteBuf進(jìn)行泄露監(jiān)控。

  3. 增強(qiáng):在簡(jiǎn)單的基礎(chǔ)上,每一次對(duì)ByteBuf的調(diào)用都會(huì)嘗試記錄調(diào)用軌跡,消耗較大。

  4. 偏執(zhí):在增強(qiáng)的基礎(chǔ)上,對(duì)每一個(gè)ByteBuf都進(jìn)行泄露監(jiān)控,消耗最大。

一般而言,在項(xiàng)目的初期使用簡(jiǎn)單模式進(jìn)行監(jiān)控,如果沒有問題一段時(shí)間后就可以關(guān)閉。否則升級(jí)到增強(qiáng)或者偏執(zhí)模式嘗試確認(rèn)泄露位置。

追蹤和檢查泄露

泄露的檢查和追蹤主要依靠?jī)蓚€(gè)類io.netty.util.ResourceLeakDetector.DefaultResourceLeakio.netty.util.ResourceLeakDetector.前者用于追蹤一個(gè)資源對(duì)象,并且記錄對(duì)應(yīng)的調(diào)用軌跡;后者則負(fù)責(zé)管理和生成DefaultResourceLeak對(duì)象。

DefaultResourceLeak

首先來(lái)看用于追蹤資源對(duì)象的監(jiān)控對(duì)象。該類繼承了WeakReference,有幾個(gè)重要的屬性,如下

//存儲(chǔ)著最新的調(diào)用軌跡信息,record內(nèi)部通過(guò)next指針形成一個(gè)單向鏈表
private volatile Record head;
//調(diào)用軌跡不會(huì)無(wú)限制的存儲(chǔ),有一個(gè)上限閥值。超過(guò)了閥值會(huì)拋棄掉一些調(diào)用軌跡信息。
private volatile int droppedRecords;
//存儲(chǔ)著所有的追蹤對(duì)象,用于確認(rèn)追蹤對(duì)象是否處于可用。
private final Set<DefaultResourceLeak<?>> allLeaks;
//記錄追蹤對(duì)象的hash值,用于后續(xù)操作中的對(duì)象對(duì)比。
private final int trackedHash;

這個(gè)類的作用有三個(gè):

  1. 調(diào)用record方法記錄調(diào)用軌跡

  2. 調(diào)用close方法結(jié)束追蹤

  3. 以及本身作為WeakReference,在追蹤對(duì)象被GC回收后自身被入列到ReferenceQueue中。

先來(lái)看下record方法,代碼如下

@Override
public void record() {
 record0(null);
 }
@Override
public void record(Object hint) {
    record0(hint);
}
private void record0(Object hint) {
            if (TARGET_RECORDS > 0) {
                Record oldHead;
                Record prevHead;
                Record newHead;
                boolean dropped;
                do {
                    if ((prevHead = oldHead = headUpdater.get(this)) == null) {
                        // already closed.
                        return;
                    }
                    final int numElements = oldHead.pos + 1;
                    if (numElements >= TARGET_RECORDS) {
                        final int backOffFactor = Math.min(numElements - TARGET_RECORDS, 30);
                        if (dropped = PlatformDependent.threadLocalRandom().nextInt(1 << backOffFactor) != 0) {
                            prevHead = oldHead.next;
                        }
                    } else {
                        dropped = false;
                    }
                    newHead = hint != null ? new Record(prevHead, hint) : new Record(prevHead);
                } while (!headUpdater.compareAndSet(this, oldHead, newHead));
                if (dropped) {
                    droppedRecordsUpdater.incrementAndGet(this);
                }
            }
        }

方法record0的思路總結(jié)下也很簡(jiǎn)單,概括如下:

  1. 使用CAS方式當(dāng)前的調(diào)用軌跡對(duì)象Record設(shè)置為head屬性的值。

  2. Record對(duì)象中的pos屬性記錄著當(dāng)前軌跡鏈的長(zhǎng)度,當(dāng)追蹤對(duì)象的軌跡隊(duì)鏈的長(zhǎng)度超過(guò)配置值時(shí),有一定的幾率(1-1/2<sup>min(n-target_record,30)</sup>)將最新的軌跡對(duì)象從鏈條中刪除。

  3. CAS成功后,如果有拋棄頭部的軌跡對(duì)象,則拋棄計(jì)數(shù)+1。

步驟2中在鏈條過(guò)長(zhǎng)時(shí)選擇刪除最新的軌跡對(duì)象是基于以下兩點(diǎn)出發(fā):

  1. 一般泄漏都發(fā)生在最后一次使用后忘記調(diào)用釋放方法造成,因此替換最新的歸集對(duì)象,并不會(huì)造成判斷信息的丟失

  2. 一般而言,關(guān)注泄漏對(duì)象,也需要了解對(duì)象實(shí)例的申請(qǐng)位置,因此刪除節(jié)點(diǎn)時(shí)不能從頭開始刪除。

在來(lái)看看close方法。代碼如下

public boolean close(T trackedObject) {
            assert trackedHash == System.identityHashCode(trackedObject);
            try {
                return close();
            } finally {
                reachabilityFence0(trackedObject);
            }
        }
public boolean close() {
            if (allLeaks.remove(this)) {
                // Call clear so the reference is not even enqueued.
                clear();
                headUpdater.set(this, null);
                return true;
            }
            return false;
        }
private static void reachabilityFence0(Object ref) {
            if (ref != null) {
                synchronized (ref) {
                }
            }
        }

close方法本身沒有什么,就是將資源進(jìn)行了清除。需要解釋的是方法reachabilityFence0。不過(guò)該方法需要在下文的報(bào)告泄露中才會(huì)具備作用,這邊先暫留。

ResourceLeakDetector

該類用于按照規(guī)則進(jìn)行追蹤對(duì)象的生成,外部主要是調(diào)用其方法track,代碼如下

public final ResourceLeakTracker<T> track(T obj) {
        return track0(obj);
    }
private DefaultResourceLeak track0(T obj) {
        Level level = ResourceLeakDetector.level;
        if (level == Level.DISABLED) {
            return null;
        }
        if (level.ordinal() < Level.PARANOID.ordinal()) {
            if ((PlatformDependent.threadLocalRandom().nextInt(samplingInterval)) == 0) {
                reportLeak();
                return new DefaultResourceLeak(obj, refQueue, allLeaks);
            }
            return null;
        }
        reportLeak();
        return new DefaultResourceLeak(obj, refQueue, allLeaks);
    }

從生成策略來(lái)看,只要是小于PARANOID級(jí)別都是抽樣生成。生成的追蹤對(duì)象上一個(gè)章節(jié)已經(jīng)分析過(guò)了,這邊主要來(lái)看reportLeak方法,如下

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;
            }
            //返回true意味著資源沒有調(diào)用close或者dispose方法結(jié)束追蹤就被GC了,意味著該資源存在泄漏。
            if (!ref.dispose()) {
                continue;
            }
            String records = ref.toString();
            if (reportedLeaks.putIfAbsent(records, Boolean.TRUE) == null) {
                if (records.isEmpty()) {
                    reportUntracedLeak(resourceType);
                } else {
                    reportTracedLeak(resourceType, records);
                }
            }
        }
    }
boolean io.netty.util.ResourceLeakDetector.DefaultResourceLeak#dispose() {
            clear();
            return allLeaks.remove(this);
        }

可以看到,每次生成資源追蹤對(duì)象時(shí),都會(huì)遍歷引用隊(duì)列,如果發(fā)現(xiàn)泄漏對(duì)象,則進(jìn)行日志輸出。

這里面有個(gè)細(xì)節(jié)的設(shè)計(jì)點(diǎn)在于DefaultResourceLeak進(jìn)入引用隊(duì)列并不意味著一定內(nèi)存泄露。判斷追蹤對(duì)象是否泄漏的規(guī)則是對(duì)象在被GC之前是否調(diào)用了DefaultResourceLeakclose方法。舉個(gè)例子,PooledByteBuf只要將自身持有的內(nèi)存釋放回池化區(qū)就算是正確的釋放,其后其實(shí)例對(duì)象可以被GC回收掉。

因此方法reportLeak在遍歷引用隊(duì)列時(shí),需要通過(guò)調(diào)用dispose方法來(lái)確認(rèn)追蹤對(duì)象的dispose是否調(diào)用或者close方法是否被調(diào)用過(guò)。如果dispose方法返回true,則意味著被追蹤對(duì)象未調(diào)用關(guān)閉方法就被GC,那就意味著造成了泄露。

上個(gè)章節(jié)曾提到的一個(gè)方法reachabilityFence0。

在JVM的規(guī)定中,如果一個(gè)實(shí)例對(duì)象不再被需要,則可以判定為可回收。即使該實(shí)例對(duì)象的一個(gè)具體方法正在執(zhí)行過(guò)程中,也是可以的。更確切一些的說(shuō),如果一個(gè)實(shí)例對(duì)象的方法體中,不再需要讀取或者寫入實(shí)例對(duì)象的屬性,則此時(shí)JVM可以回收該對(duì)象,即使方法還沒有完成。

然而這樣會(huì)導(dǎo)致一個(gè)問題,在close方法中,如果close方法還沒有執(zhí)行完畢,trackedObject對(duì)象實(shí)例就被GC回收了,就會(huì)導(dǎo)致DefaultResourceLeak對(duì)象被加入到引用隊(duì)列中,從而可能在reportLeak方法調(diào)用中觸發(fā)方法dispose,假設(shè)此時(shí)close方法才剛開始執(zhí)行,則dispose方法可能返回true。程序就會(huì)判定這個(gè)對(duì)象出現(xiàn)了泄露,然而實(shí)際上卻沒有。

要解決這個(gè)問題,只需要讓close方法執(zhí)行完畢前,讓對(duì)象不要回收即可。reachabilityFence0方法就完成了這個(gè)作用。

以上是“Netty怎么監(jiān)控內(nèi)存泄露”這篇文章的所有內(nèi)容,感謝各位的閱讀!相信大家都有了一定的了解,希望分享的內(nèi)容對(duì)大家有所幫助,如果還想學(xué)習(xí)更多知識(shí),歡迎關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道!

名稱欄目:Netty怎么監(jiān)控內(nèi)存泄露
網(wǎng)頁(yè)地址:http://muchs.cn/article36/ihgssg.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供品牌網(wǎng)站建設(shè)、網(wǎng)站營(yíng)銷、虛擬主機(jī)、Google、微信小程序、網(wǎng)站建設(shè)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)

小程序開發(fā)