Java內存區域與內存溢出異常

一.運行時數據區域

1.程序計數器

程序計數器是一塊較小的內存空間,它可以看做是當前線程所 執行的字節碼的行號指示器。在虛擬機的概念模型裏,字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都需要以來這個急死乎其來完成。

用於Java虛擬機的多線程是通過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器都只會執行一條線程中的指令。因此,爲了線程切換後能恢復到正確的執行位置,每條線程都需要有一個獨立的程序計數器,各條線程之間計數器互不影響,獨立存儲,我們稱這類內存區域爲‘線程私有’的內存。

如果線程正在執行的是一個Java方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址;如果正在執行的是Native方法,這個計數器值則爲空。此內存區域是唯一一個在Java虛擬機範圍在沒有規定任何OutOfMemoryError情況的區域。

2.Java虛擬機棧

線程私有,生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀用於存儲局部變量表、操作數棧、動態鏈接、方法出口等消息。每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。

虛擬機棧存放着基本數據類型(boolean byte char short int long float double )、對象引用和returnAddress類型(指向一條字節碼指令的地址)。其中long跟double會佔用2個局部變量空間,其餘的佔一個。虛擬機棧所需的內存空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的局部變量空間是完全確定的,在方法運行期間不會改變虛擬機棧的大小。

在Java虛擬機規範中,對這個區域規定了兩種異常狀況:若線程請求棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常;若虛擬機棧擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError異常。

3.本地方法棧

本地方法棧與虛擬機棧所發揮的作用非常相似,它們之間的區別不過是虛擬機棧爲虛擬機執行Java方法(字節碼)服務,而本地方法棧則爲虛擬機使用到的Native方法服務。本地方法棧中方法使用語言、方式與數據結構沒有強制規定,它也會拋出StackOverflowError和OutOfMemoryError異常。

4.Java堆

對於大多數應用來說,Java堆(Java heap)是Java虛擬機所管理的內存中最大的一塊。Java堆是被所有線程共享的一塊內存區域,在虛擬機啓動的時候創建。此區域存在的唯一目的就是存放對象實例,幾乎所有對象實例都在這裏分配內存。

Java堆是垃圾收集器管理的主要區域,因此很多時候也被稱做“GC堆”。

根據Java虛擬機規範的規定,Java堆可以處於物理上不連續的內存空間中,只要邏輯上是連續的即可。在實現時,既可以實現成固定大小的,也可以是可擴展的,當前主流的虛擬機都是按照可擴展來實現的(通過-Xmx和-Xms控制)。如果在堆中沒有內存完成實例分配,並且堆也是無法再擴展時,將會拋出OutOfMemoryError異常。

5.方法區

方法區跟Java堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。Java虛擬機規範把方法區描述爲堆的一個邏輯部分,爲了與Java堆區分出來,別名爲Non-Heap(非堆)。

Java虛擬機規範對方法區的限制非常的寬鬆,除了和Java堆一樣不需要連續的內存和可以選擇固定大小或者可擴展外,還可以選擇不實現垃圾收集。這區域的內存回收目標主要是針對常量池的回收和對類型的卸載,一般來說,這個區域的回收“成績”比較難以令人滿意,尤其是類型的卸載,條件相對苛刻,但是這部分區域的回收確實是必要的。

根據Java虛擬機規範的規定,當方法區無法滿足內存分配需求時,將拋出OutOfMemoryError異常。

6.運行時常量池

運行時常量池是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用於存放編譯期生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放。

運行時常量池相對於Class文件常量池的另外一個重要特徵是具備動態性,Java語言並不要求常量一定只有編譯期才能產生,也就是並非預置入Class文件中常量池的內容才能進入方法運行時常量池,運行期間也可能將新的常量放入池中,這種特性被開發人員利用得比較多的就是Stirng類的intern()方法。

當常量池無法再申請到內存時會拋出OutOfMemoryError異常。

7.直接內存

直接內存並不是虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域。但是這部分內存也被頻繁地使用,而且也可能導致OutOfMemoryError異常。

在JDK1.4中新加入了NIO(New IO)類,引入了一種基於通道與緩存區的I/O方式,它可以使用Native函數庫直接分配堆外內存,然後通過一個存儲在Java堆中的DirectByteBuffer對象作爲這塊內存的引用進行操作。這樣能在一些場景中顯著提高性能,以爲避免了在Java堆和native堆中來回複製數據。

本機直接內存的分配不會受到Java堆大小的限制,但既然是內存,肯定會受到本機總內存大小以及處理器尋址空間的限制。服務器管理員在配置虛擬機參數時,會根據實際內存設置-Xmx等參數信息,但經常忽略直接內存,使得各個內存區域總和大於物理內存限制(包括物理的和操作系統級的限制),從而導致動態擴展時出現OutOfMemoryError異常。

二.HotSpot虛擬機對象探祕

以HotStop虛擬機和常用的內存區域Java堆爲例,深入探討HotStop虛擬機在Java堆中對象分配、佈局和訪問的全過程。

1.對象的創建

虛擬機遇到一條new指令時,首先將去檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。

在類加載檢查完成過後,接下來虛擬機將爲新生對象分配內存。對象所需內存的大小在類加載完成後便可完全確定,爲對象分配空間的任務等同於把一塊確定大小的內存從Java堆中劃分出來。分配方式根據Java堆是否規整,分爲“指針碰撞”和“空閒列表”兩類,選擇哪種分配方式由Java堆是否規整決定,而Java堆是否規整又有所採用的垃圾收集器是否帶有亞索調整功能決定。因此,使用Serial、ParNew等帶Compat過程的收集器時,系統採用的分配算法是指針碰撞;使用CMS這種基於Mark-Sweep算法的收集器時,通常採用空閒列表。

對象創建在虛擬機中是非常頻繁地行爲,在併發情況下並不是線程安全的。解決這個問題由兩種方案,一是對分配內存空間的動作進行同步處理—實際上虛擬機採用CAS配上失敗重試的方式保證更新操作的原子性;另一種是把內存分配的動作按照先生劃分在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊內存。

內存分配完成後,虛擬機需要將分配到的內存空間都初始化爲零值,保證對象的實例字段在Java代碼中可以不賦初始值就直接使用,程序能訪問到的這些字段的數據類型所對應的零值。

接下來,虛擬機要對對象進行必要的設置,例如這個對象是哪個類的實例、如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息,存放在對象的對象頭中。

執行完new指令之後會接着執行方法,把對象按照程序員的醫院進行初始化,這樣一個真正可用的對象纔算完全產生出來。

2.對象的內存佈局

在HotStop虛擬機中,對象在內存中存儲的佈局可以分爲3塊區域:對象頭、實例數據和對齊填充。

對象頭包括兩部分信息,第一部分用於存儲對象自身的運行時數據,這部分長度在32位和64位的虛擬機中分別爲32bit和64bit。另一部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。

示例數據部分是對象真正存儲的有效信息,也是在程序代碼中所定義的各種類型的字段內容。這部分的存儲順序會受到虛擬機分配策略參數和字段在Java源碼中定義順序的影響,相同寬度的字段總是被分配到一起。

對齊填充並不是必然存在的,在沒有特別的含義,它僅僅起到佔位符的作用。

3.對象的訪問定位

建立對象是爲了使用對象,我們的Java程序需要通過棧上的reference數據來操作堆上的具體對象。用於reference類型在Java虛擬機規範只規定了一個指向對象的引用,並沒有定義這個引用應該通過何種方式去定位、訪問堆中的對象的具體位置,所以對象訪問方式也是取決於虛擬機實際而定的。目前主流對的訪問方式有使用句柄和直接指針兩種。

如果使用句柄訪問的話,Java堆中將會劃分出一塊內存來作爲句柄池,reference中存儲的就是對象的句柄地址,而句柄中包含了對象實例數據與類型數據各自的具體地址信息。

如果使用直接指針訪問,那麼Java堆對象的佈局中就必須考慮如何爲防止訪問類型數據相關信息,而reference中存儲的直接就是對象地址。

發佈了43 篇原創文章 · 獲贊 4 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章