程式扎記: [ JVM 應用 ] Java 記憶體區域的記憶體溢出異常

標籤

2012年6月27日 星期三

[ JVM 應用 ] Java 記憶體區域的記憶體溢出異常

概述 : 
對 Java 程式 Programmer 來說, 在 JVM 的自動記憶體管理機制下, 不需要像 C/C++ Programmer 需要對每一個 new 操作去寫配對的 delete/free 程式碼, 而且不容易出現記憶體洩漏與記憶體溢出的問題, 看起來由 JVM 管理記憶體管理記憶體可以省很多事, 不過也正是因為記憶體控制都是由像 Blackbox 的 JVM 管控, 一旦出現 Memory leak 或是 Out Of Memory Error 的問題, 如果不了解 JVM 如何使用記憶體, 那排除錯誤將成為一項困難的工作. 

執行時資料區域 : 
JVM 在執行 Java 程式過程中會把它所管理的記憶體劃分為若干個區域. 這些區域各有用途與各自建立與銷毀的時間. 底下為這些區域的示意圖 : 
 

底下針對這些區域進行簡單說明. 

- 程式計數器 
程式計數器 (Program Counter Register) 可以看成是當前執行序所執行的位元組編碼的行號指示器. JVM 就是透過它來選取下一個要執行的 byte code 指令. 由於 JVM 的多執行序是透過分配處理器的執行時間方式實現, 在同一時間, 一個處理器 (或一個核心) 只會執行一條指令. 因此為了執行序切換後能恢復到正確的執行位置, 每個執行序都有一個獨立的程式計數器, 互不影響並獨立儲存. 

- Java 虛擬器堆疊 
跟程式計數器一樣, JVM Stack 也是執行序私有的, 它的生命週期與執行序相同. JVM Stack 描述的是 Java 方法執行的記憶體模型 : 每個方法被執行的時候都會同時建立一個 Stack Frame 用於儲存區域變數表, 動態連結, 方法出口等資訊. 每一個方法被呼叫至執行完成過程, 就對應一個 Stack Frame 從入 Stack 到出 Stack 的過程. 

在 JVM 規範中, 對這個區域規定了兩種異常 : 如果執行序請求 Stack 深度大於 JVM 所允許的深度, 將拋出 StackOverflowError 異常 ; 如果 JVM Stack 可以動態延伸, 當延伸時無法申請到足夠記憶體時會拋出 OutOfMemoryError 異常. 

- 本地方法堆疊 
本地方法堆疊 (Native Method Stacks) 與 JVM Stack 的功用相似, 其區別是本地方法堆疊是 JVM 使用到 Native 方法而不是 Java 方法. 

- Java 堆 
對大多數應用來說, Java 堆 (Heap) 是 JVM 管理記憶體中最大的一塊. Java Heap 是被所有執行緒共用的一塊記憶體區域, 在 JVM 啟動時建立. 此記憶體區域的唯一目的就是存放物件實例, 幾乎所有的物件實例都在這裡分配記憶體. 根據 JVM 規範, Java Heap 可以處於物理上不連續的記憶體空間, 只要邏輯上是連續的即可. 另外 Java Heap 的大小可以是固定大小也可以透過 -Xmx 與 -Xms 控制可延伸的最大與最小的 size. 當記憶體不夠建立實例且也不能延伸時, 將會拋出 OutOfMemoryError 異常. 

- 方法區 
方法區 (Method Area) 和 Java Heap 一樣, 是各個執行緒共用的記憶體區域, 它用於儲存 JVM 載入的類別資訊, 常數, 靜態變數, 即時編譯器編譯後的程式碼等資料. 對於習慣在 HotSpot JVM 上開發與部署程式的開發者來說, 很多人把方法區稱為 "永久代" (Permanent Generation), 本質上兩者並不等價. 這個區域和 Java Heap 一樣可以選擇固定大小或者可延伸外, 還可以選擇在這個區域不實現 GC. 根據 JVM 規定, 當方法區無法滿足記憶體分配需求時, 將拋出 OutOfMemoryError 異常. 

- 執行時常數池 
執行時期常數池 (Runtime Constant Pool) 是方法區的一部分. Class 當中除了有類別版本, 欄位, 方法等描述資訊外, 還有一項資訊是存放在常數池的各種字面量和符號參考, 這部分將在類別載入後存放到方法區的執行時期常數池. 既然常數池是方法區的一部分, 自然會收到記憶體限制, 當常數池無法在申請到記憶體時會拋出 OutOfMemoryError 異常. 

- 直接記憶體 
直接記憶體 (Direct Memory) 並不是 JVM 執行時資料區的一部分, 也不是 JVM 規範中定義的記憶體區域, 但是這部分記憶體頻繁的地被使用, 且也可能導致 OutOfMemoryError 異常. 在 JDK 1.4 新加入 NIO(New Input/Output) 類別, 導入一種基於通道 (Channel) 與緩衝區 (Buffer) 的 I/O 方式, 它使用 Native 函式程式庫直接分配 Heap 外的記憶體, 然後透過一個儲存在 Java Heap 的 DirectByteBuffer 物件作為這塊記憶體的參考與進行操作. 這樣在一些 User scenario 能避免在 Java Heap 與 Native Heap 中來回複製資料而顯著提升性能. 

對象訪問 : 
物件訪問在 Java 語言中無處不在, 但即使是最簡單的訪問也會涉及 Java 堆疊, Java Heap 與方法區這三個最重要的記憶體區域之間的關聯關係, 如下面這句代碼 : 
  1. Object obj = new Object();  
假設這句代碼出現在方法中, "Object obj" 這部分的語義將會反映到 Java Stack 的本地變數表中, 最為一個 reference 類型資料出現. 而 "new Object()" 這部分語義將會反映到 Java Heap 中, 形成一塊儲存 Object 類型的實例資料結構. 另外在 Java Heap 中還必須包含能尋找到此物件類型資料 (如物件類型, 父類別, 實作介面 etc) 的地址資訊, 這些資料儲存在方法區中. 

由於 reference 類型在 JVM 規範中只規定了一個指向物件的參考, 並沒有定義這個參考應該透過何種方式去定位, 以及訪問到 Java Heap 中的物件具體位置, 因此不同虛擬機實現方法略有不同, 主流訪問方式有兩種, 說明如下 : 
- 使用控制碼訪問方式 
如果使用控制碼訪問方式, Java 將會劃出一塊記憶體來做為控制碼池, reference 中儲存的就是物件碼位址, 而控制碼中包含了物件實例資料和類別資訊各自的具體位址 : 
 

- 使用直接指標訪問 
如果使用直接指標訪問方式, Java Heap 物件的布局中就必須考慮如何放置訪問類型資訊的相關方法, 而好處是是速度快. reference 中直接儲存物件位址 : 
 

OutOfMemoryError 異常 : 
底下將透過程式碼示範如何造成 OutOfMemoryError 異常, 知道如何造成異常, 日後便能對該異常較不陌生. 而底下例子大多需要設定 JVM 參數來讓異常及早發生, 可以參考下圖進行設定 : 
 

- Java Heap 溢出 
Java Heap 用於儲存物件實例, 我們只要不斷的建立物件, 並且保證該物件都有被參考 (使用 List 來存放建立的物件.), 即可避免 GC 收集這些物件而造成記憶體溢出 : 
  1. package ch02;  
  2.   
  3. import java.util.ArrayList;  
  4. import java.util.List;  
  5.   
  6. public class Ex2_1 {  
  7.     static class OOMObject{}  
  8.   
  9.     /** 
  10.      * VM Args : -verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 
  11.      * Goal : Test Java Heap OOM. 
  12.      * @param args 
  13.      */  
  14.     public static void main(String[] args) {  
  15.         List list = new ArrayList();  
  16.         while(true)  
  17.         {  
  18.             list.add(new OOMObject());  
  19.         }  
  20.     }  
  21. }  
執行會出現異常如下 : 
 

Java Heap 記憶體 OOM (OutOfMemory Error) 是實際應用中最常見的記憶體異常狀況. 要解決這個異常, 一般的手法是透過記憶體映射分析工具 (如 Eclipse Memory Analyzer)對 dump 出來的 Heap 快照進行分析, 重點是確認記憶體中的物件是否必要的, 也就是先要分清楚是出現了 Memory leak 還是 Memory overflow! 

如果是 Memory leak 可能就不是我們可以處理的, 通常應該是 JVM 的 bug ; 如果不存在 Memory leak, 換句話說就是記憶體中的物件過度消耗記憶體, 那就可以考慮使用 -Xmx 來加大 JVM 的延伸記憶體使用量. 或者從程式碼著手讓不需要用到的物件盡可能進入 GC 收集的名單中, 即降低 Idle 的物件數. 

補充說明 : 
[ Java 常見問題 ] JDK 6.0 JVM最大記憶體設定
[ Java 常見問題 ] 'java.lang.OutOfMemoryError: GC overhead limit exceeded'

沒有留言:

張貼留言

網誌存檔

關於我自己

我的相片
Where there is a will, there is a way!