成人国产在线小视频_日韩寡妇人妻调教在线播放_色成人www永久在线观看_2018国产精品久久_亚洲欧美高清在线30p_亚洲少妇综合一区_黄色在线播放国产_亚洲另类技巧小说校园_国产主播xx日韩_a级毛片在线免费

資訊專欄INFORMATION COLUMN

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

layman / 929人閱讀

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

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

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

應(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);
        }
        // 判斷下一個(gè)偏移量 是否大于 總數(shù)
        request.setPageNo(request.getPageNo() + 1);
        // 查詢下一頁(yè)數(shù)據(jù)
        List  newQueryData = queryDao.selectDataByPage(request);
        queryAllData(request, newQueryData, count, path, allData);
    }

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

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

因?yàn)閷懗绦蛑?,查詢了一下滿足條件的數(shù)據(jù)總共有 200 w 數(shù)據(jù),這樣如果直接一把查詢出所有數(shù)據(jù),主要擔(dān)心堆內(nèi)存直接占滿,導(dǎo)致 OOM 錯(cuò)誤。

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

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

這張圖為 GC 之后堆內(nèi)存占用情況??梢钥闯龆褍?nèi)存在 Full GC 之后并沒有很快的降下來且很快下一次 Full GC 就開始了。這樣大致可以看出,程序沒有在期待時(shí)間內(nèi)運(yùn)行結(jié)束,就是由于堆內(nèi)被占用過多,持續(xù)引起Full GC,應(yīng)用程序線程持續(xù)被掛起。然后我們?cè)倏炊褍?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),不會(huì)是 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);
        }
        // 判斷下一個(gè)偏移量 是否大于 總數(shù)
        request.setPageNo(request.getPageNo() + 1);
        // 查詢下一頁(yè)數(shù)據(jù)
        newQueryData = queryDao.selectDataByPage(request);
        queryAllData(request, newQueryData, count, path, allData);

改完代碼,立刻部署,開始運(yùn)行程序。這個(gè)時(shí)候查看堆內(nèi)存占用情況,就可以知道改動(dòng)是否有效。這里推薦一個(gè)方便查看 JVM 進(jìn)程信息的工具 vjtop。可以快速查看堆內(nèi)存占用情況。

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

分析

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

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

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

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

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

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

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

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

 總結(jié)

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

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

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

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

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

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

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

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

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

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

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

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

轉(zhuǎn)載請(qǐng)注明本文地址:http://systransis.cn/yun/72644.html

相關(guān)文章

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

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

    red_bricks 評(píng)論0 收藏0
  • 2017拼多多前端筆試

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

    Jioby 評(píng)論0 收藏0
  • 2017拼多多前端筆試

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

    caiyongji 評(píng)論0 收藏0
  • 2017拼多多前端筆試

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

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

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

    Youngs 評(píng)論0 收藏0

發(fā)表評(píng)論

0條評(píng)論

最新活動(dòng)
閱讀需要支付1元查看
<