自恢復JVM

jopen 9年前發布 | 13K 次閱讀 JVM Java開發

bd255d41018a00afa720f143c5a00c1c.jpg

我們有一個應用程序,它能夠正確地恢復自己,而不是需要關閉并重啟。剛開始時它出現錯誤,但是一段時間后開始平穩運行。為了給出一個這種應用程序的例子,我們從Heinz Kabutz的Java簡訊上五年前的一篇文章中獲取靈感,盡可能簡單地重新構造出了如下的實例:

package eu.plumbr.test;
    package eu.plumbr.test;

    public class HealMe {
      private static final int SIZE = (int) (Runtime.getRuntime().maxMemory() * 0.6);

      public static void main(String[] args) throws Exception {
        for (int i = 0; i < 1000; i++) {
          allocateMemory(i);
        }
      }

      private static void allocateMemory(int i) {
        try {
          {
            byte[] bytes = new byte[SIZE];
            System.out.println(bytes.length);
          }

          byte[] moreBytes = new byte[SIZE];
          System.out.println(moreBytes.length);

          System.out.println("I allocated memory successfully " + i);

        } catch (OutOfMemoryError e) {
          System.out.println("I failed to allocate memory " + i);
        }
      }
    }

上面的代碼在每個循環中申請兩大塊內存,每次申請的內存大小是整個可用堆空間的60%。由于這兩次內存申請是順序出現在同一個方法中,因此你可能會想,這 段代碼會一直拋出java.lang.OutOfMemoryError: Java heap space錯誤,而且永遠不會成功地執行allocateMemory()方法。

那么,讓我們通過靜態分析源代碼,來看看我們的預期是否正確:

  1. 經過快速初步檢查,這段代碼確實不夠完善,因為它嘗試申請的內存超過了JVM中可用的數量。
  2. 如果我們仔細的檢查就會注意到,第一次內存申請是在一個代碼塊中,它意味著這個代碼塊中定義的變量只在塊中是可見的。這就表明,變量bytes在代碼塊結束時應該會被GC回收掉。所以,我們的代碼實際上從一開始就能夠正確運行,因為當嘗試申請moreBytes內存時,前面申請的內存bytes應該已經被銷毀了。
  3. 如果我們查看編譯后的class文件,將看到下面的字節碼:
private static void allocateMemory(int);
    Code:
       0: getstatic     #3                  // Field SIZE:I
       3: newarray       byte
       5: astore_1      
       6: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
       9: aload_1       
      10: arraylength   
      11: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
      14: getstatic     #3                  // Field SIZE:I
      17: newarray       byte
      19: astore_1      
      20: getstatic     #4                  // Field java/lang/System.out:Ljava/io/PrintStream;
      23: aload_1       
      24: arraylength   
      25: invokevirtual #5                  // Method java/io/PrintStream.println:(I)V
---- cut for brevity ----

這里我們看到,在偏移地址3和5,申請了第一個數組并存放序號為1的局部變量。然后在偏移地址17,開始申請另一個數組。但是第一個數組仍然被局部變量引用,因此第二次內存申請應該會因為內存溢出而一直失敗。字節碼解釋器不能簡單的讓GC清除第一個數組,因為它仍然被引用。

我們的靜態代碼分析顯示,由于兩種潛在的原因上面的代碼不應該成功運行;而在一種情況下,它應該成功運行。這3種情形中的哪一種是正確的呢?讓我們運行代碼來看看。結果表明,兩種分析結果都是正確的。首先,應用程序申請內存失敗了;但是過了一段時間后(在我的Mac OS X系統上、Java 8環境下出現在第255次),內存申請開始成功:

java -Xmx2g eu.plumbr.test.HealMe
1145359564
I failed to allocate memory 0
1145359564
I failed to allocate memory 1

… cut for brevity ...

I failed to allocate memory 254
1145359564
I failed to allocate memory 255
1145359564
1145359564
I allocated memory successfully 256
1145359564
1145359564
I allocated memory successfully 257
1145359564
1145359564
Self-healing code is a reality! Skynet is near...

為了理解實際上發生了什么,我們需要仔細思考:在程序執行過程中,什么變化了?當然,顯而易見的發生了即時編譯。如果你回想一下,即時編譯是用來優化代碼熱點的一個JVM內置機制。JIT監視正在運行的代碼,當監測到一個熱點時,JIT將字節碼編譯成本地代碼,并應用諸如內聯方法與死代碼消除等不同優化。

讓我們打開下面的命令行選項并重新運行程序,來檢查是否是這種可能:

 -XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation

它將產生一個log文件,在我們的例子中是hotspot_pid38139.log。其中38139是Java進程的PID。在這個文件中,能夠看到下面的一行:

<task_queued compile_id='94' method='HealMe allocateMemory (I)V' bytes='83' count='256' iicount='256' level='3' stamp='112.305' comment='tiered' hot_count='256'/>

這行內容說明,在執行allocateMemory()方法256次之后,C1編譯器決定將這個方法放到C1第3級別編譯的隊列中。你可以在這里了解更多編譯級別和不同閥值的信息。所以,前256次循環中是運行在解釋器模式,即一個簡單的基于棧的字節碼解釋器。它不能預先知道哪些變量是否會在將來被用到,比如我們的例子中的bytes。但是JIT一次檢查整個方法,并推斷出變量bytes不再被使用,能夠被GC回收。因此垃圾回收終于出現,我們的程序神奇地自我恢復了。現在,我只希望讀者們沒有人正在實際的產品中調試類似的問題。但是如果你希望讓某個人悲劇一次,把類似的代碼加入到產品中將會是一個“靠譜的”方法。

原文鏈接: javacodegeeks 翻譯: ImportNew.com - shenggordon
譯文鏈接: http://www.importnew.com/14137.html

 本文由用戶 jopen 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!