如何編碼實現記憶體溢位

oschina發表於2013-04-26

  這將會是一篇比較邪惡的文章,當你想在某個人的生活中製造悲劇時你可能會去google搜尋它。在Java的世界裡,記憶體溢位僅僅只是你在這種情況下可能會引入的一種bug。你的受害者會在辦公室裡度過幾天甚至是幾周的不眠之夜。

  在這篇文章中我將會介紹兩種溢位方式,它們都是比較容易理解和重現的。並且它們都是來源現實專案的案例研究,但是為了讓你清晰地掌握,我把它們簡化了。

  不過放心,在我們遇到和解決了很過溢位bug之後,類似的案例將會比你想象得更加普遍。

  先來一個進入狀態的,在使用HashSet/HashMap時,所用鍵值沒有或者其equals()/hashCode()方法不正確,這會導致一個臭名昭著的錯誤。

class KeylessEntry {
 
   static class Key {
      Integer id;
 
      Key(Integer id) {
         this.id = id;
      }
 
      @Override
      public int hashCode() {
         return id.hashCode();
      }
   }
 
   public static void main(String[] args) {
      Map m = new HashMap();
      while (true)
         for (int i = 0; i < 10000; i++)
            if (!m.containsKey(i))
               m.put(new Key(i), "Number:" + i);
   }
}

當你執行上面的程式碼時,你可能會期望它執行起來永遠不會出問題,畢竟內建的快取方案只會增加到10,000個元素,然後就不會再增加了,所有的key都已經出現在 HashMap中。然而,事情並非如此。元素將會一直增長, 因為Key這個類沒有在hashCode()後實現一個合適的equals()方法。

解決方法很簡單,只要和下面的示例一樣新增一個equals方法就可以了。但是在找到問題所在之前,你肯定已經花費了不少寶貴的腦細胞。

@Override
public boolean equals(Object o) {
   boolean response = false;
   if (o instanceof Key) {
      response = (((Key)o).id).equals(this.id);
   }
   return response;
}

下一個你得提醒朋友的是和String處理相關的操作。它的表現會很詭異,特別是結合JVM版本差異的時候。String的內部工作機制在 JDK 7u6中被改變了,所以如果你發現產品環境只是小版本號的區別,那麼你已經準備好條件了。把類似下面的程式碼給你的朋友除錯,然後問他為什麼這個bug只會在產品中出現。

class Stringer {
   static final int MB = 1024*512;
 
   static String createLongString(int length){
      StringBuilder sb = new StringBuilder(length);
      for(int i=0; i < length; i++)
         sb.append('a');
      sb.append(System.nanoTime());
      return sb.toString();
   }
 
   public static void main(String[] args){
      List substrings = new ArrayList();
      for(int i=0; i< 100; i++){
         String longStr = createLongString(MB);
         String subStr = longStr.substring(1,10);
         substrings.add(subStr);
      }
   }
}

  上面的程式碼出了什麼問題呢?當它在JDK 7u6之前的版本上執行的時候,返回的字串將會儲存一個對那個1M左右大小的字串的引用,如果你執行的時候設定為-Xmx100m,你會得到一個意想不到的oom錯誤。結合你實驗環境中平臺和版本的差異,傷腦經的事情就產生了。

  現在如果你想掩蓋你的足跡,我們可以引進一些更加高階的概念。比如

  • 在不同的類載入器中載入有破壞性的程式碼,在載入的類被原始類載入器刪除後保持對它的引用,可以模擬一個類載入器溢位
  • 把攻擊性的程式碼隱藏在finalize方法中,使得程式表現變得不可預測
  • 在一個長期執行的執行緒中加入棘手的組合,它可能在ThreadLocals中儲存了一些可以被執行緒池訪問的東西,以便管理應用執行緒。

  我希望我們給了你一些思考的原材料以及當你想修理某人時的一些素材。這將帶來無窮無盡的除錯。除非你的朋友使用 Plumbr來查詢溢位的所在地。

  除了比較直白的營銷之外,我希望我們已經證明了在Java中製造記憶體溢位是很容易的事情。相信大多數人都經歷過解決它的困難性。所以如果你喜歡這篇文章,可以訂閱我們的twitter,我們會發出我們之後對JVM效能調節的相關內容。

  英文原文:How to create a memory leak

相關文章