1. 程式人生 > >JVM 第2章 Java記憶體區域與記憶體溢位異常

JVM 第2章 Java記憶體區域與記憶體溢位異常

可以參考下,這個寫的簡練
https://blog.csdn.net/seu_calvin/article/details/51404589

1 概述

對於java程式設計師來說,在虛擬機器自動記憶體管理機制的幫助下,不需要為每一個new操作去寫配對的delete/free程式碼,不容易出現記憶體洩漏和記憶體溢位的問題。不過,一旦出現記憶體洩漏和記憶體溢位問題,如果不瞭解虛擬機器是怎麼使用記憶體的,那麼排查錯誤會很困難。

2 執行時資料區域

java虛擬機器在執行java程式的過程中會把記憶體劃分若干個不同的資料區域。有的區域隨著虛擬機器程序的啟動而存在,有的區域則依賴使用者執行緒的啟動和結束而建立和銷燬。java虛擬機器所管理的記憶體包括以下幾個執行時資料區域:…

2.1程式計數器

1、功能:是當前執行緒所執行的位元組碼的行號指示器。位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令。Java虛擬機器的多執行緒是通過執行緒輪流切換並分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個核心)都只會執行一條執行緒中的執行。為了執行緒切換後能夠恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,各執行緒之間的計數器互補影響,獨立儲存,稱這類記憶體區域為“執行緒私有”的記憶體。

2、存放內容:如果執行緒執行的是一個java方法,這個計數器記錄的是正在執行的虛擬機器位元組碼指令的地址;如果正在執行的是Native方法,這個計數器值為空。

3、佔用記憶體空間大小:在記憶體空間中佔用較小的區域。

4、是否執行緒私有:是。

5、異常:此區域是唯一一個在java虛擬機器規範中沒有規定任何OutOfMemoryError情況的區域。

2.2 java虛擬機器棧

1、定義:虛擬機器棧描述的是java方法執行的記憶體模型:每個方法在執行的同時都會建立一個棧幀用於儲存區域性表量表、運算元棧、動態連結、方法出口等資訊。每一個方法從呼叫直至執行完成的過程,就對應著一個棧幀在虛擬機器棧中入棧到出棧的過程。

2、存放內容:棧幀用於儲存區域性表量表、運算元棧、動態連結、方法出口等資訊。

3、佔用記憶體空間大小:在記憶體空間中佔用較小的區域。

4、是否執行緒私有:是。它的生命週期和執行緒相同。

5、異常:此區域包括兩種異常情況:a、如果執行緒請求的棧深度大於虛擬機器允許的棧深度,將丟擲StackOverflowError異常。b、如果虛擬機器棧可以動態擴充套件,如果擴充套件時無法申請到足夠的記憶體,就會丟擲OutOfMemeoryError異常。

6、經常有人把java記憶體分為堆記憶體和棧記憶體,這種分法比較粗糙,java記憶體區域的劃分遠比這複雜。這種劃分方法的流行智慧說明大多數程式設計師最關注的的、與物件記憶體分配關係最密切的記憶體區域是這兩塊。這裡的“棧”就是現在講的虛擬機器棧,或者說是虛擬機器棧中區域性變量表部分。

7、區域性變量表存放了編譯器可知的各種基本資料型別(boolean, byte, char, short, int, float, long, double)、物件引用型別(不等同於物件本身,可能是一個指向物件起始地址的引用指標)和returnAddress型別(指向了一條位元組碼指令的地址)。其中64位長度的long和double型別的資料會佔用2個區域性變數空間(slot),其餘型別只佔用1個。區域性變量表所需的記憶體空間在編譯期間完成分配,當進入一個方法時,這個方法需要在幀中分配多大的區域性變數空間是完全確定的,方法執行期間不會改變區域性變量表的大小。

2.3 本地方法棧

1、功能:本地方法棧和虛擬機器棧發揮的作用類似。二者之間的區別:虛擬機器棧為虛擬機器執行java方法(也就是位元組碼)服務,而本地方法棧則為虛擬機器使用的Native方法服務。

2、異常:同java虛擬機器棧。

2.4 java堆

1、定義:java堆是java虛擬機器所管理的記憶體中最大的一塊。java堆是被所有執行緒共享的記憶體區域,在虛擬機器開啟的時候建立。

2、功能:唯一作用就是存放物件例項(所有的例項物件和陣列)。java虛擬機器規範中的描述是:所有的物件例項和陣列都要在對上分配,但隨著JIT編譯器的發展,所有物件的分配都在堆上也沒有那麼絕對了。

3、也稱“GC堆”:java堆是垃圾收集器管理的主要區域。

4、分類:現在的收集器基本都採用分代收集演算法,所以java堆可被分為新生代和老年代;再細緻一點的有Eden空間、From Survivor空間、To Survivor空間等。從記憶體分配的角度,執行緒共享的java堆可能劃分出多個執行緒私有的分配緩衝區。無論如何劃分,儲存的依然是物件例項,進一步劃分的目的是為了更好地回收記憶體,或者更快的分配記憶體。

5、java堆可以處於物理上不連續的記憶體空間中,只要邏輯上連續即可。在實現時,既可以實現成固定大小的,也可以是可擴充套件的,當前的主流虛擬機器都是可擴充套件的。

6、異常:如果在堆中沒有記憶體完成例項分配時,並且堆也無法在擴充套件時,會丟擲OutOfMemoryError異常。

2.5 方法區

1、功能:方法區和java堆一樣,是各個執行緒共享的記憶體區域,它用於儲存已經被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。

2、雖然java虛擬機器規範把方法區描述為堆的一個邏輯部分,但它卻有一個別名叫做“Non-Heap(非堆)”,目的是和java堆區分開來。

3、“永久代”:很多人把方法區稱為“永久代”,但本質上二者不等價。這樣稱呼的原因是:HotSpot虛擬機器設計團隊把GC分代收集擴充套件至方法區,用永久代實現了方法區,以便垃圾收集器可以像管理java堆一樣管理方法區。

4、方法區是否在記憶體上是連續的:不需要。大小可固定也可可擴充套件。還可以選擇不實現垃圾收集。但並非資料進入了方法區就“永久”存在了。這區域主要針對常量池的回收和對型別的解除安裝,回收成績令人滿意,尤其是對型別的解除安裝。

5、記憶體回收目標:常量池的回收、對型別的解除安裝。當然,在JDK1.7後,字串常量池已經從方法區移出。

2.6 執行時常量池

1、定義:執行時常量池是方法區的一部分。Class檔案中除了有類的版本、欄位、方法、介面等描述資訊外,還有常量池,用於存放編譯期生成的字面量、符號引用和翻譯出來的直接引用,這部分內容將在類載入後進入方法區的執行時常量池中存放。

2、執行時常量池相對於class檔案常量池的另外一個特徵是具備動態性。Java常量產生的方式:
1)編譯器產生(並非預置入Class檔案中常量池的記憶體才能進入方法區執行時常量池)
2)執行期間產生(String類的intern()方法體現了這種特性)

3、異常:執行時常量池是方法區的一部分,也會受到方法區記憶體的限制,常量池無法申請到記憶體時會丟擲OutOfMemoryError異常。

2.7 直接記憶體

1、直接記憶體不是虛擬機器執行時資料區的一部分,也不是java虛擬機器規範中定義的記憶體區域。但這部分記憶體也會被頻繁使用。JDK1.4加入了NIO(New Input/Output)類,引入了一種基於通道(Channel)和緩衝區(Buffer)的I/O方式,它可以使用Native函式庫直接分配堆外記憶體,然後通過儲存在java堆中的DirectBuffer物件作為這塊記憶體的引用進行操作。因為避免了在Java堆和Native堆中來回複製資料,所以可以顯著提高效能。

2、直接記憶體不受Java堆大小的限制,但是會受到本機總記憶體大小以及處理器定址空間的限制。

3、異常:伺服器管理員在配置虛擬機器引數時,根據實際記憶體設定-Xmx等引數資訊,經常忽略直接記憶體,使得各個記憶體區域總和大於實體記憶體限制,從而導致動態擴充套件時出現OutOfMemoryError異常。

3 HotSpot虛擬機器物件

以常用的虛擬機器HotSpot和常用的內區域Java堆為例,深入探討HotSpot虛擬機器在Java堆中物件分配、佈局和訪問的全過程。

3.1 物件的建立

虛擬機器遇到一條new指令時,先去檢查這個指令的引數是否能在常量池中定位到一個類的符號引用,並檢查這個符號引用代表的類是否已被載入、解析和初始化過。若沒有,則先執行相應的類載入過程(第7章介紹)。

在類載入檢查通過後,虛擬機器將為新生物件分配記憶體。物件所需記憶體的大小在類載入完成後便可完全確定,為物件分配空間的認為就是把一塊確定大小的記憶體從Java堆中劃分出來。為物件劃分記憶體的方式有2中:
1)若Java堆中的記憶體時絕對規整的,用過的記憶體放一邊,空閒的記憶體放另一邊,中間有一個指標作為分界點指示器,那麼分配記憶體就是把指標向空閒空間那邊挪動物件大小的距離,這種分配方式稱為“指標碰撞”。
2)若Java堆中的記憶體不規整,已用過的記憶體和空間記憶體相互交錯,虛擬機器需要維護一個列表,記錄哪些記憶體塊是可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給物件例項,並更新列表上的記錄,這種分配方式成為“空閒列表”。
Java堆是否規整由垃圾收集器是否帶有壓縮整理功能決定。使用Serial, ParNew等帶有Compact過程的收集器時,系統採用指標碰撞分配記憶體,而使用CMS這種基於Mark-Sweep演算法的收集器時,採用空閒列表。

物件建立在虛擬機器中是非常頻繁的行為,即使僅僅修改一個指標所指向的位置,在併發情況下也不是執行緒安全的,可能出現正在給A物件分配記憶體,指標還沒來得及修改,物件B又同時使用了原來的指標來分配記憶體的情況。解決方案2種:
1)對分配記憶體空間的動作進行同步處理–虛擬機器採用CAS配上失敗重試的方式保證更新操作的原子性。
2)每個執行緒在Java堆中預先分配小塊記憶體,稱為本地執行緒分配緩衝(Thread Local Allocation Buffer, TLAB)。執行緒在TLAB上分配記憶體,只有TLAB用完並分配新的TLAB時,才同步鎖定。虛擬機器是否使用TLAB,可通過-XX:+UseTLAB引數設定。

記憶體分配後,虛擬機器將分配到的記憶體空間初始化為零值(不包括物件頭),若使用TLAB,這一過程可以提前至TLAB分配時進行。該操作保證了物件的例項欄位在Java程式碼中不賦值就能直接使用,程式能訪問到這些欄位的資料型別所對應的零值。

虛擬機器要對物件進行物件頭資訊的設定,包括:該物件是哪個類的例項、如何找到類的元資料資訊、物件的雜湊碼、物件的GC分代年齡。

從虛擬機器角度來看,一個新物件到這時就產生了,但從Java程式角度看,物件建立才剛開始–方法還沒執行,所有欄位還為零。所以,執行new指令後會接著執行方法,把物件按照程式設計師的意願初始化。

3.2 物件的記憶體佈局

物件記憶體中儲存的佈局可以分為3塊區域:物件頭、例項資料和對齊填充。
在這裡插入圖片描述
型別指標:物件指向它的類元資料的指標,虛擬機器通過這個指標來確定這個物件是哪個類的例項。查詢物件的元資料資訊不一定通過物件本身,所以不是所有的虛擬機器實現都必須在物件資料上保留型別指標。另外,如果物件是一個Java陣列,那麼物件頭中還需要記錄陣列的長度,因為虛擬機器可以通過普通Java物件的元資料資訊確定Java物件的大小,但是從陣列的元資料中無法確定陣列的大小。

例項資料:是物件真正儲存的有效資訊,也是程式程式碼中定義的各種型別的欄位內容。無論是父類繼承下來的,還是子類中定義的,都要記錄下來。儲存順序受虛擬機器分配策略引數和欄位在Java原始碼中的定義順序的影響。HotSpot虛擬機器預設的分配策略是longs/doubles, ints, shorts/chars, bytes/booleans, oops(Ordinary Object Pointers普通物件指標),相同寬度的欄位總是被分配到一起。在滿足這個條件的情況下,父類中定義的變數會出現在子類的前面。若CompactFields引數為true,那麼子類之中較窄的變數可以插入到父類變數的空隙之中。

對齊填充:並不是必然存在的,僅僅起著佔位符的作用。由於HotSpot虛擬機器的自動記憶體管理系統要求物件起始地址必須是8位元組的整數倍,也就是物件的大小必須是8位元組的整數倍。物件頭部分正好是8位元組的倍數,因此物件例項資料部分沒有對齊時,就需要通過對齊填充來補全。

3.3 物件的訪問定位

java程式需要通過棧上的reference資料來操作java堆上的具體物件。通過引用定位、訪問堆中物件的具體位置的主流的方式有2中:使用控制代碼、直接指標。

1)使用控制代碼:java堆中會有一塊記憶體區域專門作為控制代碼池,reference中儲存的就是物件的控制代碼地址。
在這裡插入圖片描述
好處:reference中儲存的是控制代碼地址,在物件被移動(垃圾收集時移動物件)時,只會改變控制代碼中的例項資料指標,而reference本身不用改。

2)直接指標:reference儲存的直接就是物件地址。
在這裡插入圖片描述
好處:訪問物件速度快,因為它節省了一次指標定位的時間開銷。HopSpot使用的訪問定位方式是用直接指標訪問物件。

4 實戰:OutOfMemoryError異常

4.1 Java堆溢位

java堆用於儲存物件例項,保證GC Roots到物件之間有可達路徑來避免垃圾回收機制清除這些物件,那麼當物件數列到達最大堆的容量限制後就會出現記憶體溢位(OOM)異常,異常堆疊會進一步提示"java heap space"。

將堆的最大值和最小值設定成一樣即可避免堆自動擴充套件。

解決方法:確認記憶體中的物件是否是必要的,即到底是出現了記憶體洩漏還是記憶體溢位。
1)如果是記憶體洩漏,可用工具檢視洩漏物件到GC Roots的引用鏈,找到洩漏物件是通過怎樣的路徑與GC Roots相關聯並導致垃圾收集器無法自動回收它們,從而較準確定位出洩漏程式碼的位置。
2)如果不存在記憶體洩漏,即記憶體中的物件確實必須活著,那就檢查虛擬機器的堆引數(-Xmx, -Xms),與實體記憶體對比看是否可以調大,從程式碼上檢查是否存在某些物件的宣告週期過長、持有狀態時間過長的情況,嘗試減少程式執行期的記憶體消耗。

4.2 虛擬機器棧和本地方法棧溢位

由於HotSpot不區分虛擬機器棧和本地方法棧,所以本地方法棧-Xoss引數存在,但無效,棧容量只由-Xss引數設定。2種異常:
1)如果執行緒請求的棧深度大於虛擬機器允許的最大深度,將丟擲StackOverflowError異常。
2)虛擬機器在擴充套件棧時無法申請到足夠的記憶體空間,將丟擲OutOfMemoryError異常。
2種異常存在重疊的地方:當棧空間無法分配時,到底是記憶體太小,還是已使用的棧空間太大。

單執行緒下,無論由於棧幀太大還是虛擬機器棧容量太小,都丟擲StackOverflowError異常。
多執行緒下,通過不斷新建執行緒可以出現OutOfMemoryError異常。每個執行緒的棧分配的記憶體越大,越容易產生記憶體溢位。

作業系統分配給每個程序的記憶體都是有限制的,32位限制為2GB,虛擬機器提供了控制java堆和方法區的記憶體大小的引數。2GB-最大堆容量(Xmx)-最大方法區容量(MaxPermSize),程式計數器佔記憶體很小可忽略,如果虛擬機器程序本身耗費記憶體不算其中,剩餘的記憶體就被虛擬機器棧和本地方法棧瓜分了。每個執行緒的棧分配的記憶體越大,可建立的執行緒數量就越少,越容易產生記憶體溢位。

StackOverflowError異常一般容易解決。如果出現記憶體溢位異常時,可通過減少最大堆和減少棧容量來換取更多的執行緒。java執行緒是對映到作業系統核心執行緒上的。

4.3 方法區和執行時常量池溢位

執行時常量池是方法區的一部分。String.intern()是一個Native方法,作用是:如果字串常量池中已經包含一個等於此String物件的字串,則返回代表池中這個字串的String物件;否則,將次String物件包含的字串新增到常量池中,並且返回此String物件的引用。在JDK1.6及之前的版本中,常量池分配在永久代內,可通過-XX:PermSize和-XX:MaxPermSize限制方法區大小,從而限制常量池的容量。
執行時常量池溢位,在OutOfMememoryError後面跟著提示資訊“PermGen space”,說明執行時常量池是方法區(HotSpot虛擬機器中的永久代)的一部分。

方法區用於存放class的相關資訊,如類名、訪問修飾符、常量池、欄位描述、方法描述等。同一個類檔案,被不同的載入器載入也會視為不同的類。p58

4.4 本機直接記憶體溢位

DirectMemory容量可通過-XX:MaxDirectMemorySize指定,若不指定,則預設與java堆的最大值(-Xms)一樣。p59