第2章—Java內存區域與內存溢出異常

2.1 概述

總結:本章將從概念上介紹 Java 虛擬機內存的各個區域,講解這些區域的做用、服務對象以及其中可能產生的問題。java

2.2 運行時數據區域

Java 虛擬機在執行 Java 程序的過程當中會把它所管理的內存劃分爲若干個不一樣的數據區域,這些區域都有各自的用途以及建立和銷燬時間程序員

2.2.1 程序計數器(線程私有)

因爲Java虛擬機的多線程是經過線程輪流切換並分配處理器執行時間的方式來實現的,在任意一個肯定的時刻,一個處理器都只會執行一條線程中的指令。所以,爲了線程切換後能回覆到正確的執行位置,每條線程都須要有一個獨立的程序計數器。算法

若是線程正在執行的是一個 Java 方法,這個計數器記錄的是正在執行的虛擬機字節碼指令的地址; 若是線程正在執行的是 Native 方法,這個計數器值則爲空。數組

可能出現的異常:此內存區域是惟一一個在 Java 虛擬機規範中沒有規定任何 OutOfMemoryError 狀況的區域安全

2.2.2 Java 虛擬機棧(線程私有)

Java 虛擬機棧描述的是Java方法執行的內存模型,每一個方法在執行的同時都會建立一個棧幀用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。服務器

局部變量表存放了編譯期可知的各類數據類型、對象引用和 returnAddress 類型,且它所需的內存空間在編譯期間完成分配,在方法運行期間不會改變局部變量表的大小。數據結構

可能出現的異常:1.線程請求的棧深度大於虛擬機所容許的深度,StackOverflowError多線程

        2.若是虛擬機棧能夠動態擴展,但擴展時沒法申請到足夠的內存,OutOfMemoryError函數

2.2.3 本地方法棧(線程私有)

本地方法棧與虛擬機棧的做用相似,區別是虛擬機棧爲虛擬機執行 Java 方法服務,本地方法棧則爲虛擬機執行Native方法服務。Sun Hotspot 直接將本地方法棧和虛擬機棧合二爲一。工具

可能出現的異常:StackOverflowError、OutOfMemoryError

2.2.4 Java 堆(線程共享)

全部對象實例以及數組都要在堆上分配,Java 堆在虛擬機啓動時建立,此內存的惟一目的就是存放對象實例,Java 堆能夠處於物理上不連續的內存空間中,只要邏輯上是連續的便可。

可能出現的異常:當在堆中沒有內存能夠完成實例分配,而且堆也沒法再擴展時,OutOfMemoryError。

2.2.5 方法區(線程共享)

方法區用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據,這區域的內存回收目標主要是針對常量池的回收和對類型的卸載。

可能出現的異常:當方法區沒法知足內存分配需求時,OutOfMemoryError

2.2.6 運行時常量池

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

通常來講,除了保存Class文件中描述的符號引用外,還會把翻譯出來的直接引用也存儲在運行時常量池中。

運行時常量池相對於Class文件常量池的另一個重要特徵是具有動態性,運行期間也可能將新的常量放入池中(如 String 類的 intern() 方法)

可能出現的異常:當常量池沒法再申請到內存時會拋出OutOfMemoryError

2.2.7 直接內存

直接內存並非虛擬機運行時數據區的一部分,也不是虛擬機規範中定義的內存區域。Jdk1.4中新加入了NIO類,引入了一種基於通道與緩衝區的I/O方式,它可使用 Native 函數庫直接分配對外內存,而後經過一個存儲在 Java 堆中的DirectByteBuffer對象做爲這塊內存的引用進行操做,避免了在Java堆和Native堆中來回複製數據,在一些場景中顯著提升性能。

直接內存的分配不會受到Java堆大小的限制,可是,既然是內存,確定會受到本機總內存大小和處理器尋址空間的限制。

服務器管理員在配置虛擬機參數時,會根據實際內存設置 -Xmx 等參數信息,但常常忽略直接內存,使得各個內存區域總和大於物理內存限制,從而致使動態擴展時出現OutOfMemoryError異常

2.3 HotSpot 虛擬機對象探祕

根據上述知識,咱們已經瞭解到虛擬機內存劃分爲哪幾塊以及這些塊中都分別存放哪些內容,再來了解虛擬機內存中數據的其餘細節,例如它們是如何建立、如何佈局以及如何訪問的,下面將深刻探討 HotSpot 虛擬機在 Java 堆中對象建立、佈局和訪問的全過程。

2.3.1 對象的建立

在語言層面上,建立對象(例如克隆和反序列化)一般僅僅是一個new 關鍵字而已,而在虛擬機中,對象(普通對象,不包括數組和Class對象等)的建立又是怎樣的一個過程呢?

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

2.在類加載檢查經過後,接下來虛擬機將爲新生對象分配內存。對象所需的內存大小在類加載完成後即可徹底肯定,爲對象分配空間的任務等同於把一塊肯定大小的內存從Java堆中劃分出來(兩種方式:1.指針碰撞; 2.空閒列表),選擇哪一種分配方式由Java堆是否規整決定,而Java堆是否規整又由所採用的垃圾收集器是否帶有壓縮整理功能決定,所以,使用Serial、ParNew等待Compact 過程的收集器時系統採用指針碰撞分配算法,而使用CMS 這種基於Mark-Sweep算法的收集器時,一般使用空閒列表。爲確保內存分配時的線程安全,一般使用兩種解決方案:一種是對分配內存空間的動做進行同步處理——實際上虛擬機採用CAS 配上失敗重試的方式保證更新操做的原子性;另外一種是把內存分配的動做按照線程劃分在不一樣的空間之中,即每一個線程在Java 堆中預先分配一小塊內存,稱爲本地線程分配緩衝(TLAB),哪一個線程要分配內存,就在哪一個線程的TLAB上分配,只有TLAB用完並分配新的TLAB時,才須要同步鎖定。

3.內存分配完成後,虛擬機須要將分配到的內存空間都初始化爲零值,若是使用TLAB,這一工做能夠提早至TLAB分配時進行。這一步操做保證了對象的實例字段在Java 代碼中能夠不賦初始值就直接使用,程序能訪問到這些字段的數據類型所對應的零值。

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

5.在上面工做都完成以後,從虛擬機的角度來看,一個新對象已經產生,但從Java程序的角度來看,對象的建立纔剛剛開始。通常來講,執行new 指令以後會接着執行<init>方法,把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算徹底產生出來。

2.3.2 對象的內存佈局

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

1.HotSpot 虛擬機的對象頭包括兩部分信息:

第一部分用於存儲對象自身的運行時數據(如哈希碼、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等),官方稱爲「Mark Word」。Mark Word被設計成一個非固定的數據結構以便在極小的空間內存儲儘可能多的信息,它會根據對象的狀態複用本身的存儲空間

第二部分是類型指針(並非全部虛擬機都有),即對象指向它的類元數據的指針,虛擬機經過這個指針來肯定這個對象是哪一個類的實例(句柄和直接指針)。另外,若是對象是一個Java 數組,那在對象頭中還必須有一塊用於記錄數組長度的數據,由於虛擬機能夠經過普通Java 對象的元數據信息肯定Java 對象的大小,可是從數組的元數據中卻沒法肯定數組的大小。

2.實例數據部分是對象真正存儲的有效信息,也是在程序代碼中定義的各類類型的字段內容。不管是從父類繼承下來的,仍是在子類中定義的,都須要記錄下來。這部分的存儲順序會受到虛擬機分配策略參數和字段在Java源碼中定義的順序的影響。HotSpot 虛擬機默認的分配策略是相同寬度的字段老是被分配到一塊兒,知足這個前提條件下,在父類中定義的變量會出如今子類以前。

3.對齊填充部分並非必然存在的,它僅僅起着佔位符的做用。因爲HotSpot 虛擬機的自動內存管理系統要求對象起始地址必須是8字節的整數倍,所以,當對象實例數據部分沒有對齊時,就須要經過對齊填充來補全。

2.3.3 對象的訪問定位

創建對象是爲了使用對象,Java 程序須要經過棧上的 reference 數據來操做堆上的具體對象。目前主流的訪問方式有使用句柄和直接指針兩種。

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

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

這兩種對象訪問方式各有優點,使用句柄來訪問的最大好處就是 reference 中存儲的是穩定的句柄地址;使用直接指針訪問方式的最大好處就是速度更快,它節省了一次指針定位的時間開銷。

Sun HotSpot 使用的是第二種方式進行對象訪問。

2.4 實戰: OutOfMemoryError 異常

在Java 虛擬機規範的描述中,除了程序計數器外,虛擬機內存的其餘幾個運行時數據區都有發生OutOfMemoryError(OOM)異常的可能。

本節主要內容有兩個:

第一,經過代碼驗證Java 虛擬機規範中描述的各個運行時數據區存儲的內容;

第二,在遇到實際的內存溢出異常時,能根據異常的信息快速判斷是哪一個區域的內存溢出,知道什麼樣的代碼可能會致使這些區域內存溢出,以及出現這些異常後該如何處理。

2.4.1 Java 堆溢出(產生大量對象)

Java 堆用來存儲對象實例,只要不斷地建立對象,而且保證 GC Roots 到對象之間有可達路徑來避免垃圾回收機制清楚這些對象,那麼在對象數量達到最大堆的容量限制後就會產生內存溢出異常。

將堆的最小值 -Xms 參數與最大值 -Xmx參數設置爲同樣便可避免堆自動擴展。

Java 堆內存的 OOM 異常是實際應用中常見的內存溢出異常狀況。當出現 Java 堆內存溢出時,異常信息棧信息 「java.lang.OutOfMemoryError" 會跟着進一步提示 "Java heap space" 。

要解決這個區域的異常,通常的手段是先經過內存映像分析工具對Dump出來的堆轉儲快照進行分析,重點是確認內存中的對象是不是必要的,也就是分清楚到底出現了內存泄漏仍是內存溢出。若是是內存泄漏,可進一步經過工具查看泄漏對象到 GC Roots 的引用鏈;若是不存在泄漏,就是內存中的對象都必須存活着,那就應當檢查虛擬機參數與機器物理內存對比看是否能夠調大,從代碼上檢查是否存在某些對象生命週期過長、持有狀態時間過長的狀況,嘗試減小程序運行期的內存消耗。

2.4.2 虛擬機棧和本地方法棧溢出(遞歸方法)

因爲HotSpot 虛擬機並不區分虛擬機棧和本地方法棧,因此兩種狀況一併說明。

1.線程請求的棧深度大於虛擬機所容許的深度,StackOverflowError

2.若是虛擬機棧能夠動態擴展,但擴展時沒法申請到足夠的內存,OutOfMemoryError

結論:

在單線程狀況下,不管是因爲棧幀太大仍是虛擬機棧容量過小,當內存沒法分配的時候,虛擬機拋出的都是StackOverflowError異常

在多線程狀況下,每一個線程的棧分配的內存越大,越容易產生內存溢出異常

2.4.3 方法區和運行時常量池溢出(產生大量類)

方法區用於存放Class的相關信息,如類名、訪問修飾符、常量池、字段描述、方法描述等。對於這個區域的測試,基本思路是運行大量的類去填滿方法區,直到溢出。

方法區溢出也是一種常見的內存溢出異常,當出現 Java 方法區內存溢出時,異常信息棧信息 「java.lang.OutOfMemoryError" 會跟着進一步提示 "PermGen space" ,一個類要被垃圾收集器回收掉,斷定條件是比較苛刻的。

2.4.4 本機直接內存溢出

DirectMemory容量可經過 -XX: MaxDirectMemorySize 指定,若是不指定,則默認與 Java 堆最大值同樣。

由 DirectMemory 致使的內存溢出的一個很明顯的特徵是在Heap Dump文件中不會看見明顯的異常,若是發現OOM後Dump文件很小,而程序中又直接或間接使用了NIO,那就能夠考慮是不是DirectMemory 致使。

2.5 本章總結

經過本章的學習,咱們明白了虛擬機中的內存是如何劃分的,哪部分區域、什麼樣的代碼和操做可能致使內存溢出異常。雖然Java 有垃圾收集機制,但內存溢出異常離咱們仍然並不遙遠,本章知識說明了各個區域出現內存溢出異常的緣由。

相關文章
相關標籤/搜索