JVM高級特性與實踐(一):Java內存區域 與 內存溢出異常

套用《圍城》中的一句話,「牆外面的人想進去,牆裏面的人想出來」,用此來形容Java與C++之間這堵內存動態分配和垃圾收集技術所圍成的「圍牆」就再合適不過了。java

對於從事C、C++的開發人員而言,在內存管理領域,他們具備絕對的「權利」——擁有每一個對象的控制權,並擔負着每一個對象生命週期的維護責任。而對於Java開發人員而言,在虛擬機自動內存管理機制的幫助下,無需爲每個建立new操做去配對 delete/free 代碼,減小內存泄漏和內存溢出的問題,這些都交給了Java虛擬機去進行內存控制,可是正因如此,當出現相關問題時,若不瞭解JVM使用內存規則,就難以排查錯誤。接下來以此篇文章記錄學習Java虛擬機內存各個區域概念、做用、服務對象以及可能產生的問題。算法

此篇將記錄學習如下知識點:數組

Java虛擬機內存劃分
各區域知識理論學習
各區域會產生的異常
代碼實踐哪些操做會產生內存溢出異常
各區域出現異常的緣由
一. 運行時區域內存服務器

Java虛擬機在執行Java程序時會將其管理的區域劃分爲不一樣的數據區域,不一樣區域之間擁有各自用途、建立和銷燬的時間,有的區域隨着虛擬機進程的啓動而存在,而有的區域則依賴用戶線程的生命週期。Java虛擬機所管理的內容分爲如下幾塊區域:數據結構

這裏寫圖片描述多線程

1 . 程序計數器ide

(1)含義做用學習

程序計數器(Program Counter Register)是一塊較小的內存空間,能夠看做是當前線程所執行的字節碼的行號指示器。在虛擬機概念模型中,字節碼解釋器工做時就是經過改變計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴計數器。測試

(2)計數器與多線程大數據

因爲JVM的多線程時經過線程輪流切換並分配處理器執行時間的方式來實現的,在任何一個肯定的時刻,一個處理器(對於多核處理器來講是一個內核)都只會執行一條線程中的指令。因此,爲了線程切換後能恢復到正確的執行位置,每條線程須要一個獨立的程序計數器,各線程之間計數器互不影響、獨立存儲,至關因而一塊「線程私有」的內存。

(3)虛擬機規範記錄(有關異常)

若線程正在執行的是一個Java方法,這個計數器記錄的時正在執行的虛擬機字節碼指令的地址;若執行的是Native方法,則計數器爲空(Undefined)。注意:此內存區域是惟一一個在Java虛擬機規範中沒有規定任何 OutOfMemoryError狀況的區域。

2 . Java虛擬機棧

(1)含義做用

同程序計數器相同,Java虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每一個方法在執行的同時都會建立一個棧幀用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每個方法從調用直至執行完成的過程,會對應一個棧幀在虛擬機棧中入棧到出棧的過程。

(2)Java內存區分誤區

大多數人覺得Java內存區分爲堆內存(Heap)和棧內存(Stack),這是一種誤區,Java內存區域的劃分遠比這種粗糙的分法更加複雜。這種劃分方式普遍流傳是因爲大多數開發者關注與對象內存分配關係最密切的內存區域就是這兩塊,有關「堆」的知識後續載提,這裏的「棧」指的就是虛擬機棧,或者說是虛擬機棧中的變量表部分。

(3)虛擬機棧中的局部變量表

局部變量表中存放了編譯期可知的

八大數據類型(boolean、byte、char、short、int、float、long、double)。
對象引用(reference類型,它不等於對象自己,多是一個指向對象起始地址的指針,也多是指向一個表明對象的句柄或其餘與此對象相關的位置)
returnAddress類型(指向了一條字節碼指令的地址)
其中64位長度的long和double類型的數據會佔用2個局部變量空間(Slot),其他數據類型只佔用1個。局部變量表所需的內存控件在編譯期間完成分配,當進入一個方法時,這個方法須要在幀中分配多大的局部變量空間是徹底肯定的,在方法運行期間不會改變局部變量表的大小。

(4)虛擬機規範記錄(有關異常)

在Java虛擬機規範中,對這個區域規定了兩種異常情況:

若線程請求的棧深度大於虛擬機所容許的深度,將拋出StackOverflowError異常。
若虛擬機能夠動態擴展(當前大部分Java虛擬機均可動態擴展,只不過Java虛擬機規範也容許固定長度的虛擬機棧),當擴展時沒法申請到足夠的內存,就會拋出OutOfMemoryError異常。
3 . 本地方法棧

(1)含義做用

本地方法棧(Native Method Stack)與虛擬機棧所發揮的做用相似,它們之間的區別是:虛擬機棧爲虛擬機執行Java方法(也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的Native方法服務。

(2)虛擬機規範記錄(有關異常)

在虛擬機規範中對本地方法棧中使用的語言、方式和數據結構並沒有強制規定,所以具體的虛擬機可實現它。甚至有的虛擬機(Sun HotSpot虛擬機)直接把本地方法棧和虛擬機棧合二爲一。

與虛擬機同樣,本地方法棧會拋出StackOverflowError和OutOfMemoryError異常。

4 . Java堆

(1)含義做用

對於大多數應用而言,Java堆(Heap)是Java虛擬機所管理的內存中最大的一塊,它是被全部線程共享的一塊內存區域,在虛擬機啓動時建立。此內存區域惟一的目的是存放對象實例,幾乎全部的對象實例都在這裏分配內存。Java虛擬機規範中描述道:全部的對象實例以及數組都要在堆上分配,可是隨着JIT編譯器的發展和逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會致使一些微妙的變化發生,全部的對象都在堆上分配的定論也並不「絕對」了。

(2)Java堆與垃圾回收器

Java堆是垃圾回收器管理的主要區域,所以被稱爲「GC堆」(Garbage Collected Heap)。

從內存回收角度看,因爲目前收集器基本採用分代收集算法,因此Java堆可細分爲:新生代和老年代。
從內存分配角度來看,線程共享的Java堆中可能劃分出多個線程私有的分配緩衝區(TLAB:Thread Local Allocation Buffer)。
不過不管如何劃分,都與存放內容無關,不管哪一個區域,存放的都是對象實例,進一步劃分目的是爲了更好地回收內存,或者是更快地分配內存。此節僅對內存區域做用進行學習,Java堆上各個區域分配回收等細節與內存分配策略有關,後續講解。

(3)虛擬機規範記錄(有關異常)

根據Java虛擬機規範的規定,Java堆能夠處於物理上不連續的內存中,只要邏輯上是連續的便可,就像磁盤空間。在實現時,能夠實現成固定大小或可擴展的,不過當前主流虛擬機是按照可擴展進行實現的(經過-Xmx和 -Xms控制)。

若堆中沒有內存完成實例分配,而且堆也沒法擴展時,將會拋出OutOfMemoryError異常。

5 . 方法區

(1)含義做用

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

(2)虛擬機規範記錄(有關異常)

Java虛擬機規範對方法區的限制很是寬鬆,除了和Java堆同樣不須要連續的內存和能夠選擇固定大小或可擴展外,還能夠選擇不實現垃圾收集。相對而言,垃圾收集行爲在這個區域比較少見。此區域的內存回收目標主要是針對常量池的回收和對類型的卸載,通常來講,回收效果難以使人滿意,尤爲是類型的卸載,條件相對苛刻,可是這部分區域回收是有必要的。

根據Java虛擬機規範的規定,當方法沒法知足內存需求時,將會拋出OutOfMemoryError異常。

6 . 運行時常量池

(1)含義做用

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

(2)運行時常量池和Class文件

Java虛擬機對Class文件每一部分(天然包括常量池)的格式有嚴格規定,每個字節用於存儲那種數據都必須符合規範上的要求才會被虛擬機承認、裝載和執行。但對於運行時常量池,Java虛擬機規範沒有作任何有關細節的要求,不一樣的提供商實現的虛擬機能夠按照本身的需求來實現此內存區域。不過通常而言,除了保存Class文件中的描述符號引用外,還會把翻譯出的直接引用也存儲在運行時常量池中。

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

(3)虛擬機規範記錄(有關異常)

運行時常量池是方法區的一部分,天然受到方法區的內存限制,當常量池沒法再申請到內存時會拋出OutOfMemoryError異常。

7 . 直接內存

(1)含義做用

直接內存(Direct Memory)並非虛擬機運行時數據的一部分,也不是Java虛擬機規範中定義的內存區域。但這部份內存也被頻繁運用,而卻可能致使OutOfMemoryError異常出現。

(3)有關異常

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

二. 實踐:重現OutOfMemoryError異常

在Java虛擬機規範中,除了程序計數器外,虛擬機內存的其它區域都有發生OOM異常的可能,本節內容的目的爲:

經過代碼驗證Java虛擬機規範中描述的各個運行時區域存儲的內容。
開發者在遇到實際的內存溢出時,能根據異常信息判斷出是那個區域,定位到錯誤緣由。
1 . Java堆溢出

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

【Java堆內存溢出異常測試】

/**
* VM Args:-Xms20m -Xmx20m -XX:+HeapDumpOnOutOfMemoryError
*/
public class HeapOOM {

static class OOMObject {
}

public static void main(String[] args) {
List<OOMObject> list = new ArrayList<OOMObject>();

while (true) {
list.add(new OOMObject(www.caihonyule.com));
運行結果:

java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid1820.hprof ...
Heap dump file created [24787111 bytes in 0.346 secs]
Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
結果分析:

Java堆內存的OOM異常是實際應用中常見的內存溢出異常狀況。當出現異常時,堆棧消息「java.lang.OutOfMemoryError」會進一步提示「java heap space」,而問題緣由很明顯,對象數量過多,到達最大堆的容量限制。

2 . 虛擬機棧和本地方法棧溢出

在JAVA虛擬機規範描述了兩種異常:

若是線程請求的棧深度大於虛擬機所容許的最大深度,將拋出StackOverflowError
若是虛擬機在擴展棧時沒法申請到足夠的內存空間,將拋出OutOfMemoryError。
以上兩種異常實質上存在着重疊的部分:當棧空間沒法繼續分配時,是內存過小仍是已使用的棧空間過大,其本質只是針對同一件事情的兩種描述。

【虛擬機棧和本地方法棧OOM測試(僅做第一點測試)】

/**
* 若是線程請求的棧深度大於虛擬機所容許的最大深度,將拋出StackOverflowError
* 若是虛擬機在擴展棧時沒法申請到足夠的內存空間,將拋出OutOfMemoryError
* VM Args:-Xss128k
*/
public class JavaVMStackSOF {

private int stackLength = 1;

public void stackLeak(www.hbwfjx.cn/) {
stackLength++;
stackLeak();
}

public static void main(String[] args) throws Throwable {
JavaVMStackSOF oom = new JavaVMStackSOF();
try {
oom.stackLeak();
} catch (Throwable e) {
System.out.println("stack length:" + oom.stackLength);
throw e;
運行結果:

stack length:2403
Exception in thread "main" java.lang.StackOverflowError
at baby.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:11)
at baby.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)
at baby.oom.JavaVMStackSOF.stackLeak(JavaVMStackSOF.java:12)

結果分析:

以上測試代表,在單線程下,不管是因爲棧幀太大仍是虛擬機棧容量過小,當內存沒法分配的時候,虛擬機拋出的都是StackOverflowError。

若是測試時不只限於單線程,經過不斷地創建線程的方式能夠產生內存溢出異常,以下代碼測試。可是這樣產生的內存溢出異常與棧空間是否足夠大並不存在任何聯繫,準確說在這種狀況下,爲每一個線程的棧分配內存越大,越容易產生內存溢出的異常。

簡述操做系統內存分配

其實緣由不難理解,因爲操做系統分配給每一個進程的內存是有限的,例如32位的Windows限制爲2GB,虛擬機提供了參數來控制Java堆和方法區域的這兩部份內存的最大值。剩餘的內存爲2GB(操做系統限制) - Xmx(最大堆容量)- MaxPermSize(最大方法區容量),程序計數器消耗內存少到能夠忽略。若是虛擬機進程自己耗費的內存不計算在內,剩下的內存就由虛擬機棧和本地方法瓜分。每一個線程分配到的棧容量越大,能夠創建的線程數量天然越少,創建線程越容易耗盡剩下內存。

【建立線程致使內存溢出異常】

/**
* VM Args:-Xss2M (這時候不妨設大些)
*/
public class JavaVMStackOOM {

private void dontStop() {
while (true) {
}
}

public void stackLeakByThread() {
while (true) {
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
dontStop();
}
});
thread.start(www.ei66yule.cn/);
}
}

public static void main(String[] args) throws Throwable {
JavaVMStackOOM oom = new JavaVMStackOOM();
oom.stackLeakByThread()
運行結果:

Exception in thread "main" java.lang.OutOfMemoryError: unable to create new native thread
1
1
3 . 方法區和運行常量池溢出

因爲運行時常量池是方法區的一部分,所以二者測試放在一塊兒。測試中涉及到String.intern()方法,它是一個Native方法,做用爲:若是字符串常量池中已經包含一個等於此String對象的字符串,則返回常量池中此字符串的String對象;不然將此String對象包含的字符串添加到常量池中,並返回String對象的引用。

【運行時常量池致使的內存溢出異常】

/**
* VM Args:-XX:PermSize=10M -XX:MaxPermSize=10M
*/
public class RuntimeConstantPoolOOM {

public static void main(String[] args) {
// 使用List保持着常量池引用,避免Full GC回收常量池行爲
List<String> list = new ArrayList<String>();
// 10MB的PermSize在integer範圍內足夠產生OOM了
int i = 0;
while (true) {
list.add(String.valueOf(i++www.lieqibiji.com).intern());
運行結果:

Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
at java.lang.String.intern(Native Method)
at baby.oom.RuntimeConstantPoolOOM.main(RuntimeConstantPoolOOM.java:18)
結果分析:

從運行結果能夠看出,運行時常量池溢出,在OutOfMemoryError後面跟隨的提示信息時「PermGen space」,說明運行時常量池屬於方法區,而問題緣由就是重複添加String對象到常量池而致使內存不足出現OOM異常。(注意:當運行時常量池過大或者類過多時就會致使方法區溢出)

4. 本機直接內存溢出

DirectMemory容量可經過 -XX: MaxDirectMemorySize指定,若不指定則默認與Java堆最大值(-Xmx指定)相同。如下代碼越過了DerictByteBuffer類,直接經過反射獲取Unsafe 實例進行內存分配。雖然使用DerictByteBuffer分配內存也會拋出內存溢出異常,但它拋出異常時並無真正向操做系統申請分配,而是經過計算得知內存沒法分配,因而手動拋出異常,真正申請分配內存的方法是unsafe.allocateMemory()。

【使用unsafe 分配本機內存】

/**
* VM Args:-Xmx20M -XX:MaxDirectMemorySize=10M
*/
public class DirectMemoryOOM {

private static final int _1MB = 1024 * 1024;

public static void main(String[] args) throws Exception {
Field unsafeField = Unsafe.class.getDeclaredFields()[0];
unsafeField.setAccessible(true);
Unsafe unsafe = (Unsafe) unsafeField.get(null);
while (true) {
unsafe.allocateMemory(_www.wmyl11.com/ 1MB);
運行結果:

Exception in thread "main" java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at baby.oom.DirectMemoryOOM.main(DirectMemoryOOM.java:20)
1
2
3

1
2
3
結果分析:

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

相關文章
相關標籤/搜索