亚洲中字慕日产2020,大陆极品少妇内射AAAAAA,无码av大香线蕉伊人久久,久久精品国产亚洲av麻豆网站

資訊專欄INFORMATION COLUMN

小心遞歸中內(nèi)存泄漏

layman / 983人閱讀

摘要:小心遞歸中內(nèi)存泄漏前段時間由于業(yè)務(wù)需要,需要從數(shù)據(jù)庫中查詢出來所有滿足條件的數(shù)據(jù),然后導(dǎo)入到文件中。綜上,我們可以得知程序出現(xiàn)了內(nèi)存泄漏。

小心遞歸中內(nèi)存泄漏

前段時間由于業(yè)務(wù)需要,需要從數(shù)據(jù)庫中查詢出來所有滿足條件的數(shù)據(jù),然后導(dǎo)入到文件中。于是隨便寫了個程序,查詢出所有滿足條件然后再寫入文件。但是實際上線后卻發(fā)現(xiàn),程序剛開始運行馬上看到部分?jǐn)?shù)據(jù)寫入到文件,但是后面運行越來越慢,于是對此分析排查了一下。

應(yīng)用環(huán)境

JDK 1.7 + Spring 4.3 + mybatis + oracle

問題排查

查詢以及寫入文件偽代碼如下:

    private void queryAllData(Request request, List querData, int count, String path, List allData) {
        if (CollectionUtils.isEmpty(querData)) {
            return;
        }
        allData.addAll(querData);
        // 總 List 大于一定指定數(shù)量將數(shù)據(jù)刷新到文件
        if (allData.size() > 20000) {
            saveToFile(request, allData, path);
        }
        // 判斷下一個偏移量 是否大于 總數(shù)
        request.setPageNo(request.getPageNo() + 1);
        // 查詢下一頁數(shù)據(jù)
        List  newQueryData = queryDao.selectDataByPage(request);
        queryAllData(request, newQueryData, count, path, allData);
    }

其中 queryDao.selectDataByPage 為一個分頁查找方法。這個方法目的就在于遞歸查找分頁數(shù)據(jù),如果某一頁數(shù)據(jù)為空,就代表查詢結(jié)束,此時已查詢出所有數(shù)據(jù)。

為什么不直接執(zhí)行 select * from table where a=xx 類似的數(shù)據(jù)直接查出所有數(shù)據(jù)?

因為寫程序之前,查詢了一下滿足條件的數(shù)據(jù)總共有 200 w 數(shù)據(jù),這樣如果直接一把查詢出所有數(shù)據(jù),主要擔(dān)心堆內(nèi)存直接占滿,導(dǎo)致 OOM 錯誤。

寫完代碼,部署到線上,然后執(zhí)行導(dǎo)出數(shù)據(jù),就放著不管,干其他事。過一段時間回來看數(shù)據(jù)導(dǎo)出結(jié)果,這個時候大吃一驚,程序竟然還沒有結(jié)束,數(shù)據(jù)也才導(dǎo)出 3/4 左右。這個時候意識到程序肯定存在問題,于是仔細(xì)檢查了一遍代碼,也沒看出什么。

沒辦法,這個時候只能分析線上程序 GC 情況了,幸好開啟了打印 GC 日志的選項。拿到 GC 日志文件后,由于不太精通 GC 日志詳細(xì)內(nèi)容,只能借靠外部力量了。GC 日志分析網(wǎng)站,該網(wǎng)站可以分析 GC 日志,然后可以查看各個時間點堆內(nèi)存占用情況。分析情況如圖。

這張圖為 GC 之后堆內(nèi)存占用情況。可以看出堆內(nèi)存在 Full GC 之后并沒有很快的降下來且很快下一次 Full GC 就開始了。這樣大致可以看出,程序沒有在期待時間內(nèi)運行結(jié)束,就是由于堆內(nèi)被占用過多,持續(xù)引起Full GC,應(yīng)用程序線程持續(xù)被掛起。然后我們再看堆內(nèi)存老年代占用情況。

如上圖,堆內(nèi)存老年代占用空間持續(xù)上升直到接近占滿,引起 Full GC,并沒有緩解這種情況,之后內(nèi)存占用一直接近到占滿。

綜上,我們可以得知程序出現(xiàn)了內(nèi)存泄漏。

知道了原因,我們就好順著找到問題。又順著捋了一遍代碼,可惜的是并沒有看出問題。難道是 allData 數(shù)據(jù)集合越來越大,然后導(dǎo)致該現(xiàn)象?仔細(xì)查看了 saveToFile 代碼邏輯。

        List lines = Lists.newArrayListWithExpectedSize(allData.size());
        for (Data data : allData) {
            String line = process(data);
            lines.add(line);
        }
        String fileName = "xx.txt";
         try {
            log.info("文件開始輸出,輸出行數(shù){}", lines.size());
            FileUtils.writeLines(new File(fileName), "utf-8", lines, true);
            allData.clear();
            lines = null;
        } catch (IOException e) {
            log.error("文件輸出失敗", e);
            // 輸出失敗,先不管了,將數(shù)據(jù)繼續(xù)保存集合中
        }

可以看到,數(shù)據(jù)一旦寫入到文件中,allData 集合立刻清空,所以不可能是該問題導(dǎo)致。

看了好幾遍代碼之后,還是無法確定問題原因。最后一遍查看代碼,靈關(guān)一現(xiàn),不會是 newQueryData 導(dǎo)致的問題吧?嘗試把這里代碼改成下面方式。

    private void queryAllData(Request request, List querData, int count, String path, List allData) {
        if (CollectionUtils.isEmpty(querData)) {
            return;
        }
        allData.addAll(querData);
        // queryData 放入到 allData 中后,將 querData 結(jié)合清空。
        querData.clear();
        // 總 List 大于一定指定數(shù)量將數(shù)據(jù)刷新到文件
        if (allData.size() > 20000) {
            saveToFile(request, allData, path);
        }
        // 判斷下一個偏移量 是否大于 總數(shù)
        request.setPageNo(request.getPageNo() + 1);
        // 查詢下一頁數(shù)據(jù)
        newQueryData = queryDao.selectDataByPage(request);
        queryAllData(request, newQueryData, count, path, allData);

改完代碼,立刻部署,開始運行程序。這個時候查看堆內(nèi)存占用情況,就可以知道改動是否有效。這里推薦一個方便查看 JVM 進(jìn)程信息的工具 vjtop??梢钥焖俨榭炊褍?nèi)存占用情況。

運行 vjtop 之后,一直盯著堆內(nèi)存占用情況。然后發(fā)現(xiàn) eden 空間持續(xù)上升直到接近到滿,然后發(fā)生 Minor GC ,eden 空間迅速清空。 old 區(qū)內(nèi)存也沒有一直占用接近到滿這么夸張。大概占用 1/5 內(nèi)存。改善情況如想象中一致,等待一定時間后,數(shù)據(jù)導(dǎo)出完畢。

分析

現(xiàn)在我們分析為什么出現(xiàn)內(nèi)存泄漏。

我們知道 jvm 運行時,內(nèi)存區(qū)分為 堆,虛擬機(jī)棧,方法區(qū)等。上面我們發(fā)生的現(xiàn)象就與虛擬機(jī)棧有關(guān)。

什么事虛擬機(jī)棧?

摘錄深入 Java 虛擬機(jī)一書解釋

虛擬機(jī)棧描述的是 Java 方法執(zhí)行的內(nèi)存模型:每個方法執(zhí)行時都會創(chuàng)建一個棧幀用于存儲局部變量表,操作數(shù)棧,動態(tài)鏈接,方法出口等信息。每一個方法從調(diào)用直至執(zhí)行完后的過程,就對應(yīng)一個棧幀在虛擬機(jī)棧中入棧到出棧的過程。

Java 線程執(zhí)行方法時,jvm 虛擬機(jī)棧數(shù)據(jù)結(jié)構(gòu)如圖所示。

可以看出,我們在調(diào)用函數(shù) 1 時,就將該棧幀壓如棧中。函數(shù) 1 調(diào)用函數(shù) 2 時,也將該棧幀壓入棧中。處于棧中的棧幀包含局部變量表,操作數(shù)幀等,而局部變量表包含基本數(shù)據(jù)類型,以及對象引用指針。對象指針指向堆內(nèi)存對象。就是因為對象引用指針,導(dǎo)致我們上面情況。為何這么說那。我們再看下面這張圖。

我們可以看到,棧中每個方法 newQueryData 都指向堆中真正的對象。由于遞歸執(zhí)行時,前面的方法都壓到棧中,newQueryData 一直還指向堆中對象,然后 GC 時,由于對象還處于被引用,虛擬機(jī)判定該對象存活,所以不清理這些對象。隨著遞歸方法越來越深入,堆積的 newQueryData 越來越多,量表引起質(zhì)變,導(dǎo)致堆內(nèi)存被占滿,引發(fā)虛擬機(jī)持續(xù) GC。但是每次 GC 之后卻無法騰出空間。最后我們看到的現(xiàn)象就是程序執(zhí)行很慢很慢。

 總結(jié)

這個問題本質(zhì)看起來不是很難,但是實際發(fā)生的時候排查問題著實花費不少時間。下面我們總結(jié)一下這個過程。

如果程序?qū)嶋H運行起來與預(yù)想差距太大,那么不用想了,肯定哪里出問題了,趕快登上機(jī)器查看吧。

程序運行必要節(jié)點的日志輸出需要打印。上面程序本來剛開始寫的時候,由于主觀意思,想想沒那么難,很快就擼完部署了。最后查看日志,由于沒有必要的日志輸出,都不知道程序卡在那了。

需要了解一些 JVM 相關(guān)工具,可以及時查看 JVM 相關(guān)情況,如內(nèi)存使用情況。如本文的例子,實際上我們可以 dump 內(nèi)存,然后分析哪里發(fā)生了內(nèi)存泄漏。很不幸的是,這方面本人只是處于了解層面,用的時候卻不知道如何下手,只好求助于一些現(xiàn)成開源工具完成。之后需要好好補(bǔ)這方面操作能力,哈哈哈。

本文如果使用 while 循環(huán)代替遞歸方式,問題可能更快定位。遞歸中的內(nèi)存泄漏可能更加隱蔽,很容易被我們忽略,同學(xué)們下次再寫遞歸方法的時候不僅要注意遞歸方法深度,還要注意這個過程需要及時釋放無用對象,不要讓內(nèi)存泄漏發(fā)生。

好了,文章大概就這樣了,下次文章再見了。

參考文章以及網(wǎng)站

深入 Java 虛擬機(jī) 堆內(nèi)存章節(jié)

Java JVM 中 堆,棧,方法區(qū) 詳解

gc 日志分析網(wǎng)站

查看 JVM 進(jìn)程信息的工具 -- vjtop

文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。

轉(zhuǎn)載請注明本文地址:http://www.ezyhdfw.cn/yun/72644.html

相關(guān)文章

  • 【進(jìn)階1-5期】JavaScript深入之4類常見內(nèi)存泄漏及如何避免

    摘要:本期推薦文章類內(nèi)存泄漏及如何避免,由于微信不能訪問外鏈,點擊閱讀原文就可以啦。四種常見的內(nèi)存泄漏劃重點這是個考點意外的全局變量未定義的變量會在全局對象創(chuàng)建一個新變量,如下。因為老版本的是無法檢測節(jié)點與代碼之間的循環(huán)引用,會導(dǎo)致內(nèi)存泄漏。 (關(guān)注福利,關(guān)注本公眾號回復(fù)[資料]領(lǐng)取優(yōu)質(zhì)前端視頻,包括Vue、React、Node源碼和實戰(zhàn)、面試指導(dǎo)) 本周正式開始前端進(jìn)階的第一期,本周的主題...

    red_bricks 評論0 收藏0
  • 2017拼多多前端筆試

    摘要:但是如果一個值不再用到了,引用次數(shù)卻不為,垃圾回收機(jī)制卻無法釋放這塊內(nèi)存,從而導(dǎo)致內(nèi)存泄漏。內(nèi)存泄漏垃圾回收語言的內(nèi)存泄漏主因是不需要的引用。常見內(nèi)存泄漏意外的全局變量處理未定義變量的方式比較寬松未定義的變量會在全局對象創(chuàng)建一個新變量。 簡答題: settimeout 與 setInterval的區(qū)別, 及對他們的內(nèi)存的分析 區(qū)別 setTimeout是在一段時間后調(diào)用指定函數(shù)(僅一...

    Jioby 評論0 收藏0
  • 2017拼多多前端筆試

    摘要:但是如果一個值不再用到了,引用次數(shù)卻不為,垃圾回收機(jī)制卻無法釋放這塊內(nèi)存,從而導(dǎo)致內(nèi)存泄漏。內(nèi)存泄漏垃圾回收語言的內(nèi)存泄漏主因是不需要的引用。常見內(nèi)存泄漏意外的全局變量處理未定義變量的方式比較寬松未定義的變量會在全局對象創(chuàng)建一個新變量。 簡答題: settimeout 與 setInterval的區(qū)別, 及對他們的內(nèi)存的分析 區(qū)別 setTimeout是在一段時間后調(diào)用指定函數(shù)(僅一...

    caiyongji 評論0 收藏0
  • 2017拼多多前端筆試

    摘要:但是如果一個值不再用到了,引用次數(shù)卻不為,垃圾回收機(jī)制卻無法釋放這塊內(nèi)存,從而導(dǎo)致內(nèi)存泄漏。內(nèi)存泄漏垃圾回收語言的內(nèi)存泄漏主因是不需要的引用。常見內(nèi)存泄漏意外的全局變量處理未定義變量的方式比較寬松未定義的變量會在全局對象創(chuàng)建一個新變量。 簡答題: settimeout 與 setInterval的區(qū)別, 及對他們的內(nèi)存的分析 區(qū)別 setTimeout是在一段時間后調(diào)用指定函數(shù)(僅一...

    genefy 評論0 收藏0
  • JavaScript 究竟是如何工作的?(第二部分)

    摘要:內(nèi)存泄漏指的是,程序之前需要用到部分內(nèi)存,而這部分內(nèi)存在用完之后并沒有返回到內(nèi)存池?;臼录f歸調(diào)用為什么是單線程的一個線程代表著在同一時間段內(nèi)可以單獨執(zhí)行的程序部分的數(shù)目。 原文地址:How Does JavaScript Really Work? (Part 2) 原文作者:Priyesh Patel showImg(https://segmentfault.com/img...

    Youngs 評論0 收藏0

發(fā)表評論

0條評論

最新活動
閱讀需要支付1元查看
<