記一次java內存溢出的解決過程


  注:本文主要記錄這次解決內存溢出問題的過程而不是具體問題。

  最近在寫一個搜索引擎,使用倒排索引結構進行文檔檢索,保存索引的基本思想是先將倒排列表保存到內存中一個有序Map里(TreeMap),然后當內存占用達到一定閾值的時候將內存中的倒排列表有序寫入磁盤,當磁盤已經存在索引時,則將內存中的索引和磁盤中的索引進行合並,生成新的索引,合並過程類似於歸並排序。合並內存索引和磁盤索引的代碼如下:

    public synchronized void merge(){
        LogUtil.info("InvertIndex merge start...");
        File f=new File(path); //這個文件是原磁盤上的索引文件
//磁盤存在索引,合並磁盤索引和內存索引
if (f.exists()) { String outPath=path+".temp"; File outFile=new File(outPath); TreeMap<String, TreeSet<Long>> ramSnapshot=null; ramSnapshot=ram; //ram保存的是內存索引,這里因為ram可能被其他添加文檔的線程修改,因此先存一份快照然后將ram清空,后面實際操作的是快照 ram=new TreeMap<>(); BufferedReader reader=null; PrintWriter writer=null; try {
          //合並過程,有3個指針:分別指向內存索引的當前位置、原磁盤索引讀到的位置、新的磁盤索引寫入的位置 Iterator
<Entry<String, TreeSet<Long>>> ramIterator= ramSnapshot.entrySet().iterator(); reader=new BufferedReader(new FileReader(f)); writer=new PrintWriter(new BufferedWriter(new FileWriter(outFile))); Entry<String, TreeSet<Long>> entry=ramIterator.hasNext()?ramIterator.next():null; String line=reader.readLine(); while (entry!=null&&line!=null) { long freeRam=Runtime.getRuntime().freeMemory()/1000/1000; System.out.println("freeRam: "+freeRam); String ramWord=entry.getKey(); String diskWord=line.split(separator1)[0]; String out=""; int c=ramWord.compareTo(diskWord);
            //合並過程,因為是兩個有序列表,采用類似歸並排序的合並方法,區別在於這里如果遇到倒排詞相等的時候,需要合並到一個倒排詞(合並兩者文檔列表)
if (c==0) { TreeSet<Long> ramDocIds=entry.getValue(); TreeSet<Long> diskDocIds=this.convertLine2DocIds(line); TreeSet<Long> union=ramDocIds; union.addAll(diskDocIds); out=this.convertIndex2Line(ramWord, union); entry=ramIterator.hasNext()?ramIterator.next():null; line=reader.readLine(); }else if (c<0) { out=this.convertIndex2Line(ramWord, entry.getValue()); entry=ramIterator.hasNext()?ramIterator.next():null; }else { out=this.convertIndex2Line(diskWord, this.convertLine2DocIds(line)); line=reader.readLine(); } writer.println(out); } LogUtil.info("InvertIndex complex merge complete."); while (ramIterator.hasNext()) { entry=ramIterator.next(); String out=this.convertIndex2Line(entry.getKey(), entry.getValue()); writer.println(out); } LogUtil.info("InvertIndex ram merge complete."); while ((line=reader.readLine())!=null) { writer.println(line); } LogUtil.info("InvertIndex disk merge complete."); } catch (Exception e) { LogUtil.err("merge ram index and disk index fail.", e); }finally { try { if (reader!=null) { reader.close(); } if (writer!=null) { writer.close(); } } catch (Exception e2) {LogUtil.err("release resource fail.", e2);} } f.delete(); if (!outFile.renameTo(new File(path))) { throw new RuntimeException("rename temp file fail."); } }else { //磁盤上原本不存在索引,直接將內存索引寫入磁盤
//代碼略
}

  代碼的主要思想是維持3個指針:內存索引是一個有序TreeMap,iterator相當於一個虛擬指針;磁盤索引也是有序的,BufferdReader相當於一個虛擬指針;合並后生成的新索引使用BufferdWriter作為指針。然后使用歸並排序的思想,比較內存索引詞和磁盤索引詞的大小,哪個小就將哪個寫入新的索引然后將其指針前進一位,如果兩個索引詞相等,則合並兩者的文檔列表。

  從上面的描述來看以上代碼使用的內存應該是O(1)的,因為內存中除了內存索引之外,同一時刻只會從磁盤索引讀出一行,但是實際運行的時候,總是在合並時報出GC overhead limit exceeded,這個異常就是說jvm用了大量時間(超過98%)執行GC但是只釋放了很少的堆內存(小於2%),換句話說就是OOM的前兆。根據我對程序的內存占用的分析,這種情況是不正常的。

  於是給程序添加  “-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=C:/dump” 參數,在程序出現內存溢出異常的時候dump出內存信息。

  用eclipse MAT查看占用內存最多的對象:

發現是TreeMap的實例,在這個程序中我用了TreeMap來保存內存索引,而在合並索引的過程中,內存索引的大小應該是不變的,那么為什么會溢出呢?

  通過看代碼將問題定位在了紅色代碼處:

這里首先將ramDocIds指向entry.getValue(),然后又將union指向ramDocIds,此時union實際上是直接指向entry.getValue()的,然后union執行了addAll操作。。眾所周知,addAll操作是將元素加到對象本身的,這里我的原意是聲明一個局部變量保存列表合並結果然后存入新的索引文件,但是無意中卻同時修改了內存索引導致內存索引越來越大。

 

OK,到這里問題就解決了,只需要將

TreeSet<Long> union=ramDocIds;

改為

TreeSet<Long> union=new TreeSet<>(ramDocIds); 

即可。

 

回顧一下這次解決內存溢出問題的過程:  

  1. 分析程序空間復雜度,看內存溢出的是否正常。
  2. 添加jvm參數,讓程序在內存溢出的時候dump出內存快照。
  3. 使用eclipse MAT分析占用內存最多的對象。
  4. 在源碼中找到相應的對象查找問題。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM