參考:http://oldblog.csdn.net/column/details/16384.html
套用《圍城》中的一句話,“牆外面的人想進去,牆裡面的人想出來”,用此來形容Java與C++之間這堵記憶體動態分配和垃圾收集技術所圍成的“圍牆”就再合適不過了。
對於從事C、C++的開發人員而言,在記憶體管理領域,他們具有絕對的“權利”——擁有每個對象的控制權,並擔負著每個對象生命週期的維護責任。而對於Java開發人員而言,在虛擬機器自動記憶體管理機制的協助下,無需為每一個建立new操作去配對 delete/free 代碼,減少記憶體流失和記憶體溢出的問題,這些都交給了Java虛擬機器去進行記憶體控制,但是正因如此,當出現相關問題時,若不瞭解JVM使用記憶體規則,就難以排查錯誤。接下來以此篇文章記錄學習Java虛擬機器記憶體各個地區概念、作用、服務物件以及可能產生的問題。
此篇將記錄學習以下知識點: Java虛擬機器記憶體劃分 各地區知識理論學習 各地區會產生的異常 代碼實踐哪些操作會產生記憶體溢出異常 各地區出現異常的原因
一. 運行時地區記憶體
Java虛擬機器在執行Java程式時會將其管理的地區劃分為不同的資料區域,不同地區之間擁有各自用途、建立和銷毀的時間,有的地區隨著虛擬機器進程的啟動而存在,而有的地區則依賴使用者線程的生命週期。Java虛擬機器所管理的內容分為以下幾塊地區:
1 . 程式計數器
(1)含義作用
程式計數器(Program Counter Register)是一塊較小的記憶體空間,可以看作是當前線程所執行的位元組碼的行號指標。在虛擬機器概念性模型中,位元組碼解譯器工作時就是通過改變計數器的值來選取下一條需要執行的位元組碼指令,分支、迴圈、跳轉、異常處理、線程恢複等基礎功能都需要依賴計數器。
(2)計數器與多線程
由於JVM的多線程時通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個核心)都只會執行一條線程中的指令。所以,為了線程切換後能恢複到正確的執行位置,每條線程需要一個獨立的程式計數器,各線程之間計數器互不影響、隔離儲存區 (Isolated Storage),相當於是一塊“線程私人”的記憶體。
(3)虛擬機器規範記錄(有關異常)
若線程正在執行的是一個Java方法,這個計數器記錄的時正在執行的虛擬機器位元組碼指令的地址;若執行的是Native方法,則計數器為空白(Undefined)。注意:此記憶體地區是唯一一個在Java虛擬機器規範中沒有規定任何 OutOfMemoryError情況的地區。 2 . Java虛擬機器棧
(1)含義作用
同程式計數器相同,Java虛擬機器棧(Java Virtual Machine Stacks)也是線程私人的,它的生命週期與線程相同。虛擬機器棧描述的是Java方法執行的記憶體模型:每個方法在執行的同時都會建立一個棧幀用於儲存局部變數表、運算元棧、動態連結、方法出口等資訊。每一個方法從調用直至執行完成的過程,會對應一個棧幀在虛擬機器棧中入棧到出棧的過程。
(2)Java記憶體區分誤區
大多數人以為Java記憶體區分為堆記憶體(Heap)和棧記憶體(Stack),這是一種誤區,Java記憶體地區的劃分遠比這種粗糙的分法更加複雜。這種劃分方式廣泛流傳是由於大多數開發人員關注與對象記憶體配置關係最密切的記憶體地區就是這兩塊,有關“堆”的知識後續載提,這裡的“棧”指的就是虛擬機器棧,或者說是虛擬機器棧中的變數表部分。
(3)虛擬機器棧中的局部變數表
局部變數表中存放了編譯期可知的 八大資料類型(boolean、byte、char、short、int、float、long、double)。 對象引用(reference類型,它不等於對象本身,可能是一個指向對象起始地址的指標,也可能是指向一個代表對象的控制代碼或其他與此對象相關的位置) returnAddress類型(指向了一條位元組碼指令的地址)
其中64位長度的long和double類型的資料會佔用2個局部變數空間(Slot),其餘資料類型只佔用1個。局部變數表所需的記憶體控制項在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的局部變數空間是完全確定的,在方法運行期間不會改變局部變數表的大小。
(4)虛擬機器規範記錄(有關異常)
在Java虛擬機器規範中,對這個地區規定了兩種異常狀況: 若線程請求的棧深度大於虛擬機器所允許的深度,將拋出StackOverflowError異常。 若虛擬機器可以動態擴充(當前大部分Java虛擬機器都可動態擴充,只不過Java虛擬機器規範也允許固定長度的虛擬機器棧),當擴充時無法申請到足夠的記憶體,就會拋出OutOfMemoryError異常。 3 . 本地方法棧
(1)含義作用
本地方法棧(Native Method Stack)與虛擬機器棧所發揮的作用類似,它們之間的區別是:虛擬機器棧為虛擬機器執行Java方法(也就是位元組碼)服務,而本地方法棧則為虛擬機器使用到的Native方法服務。
(2)虛擬機器規範記錄(有關異常)
在虛擬機器規範中對本地方法棧中使用的語言、方式和資料結構並無強制規定,因此具體的虛擬機器可實現它。甚至有的虛擬機器(Sun HotSpot虛擬機器)直接把本地方法棧和虛擬機器棧合二為一。
與虛擬機器一樣,本地方法棧會拋出StackOverflowError和OutOfMemoryError異常。 4 . Java堆
(1)含義作用
對於大多數應用而言,Java堆(Heap)是Java虛擬機器所管理的記憶體中最大的一塊,它是被所有線程共用的一塊記憶體地區,在虛擬機器啟動時建立。此記憶體地區唯一的目的是存放對象執行個體,幾乎所有的對象執行個體都在這裡分配記憶體。Java虛擬機器規範中描述道:所有的對象執行個體以及數組都要在堆上分配,但是隨著JIT編譯器的發展和逃逸分析技術逐漸成熟,棧上分配、標量替換最佳化技術將會導致一些微妙的變化發生,所有的對象都在堆上分配的定論也並不“絕對”了。
(2)Java堆與記憶體回收行程
Java堆是記憶體回收行程管理的主要區域,因此被稱為“GC堆”(Garbage Collected Heap)。 從記憶體回收角度看,由於目前收集器基本採用分代收集演算法,所以Java堆可細分為:新生代和老年代。 從記憶體配置角度來看,線程共用的Java堆中可能劃分出多個線程私人的分配緩衝區(TLAB:Thread Local Allocation Buffer)。
不過無論如何劃分,都與存放內容無關,無論哪個地區,存放的都是對象執行個體,進一步劃分目的是為了更好地回收記憶體,或者是更快地分配記憶體。此節僅對記憶體地區作用進行學習,Java堆上各個地區分配回收等細節與記憶體配置策略有關,後續講解。
(3)虛擬機器規範記錄(有關異常)
根據Java虛擬機器規範的規定,Java堆可以處於物理上不連續的記憶體中,只要邏輯上是連續的即可,就像磁碟空間。在實現時,可以實現成固定大小或可擴充的,不過當前主流虛擬機器是按照可擴充進行實現的(通過-Xmx和 -Xms控制)。
若堆中沒有記憶體完成執行個體分配,並且堆也無法擴充時,將會拋出OutOfMemoryError異常。 5 . 方法區
(1)含義作用
方法區(Method Area)與Java堆一樣,是各個線程共用的記憶體地區,它用於儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的代碼等資料。雖然Java虛擬機器規範把方法區描述為堆的一個邏輯部分,但是它有一個別名叫做 Non-Heap(非堆),目的是為了和Java堆區分開來。
(2)虛擬機器規範記錄(有關異常)
Java虛擬機器規範對方法區的限制非常寬鬆,除了和Java堆一樣不需要連續的記憶體和可以選擇固定大小或可擴充外,還可以選擇不實現垃圾收集。相對而言,垃圾收集行為在這個地區比較少見。此地區的記憶體回收目標主要是針對常量池的回收和對類型的卸載,一般來說,回收效果難以令人滿意,尤其是類型的卸載,條件相對苛刻,但是這部分地區回收是有必要的。
根據Java虛擬機器規範的規定,當方法無法滿足記憶體需求時,將會拋出OutOfMemoryError異常。 6 . 運行時常量池
(1)含義作用
運行時常量池(Runtime Constant Pool)是方法區的一部分。Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有一項資訊是常量池(Constant Pool Table),用於存放編譯期產生的各種字面量和符號引用,這部分內容將在類載入後進入方法區的運行時常量池存放。
(2)運行時常量池和Class檔案
Java虛擬機器對Class檔案每一部分(自然包括常量池)的格式有嚴格規定,每一個位元組用於儲存那種資料都必須符合規範上的要求才會被虛擬機器認可、裝載和執行。但對於運行時常量池,Java虛擬機器規範沒有做任何有關細節的要求,不同的供應商實現的虛擬機器可以按照自己的需求來實現此記憶體地區。不過一般而言,除了儲存Class檔案中的描述符號引用外,還會把翻譯出的直接引用也儲存在運行時常量池中。
運行時常量池相對於Class檔案常量池的另外一個重要特徵是具備動態性,Java語言並不要求常量一定只有編譯器才能產生,也就是並非置入Class檔案中的常量池的內容才能進入方法區運行時常量池,運行期間也可能將新的常量放入池中,此特性被開發人員利用得比較多的便是String類的intern() 方法。
(3)虛擬機器規範記錄(有關異常)
運行時常量池是方法區的一部分,自然受到方法區的記憶體限制,當常量池無法再申請到記憶體時會拋出OutOfMemoryError異常。 7 . 直接記憶體
(1)含義作用
直接記憶體(Direct Memory)並不是虛擬機器運行時資料的一部分,也不是Java虛擬機器規範中定義的記憶體地區。但這部分記憶體也被頻繁運用,而卻可能導致OutOfMemoryError異常出現。
(3)有關異常
本機直接記憶體的分配不會受到Java堆大小的限制,但是既然是記憶體,還是會受到本機總記憶體(包括RAM以及SWAP區或分頁檔案)大小以及處理器定址空間的限制。伺服器管理員在配置虛擬機器參數時,會根據實際記憶體設定-Xmx等參數資訊,但經常忽略直接記憶體,使得各個記憶體地區總和大於實體記憶體限制(包括物理的和作業系統的限制),從而導致動態擴充時出現OutOfMemoryError異常。 二. 實踐:重現OutOfMemoryError異常
在Java虛擬機器規範中,除了程式計數器外,虛擬機器記憶體的其它地區都有發生OOM異常的可能,本節內容的目的為: 通過代碼驗證Java虛擬機器規範中描述的各個運行時地區儲存的內容。 開發人員在遇到實際的記憶體溢出時,能根據異常資訊判斷出是那個地區,定位到錯誤原因。 1 . Java堆溢出
Java堆用於儲存物件執行個體,只要不斷地建立對象,並且保證GC Roots到對象之間有可達路徑來避免記憶體回收機制清除這些對象,那麼在對象數量到達最大堆的容量限制後就會產生記憶體溢出異常。
【Java堆記憶體溢出異常測試】/** * VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError */public class HeapOOM { static class OOMObject { } public static void main(String[] args) { List<OOMObject> list = new ArrayList<OOMObject>(); while (true) { list.add(new OOMObject()); } }} 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
運行結果:
java.lang.OutOfMemoryError: Java heap space Dumping heap to java_pid1820.hprof ... Heap dump file created [24787111 bytes in 0.346 secs] Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
1 2 3 4 5
結果分析:
Java堆記憶體的OOM異常是實際應用中常見的記憶體溢出異常情況。當出現異常時,堆棧訊息“java.lang.OutOfMemoryError”會進一步提示“Java heap space”,而問題原因很明顯,對象數量過多,到達最大堆的容量限制。 2 . 虛擬機器棧和本地方法棧溢出
在JAVA虛擬機器規範描述了兩種異常: 如果線程請求的棧深度大於虛擬機器所允許的最大深度,將拋出StackOverflowError 如果虛擬機器在擴充棧時無法申請到足夠的記憶體空間,將拋出OutOfMemoryError。
以上兩種異常實質上存在著重疊的部分:當棧空間無法繼續分配時,是記憶體太小還是已使用的棧空間過大,其本質只是針對同一件事情的兩種描述。
【虛擬機器棧和本地方法棧OOM測試(僅作第一點測試)】/** * 如果線程請求的棧深度大於虛擬機器所允許的最大深度,將拋出StackOverflowError * 如果虛擬機器在擴充棧時無法申請到足夠的記憶體空間,將拋出OutOfMemoryError * VM Args:-Xss128k */ public class JavaVMStackSOF { private int stackLength = 1; public void stackLeak() { stackLength++; stackLeak(); } public static void main(String[] args) throws Throwable { JavaVMStackSOF oom = new JavaVMStackSOF(); try { oom.stackLeak(); } catch (Throwable e) { System.out.println("stack length:" + oom.stackLength); throw e; } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
運行結果:
stack length:2403 Exception in thread "main" java