JVM之Java內存區域及內存溢出異常 …

一、Java虛擬機在執行Java程序的過程中會把它所管理的內存劃分爲若干個不同的數據區域。
1、程序計數器
佔用較小內存空間,可以看做當前線程所執行的字節碼的行號指示器,線程擁有獨立的計數器。
如果線程正在執行java方法,計數器記錄正在執行的虛擬機字節碼指令的地址;如果正在執行native方法,計數器值則爲空。
此內存區域是唯一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError情況的區域。
2、Java虛擬機棧
與程序計數器一樣,也是線程私有的,它的生命週期與線程相同。
虛擬機棧描述的是Java方法執行的內存模型:每個方法在執行的同時都會創建一個棧幀用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。每個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機中入棧和出棧的過程。
局部變量表存放了編譯期可知的各種數據類型(可能是指向對象起始地址的引用指針,也可能是指向一個代表對象的句柄或其他與此對象有關的位置)和returnAddress類型(指向了一條字節碼指令的地址)
這個區域規定了兩種異常狀況:如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverflowError異常;如果虛擬機棧可以動態擴展(當前大部分的Java虛擬機都可動態擴展,只不過Java虛擬機規範中也允許固定長度的虛擬機棧),如果擴展時無法申請到足夠的內存,就會拋出OutOfMemoryError異常。
3、本地方法棧
與虛擬機棧的區別是,本地方法棧用於爲native方法服務。
虛擬機規範沒有對本地方法棧中方法使用的語言、使用方法和數據結構並沒有強制規定,因此具體的虛擬機可以自由實現它,甚至有的虛擬機(如Sun HotSpot虛擬機)直接就把本地方法棧和虛擬機棧合二爲一。
與虛擬機棧一樣,本地方法棧區域也會拋出StackOverflowError和OutOfMemoryError異常。
4、Java堆
對於大多數應用來說,Java堆(Java Heap)是Java虛擬機所管理的內存中最大的一塊。Java堆是被所有線程共享的一塊內存區域,在虛擬機啓動時創建。此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這裏分配內存。
5、方法區
方法區與Java堆一樣,是各個線程共享的內存區域,它用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。雖然Java虛擬機規範把方法區描述爲堆的一個邏輯部分,但是它卻有一個別名叫做Non-heap(非堆),目的應該是與Java堆區分開來。在HotSpot上習慣把方法區稱爲“永久代”(Permanent Generation),本質上兩者不等價,僅僅因爲HotSpot實現把GC分代收集擴展至方法區,或者說使用永久代來實現方法區而已,這樣HotSpot的垃圾收集器可以像管理Java堆一樣管理這部分內存,能夠省去專門爲方法區編寫內存管理代碼的工作。
使用永久代來實現方法區,並不好,因爲這樣更容易遇到內存溢出問題(永久代有-XX:MaxPermSize的上線,否則只要不達到內存上限就不會出問題),因此JDK1.7的HotSpot中,已經把原本放在永久代的字符串常量池移出。
垃圾收集行爲在這個區域是比較少出現的,這區域的內存回收目標主要是針對常量池的回收和對類型的卸載
根據Java虛擬機規範的規定,當方法區無法滿足內存分配需求時,將跑出OutOfMemoryError異常。
6、運行時常量池
運行時常量池是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用於存放編譯器生成的各種字面量和符號引用,這部分內容將在類加載後進入方法區的運行時常量池中存放。
7、直接內存
直接內存並不是虛擬機運行時數據區的一部分,也不是Java虛擬機規範中定義的內存區域。
在JDK 1.4中新加入了NIO類,引入了一種基於通道與緩衝區的I/O方式,它可以使用Native函數庫直接分配堆外內存,然後通過一個存儲在Java堆中的DirectByteBuffer對象作爲這塊內存的引用進行操作,這樣能在一些場景中顯著提高性能,因爲避免了在Java堆和Natvie堆中來回複製數據。
二、以虛擬機HotSpot和常用的內存區域Java堆爲例,深入探討HotSpot虛擬機在Java堆中對象分配、佈局和訪問的全過程。
1、對象的創建
語言層面,創建對象(例如克隆、反序列化)都是使用new關鍵字而已,對象僅限普通Java對象,不包括數組和Class對象等。虛擬機遇到new指令時,首先檢查這個指令的參數是否能在常量池中定位到一個類的符號引用,並檢查這個符號引用代表的類是否已被加載、解析和初始化過。如果沒有,那必須先執行相應的類加載過程。
加載完後,接下來虛擬機爲新生對象分配內存。對象所需內存大小在類加載完成後便可完全確定,給對象分配空間等同於把一塊確定大小的內存從Java堆中劃分出來。內存分配有“指針碰撞”和“空間列表”兩種方式,具體哪個取決於垃圾收集器是否帶有壓縮整理功能。
在上面工作都完成之後,從虛擬機的視角來看,一個新的對象已經產生了,但從Java程序的視角來看,對象創建纔剛剛開始---init方法還沒有執行,所有的字段都還爲零。執行new指令之後會接着執行init方法, 把對象按照程序員的意願進行初始化,這樣一個真正可用的對象纔算完全產生出來。
2、對象的內存佈局
在HotSpot虛擬機中,對象在內存中存儲的佈局可以分爲3塊區域:對象頭(Header)、實例數據(Instance Data)和對齊填充(Padding)。
對象頭包括兩部分信息:第一部分用於存儲對象自身的運行時數據,如哈希碼、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳;第二部分是類型指針,即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。
3、對象的訪問定位
建立對象是爲了使用對象,我們的Java程序需要通過棧上的reference數據來操作堆上的具體對象。由於reference類型在Java虛擬機規範中只規定了一個指向對象的引用,並沒有定義這個引用應該通過何種方式去定位、訪問堆中的對象的具體位置,所以對象訪問方式也是取決於虛擬機實現而定的。目前主流的訪問方式有使用句柄和直接指針兩種。
三、實戰:OutOfMemoryError異常
在Java虛擬機規範的描述中,除了程序計數器外,虛擬機內存的其他幾個運行時區域都有發生 OutOfMemoryError(下文稱OOM)異常的可能性。下面通過若干實例來驗證異常發生的場景。
在實際工作中,要根據異常的信息快速判斷是哪個區域的內存溢出,知道什麼樣的代碼可能會導致這些區域內存溢出,以及出現這些異常後該如何處理。
1、Java堆溢出
只要不斷創建對象,並且保證GC Roots到對象之間有可達路徑來避免垃圾回收機制清除這些對象,那麼在對象數量到達最大堆的容量限制後就會產生內存溢出異常。
將堆的最小值 -Xms參數與最大值-Xmx參數設置爲一樣即可避免堆自動擴展。
通過參數 -XX:+HeapDumpOnOutOfMemoryError可以讓虛擬機在出現內存溢出時Dump出當前的內存堆轉儲快照以便事後進行分析。
Java堆內存溢出時,異常堆棧信息會進一步提示“Java heap space”,要解決這個區域的異常,一般的手段是先通過內存映像分析工具(如Eclipse Memory Analyzer)對Dump出來的堆轉儲快照進行分析,重點分析師內存泄漏(Memory Leak)還是內存溢出(Memory Overflow)
如果是內存泄漏,可進一步通過工具查看泄露對象到GC Roots的引用鏈。於是就能找到泄漏對象是通過怎樣的路徑與GC Roots相關聯並導致垃圾收集器無法自動回收它們的。如果不存在泄露,換句話說,與機器物理內存對比看是否還可以調大,從代碼上檢查是否存在某些對象生命週期過長、持有狀態時間過長。
內存泄漏檢測工作“eclipse memory analyzer”,地址“http://archive.eclipse.org/mat/1.2/update-site/”
2、虛擬機棧和本地方法棧溢出
-Xoss參數用於設置本地方法棧大小,-Xss參數用於設置虛擬機棧大小,但HotSpot中棧容量只由-Xss設定。
有兩種異常:線程請求的棧深度大於虛擬機所允許的最大深度,將拋出StackOverflowError異常;如果虛擬機在擴展棧時無法申請到足夠的內存空間, 則拋出OutOfMemoryError異常。
實驗結果標明:在單線程下,無論由於棧幀太大還是虛擬機棧容量太小,當內存無法分配的時候,虛擬機拋出的都是StakOverflowError異常。如果測試時不限於單線程,通過不斷地建立線程的方式倒是可以產生內存溢出異常。
如果是建立過多線程導致的內存溢出,在不能減少線程數或者更換64位虛擬機的情況下,就只能通過減少最大棧和減少棧容量來換取更多的線程。如果沒有這方面的經驗,這種通過“減少內存”的手段來解決內存溢出的方式會比較難以想到。
3、方法區和運行時常量池溢出
常量池測試可以用使用String.intern()方法在池中創建大量的字符串
方法區測試可以在運行時產生大量的類去填滿方法區
4、本機直接內存溢出
DirectMemory容量可通過 -XX:MaxDirectMemorySize指定,如果不指定,則默認與Java堆最大值(-Xmx指定)一樣




































發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章