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

一、概述:

對於Java程序員來說,在虛擬機自動內存管理機制的幫助下,不再需要爲每一個new 的對象去寫delete/free操作。不容易出現內存溢出或者內存泄漏的問題,正因爲如此,一旦出現相關事件,我們應該懂得如何去排查問題所在。

二、運行時的數據區域

JVM在執行Java程序的過程中會把它所管理的內存劃分爲若干個區域。有的區域是隨着jvm的啓動而存在,有的區域隨着線程的創建而建立。包括了以下幾個組成部分:
2.1、程序計數器
程序計數器(Program Counter Register)是一塊很小的內存空間,是當前線程所執行的字節碼的行號指示器。
Java虛擬機的多線程是通過線程的輪流切換並分配處理器執行時間的方式在實現的。因此爲了使得線程切換後能恢復到之前的執行位置,每條線程都需要一個獨立的程序計數器,各條線程互不影響。所以我們稱這類內存區域爲“線程私有”的內存。如果線程正在執行的是Native方法,則計數器值則爲空(Undefined)。此內存區域也是唯一一個沒有在JVM規範中規定任何OOM(OutOfMemoryError)的內存。

2.2、Java虛擬機棧
首先和程序計數器一樣,Java虛擬機棧也是線程私有的,所以它的生命週期和線程相同。虛擬機棧它所描述的是Java方法執行的內存模型;每個方法在執行的時候會創建一個棧幀(Stack Frame),用於存儲局部變量表、操作數棧、動態鏈接、方法出口等。每個方法執行完成的過程就是棧幀在虛擬機棧中入棧、出棧的過程。
局部變量表存放了編譯期可知的數據類型(boolean,byte,char,short,int,float,long,double),其中64位長度的long和double類型的數據佔用了兩個局部變量空間,其餘的只佔用了一個。局部變量表所需要的內存空間在編譯期間完成分配的,在方法運行的時候不會修改局部變量的大小。
在Java虛擬機棧內存區域中,JVM規範定義了兩種異常情況:如果線程請求的棧深度大於虛擬機所允許的深度,將拋出StackOverFlowerError異常;虛擬機棧通過動態擴展,還是沒有申請到足夠的內存,將會拋出OutOfMemoryError異常。

2.3、本地方法棧
與Java虛擬棧不同的是,本地方法棧使用到的是Native方法,其餘的和虛擬機棧類似。

2.4、Java堆
Java堆通常是由JVM所管理的最大一塊內存區域,也是一塊被所有的線程所共享的內存區域,在JVM啓動的時候創建。該內存區域存放的就是對象實例以及數組,而且是垃圾收集器管理的主要區域。由於現代的收集器基本採用的都是分代收集算法,所以Java堆可以細分爲:新生代、老生代,再細緻一點可以分爲Eden空間,From Survivor空間,To Survivor空間。進一步的劃分是爲了更好的回收內存,或者更好的分配空間。
根據JVM規範的規定,Java堆上的的內存在物理上可以不連續,只要是在邏輯上是連續的即可。在實現上,既可以固定大小,又可以動態擴展(-Xmx和-Xms)。堆中如果沒有內存在進行實例分配,將會拋出OutOfMemoryError異常。

2.5、方法區
和Java堆一樣,也是一個線程共享的內存區域。方法區用於存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。也被稱之爲“永久代”,通過-XX:MaxPermSize設置其上限。方法區中如果無法滿足內存分配的需求,將會拋出OutOfMemoryError異常。GC主要回收的是常量池和類型的卸載。

2.6、運行時常量池
運行時常量池是方法區的一部分,用於存在類在編譯期生成的各種字面量和符號引用。

2.7、直接內存
並不是虛擬機運行時數據區域的一部分,但是也會導致OutOfMemoryError異常。在JDK1.4中加入的NIO,引入了通道和緩存區的I/O方式,它可以通過Native函數直接分配堆外內存,然後通過一個存儲在java堆中的DirectByteBuffer對象作爲這塊內存的引用進行操作,顯著的提高了性能。

三:JVM中對象的操作

對象的創建
在這裏,Java對象的創建只考慮new關鍵字。那麼在虛擬機遇到new的指令時,首先將去檢查這個指令的參數是否在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已被加載、解析、和初始化,如果沒有,則先執行相應的類加載過程。類加載後虛擬機將爲新生對象分配內存。Java堆內存是否規整是由採用的垃圾收集器是否帶有壓縮整理功能所決定的。因此在Serial、ParNew等帶有Compact過程的收集器時,系統採用的算法是指針碰撞,而使用CMS這種基於Mark-Sweep算法的收集器,採用的是空閒列表。
解決內存分配併發的方案:對內存分配的動作進行同步處理,另一種把內存分配的動作按照線程劃分在不同的空間之中進行,即每個線程在Java堆中預先分配一小塊內存,稱之爲本地線程分配緩衝(Thread Local Allocation Buffer TLAB),哪個線程需要分配內存,就在哪個線程的TLAB上分配,只有TLAB用完了,分配新的TLAB時,才需要鎖定同步。虛擬機是否使用TLAB可以通過-XX:+/-UseTLAB參數來設定。

對象的內存佈局
在HotSpot虛擬機中,對象在內存中的存儲佈局分爲3塊區域:對象頭、實例數據、對齊填充。
對象頭包括了兩部分信息,第一部分時用於存儲對象自身的運行時數據,如哈希碼、GC分代、
鎖標誌、偏向時間戳等。另一部分時類型指針。即對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象時哪個類的實例。

對象的訪問定位
我們的Java程序需要通過在棧中的reference(一個指向對象的引用)數據來操作堆上的數據。主流的訪問方式有:句柄(優點:不會隨着對象移動而改變,穩定)和直接指針(速度更快)。

未完,待續。。。

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