JVM-java內(nèi)存區(qū)域與內(nèi)存溢出異常
1 說明
java 與 c++之間有一堵由內(nèi)存動態(tài)分配和垃圾回收技術(shù)所圍成的高墻,墻外的人想進來, 墻內(nèi)的人想出去。然而java的使用者就是這些墻里的人。這篇文章就是介紹java虛擬機內(nèi)存的各個區(qū)域,講述這些區(qū)域的作用,服務(wù)對象以及其中可能產(chǎn)生的問題。從這里,我們開始進行翻墻工作。然而請注意,墻的那邊是高能區(qū)......
2 運行時數(shù)據(jù)區(qū)域
java虛擬機在執(zhí)行java程序過程中,會把它管理的內(nèi)存分成若干個不同的數(shù)據(jù)區(qū)。有的區(qū)域鎖著虛擬機進程的啟動而存在,有些是依賴用戶進程建立與銷毀。在java7 的虛擬機規(guī)范中規(guī)定,虛擬機所管理的內(nèi)存將會包括如下幾個運行時數(shù)據(jù)區(qū)域。下面就是java虛擬機運行時數(shù)據(jù)區(qū):

2.1 程序計數(shù)器
程序計數(shù)器是一塊較小的內(nèi)存空間,在虛擬機概念模型里,程序計數(shù)器的值是為了給字節(jié)碼解釋器提給選取嚇一跳需要執(zhí)行的字節(jié)碼只能提供幫助的。在分支,循環(huán),跳轉(zhuǎn),異常處理,線程恢復(fù)都需要依賴這個計數(shù)器。因為程序計數(shù)器能幫助線程跳轉(zhuǎn)和恢復(fù):java虛擬機執(zhí)行多線程是采用輪流切換的機制,因此為了在切換回來的時候知道在哪里繼續(xù)執(zhí)行,所以才用程序計數(shù)器。
由此可知,程序計數(shù)器是線程私有的內(nèi)存區(qū)>程序技術(shù)器也是唯一一個沒有規(guī)定任何OOM(OutOfMemoryError)情況的區(qū)域。
2.2 java虛擬機棧
java虛擬機棧是描述java方法執(zhí)行的內(nèi)存模型,每個方法執(zhí)行時候都會建立一個“棧幀”用于存儲:局部變量,操作數(shù)棧,動態(tài)鏈接,方法的返回信息等。每一個方法的調(diào)用都是一個棧幀入棧到出棧的過程。經(jīng)常有人把java內(nèi)存區(qū)域分成“堆”和“棧”,然而java的內(nèi)存區(qū)域要遠遠復(fù)雜的多,而大家口中的“?!本褪沁@里的“java虛擬機?!被蛘吒∫稽c,是虛擬機棧里的“局部變量表”部分。局部變量表存儲了編譯期就可以知道的基本數(shù)據(jù)類型,引用對象,和returnAddress類型。局部變量表的內(nèi)存空間在編譯期就已經(jīng)完成分配了。當進入一個方法時候,這個方法余姚在幀中非配多大的局部變量空間是確定的了(大家可以想一下,方法主要是由什么組成的,不就是局部變量么?既然都已經(jīng)知道要用什么局部變量了,那么這個內(nèi)存空間豈不是已經(jīng)確定了?)在java虛擬機規(guī)范中,對虛擬機棧規(guī)定了兩種異常情況:
- StackOverflowError: 如果線程請求的棧深度大于虛擬機所允許的深度,則拋出棧溢出錯誤。
- OOM 如果虛擬機棧的內(nèi)存無法繼續(xù)擴展,則拋出內(nèi)存超出錯誤。
有上可知:java虛擬機棧也是線程私有的
2.3 本地方法棧
本地方法棧與虛擬機棧發(fā)揮的作用相似,不過虛擬機棧調(diào)用的是java的方法,而本地方法棧調(diào)用的是Native方法。因為java虛擬機規(guī)范中并沒有規(guī)定“本地方法?!币檬裁凑Z言實現(xiàn),所以甚至有些虛擬機都把“本地方法?!迸c“虛擬機棧”合二為一。與虛擬機棧一樣,同樣會拋出上面的兩個異常。
2.4 java堆
java堆是java虛擬機所管理的內(nèi)存中的最大的一塊。此內(nèi)存的唯一目的就是存放對象實例!幾乎所有的對象實例都在這里分配內(nèi)存。“java堆”是java垃圾回收機制管理的主要區(qū)域,因此也被稱為“GC堆”。在內(nèi)存回收的角度來看,由于算法多采用的是分代回收,所以又被分為:新生代,老年代。 在細致一點的可以分為:Eden區(qū), From survivor空間和 To survivor空間。在內(nèi)存分配的角度來說,java堆可以分成多個線程私有的分配緩沖區(qū)TLAB(Thread Local Allocation Buffer)。
java堆 是內(nèi)存共享的
2.5 方法區(qū)
方法區(qū)與java堆一樣,用來存儲已經(jīng)被虛擬機加載的類信息、常量、靜態(tài)變量、及時編譯后的代碼數(shù)據(jù)。(那為什么叫做方法區(qū)呢?很好奇)方法區(qū)很多人有稱之為“永久代”,雖然這么稱呼并不完全準確。原來的字符串常量池在永久代里,但是現(xiàn)在看來這么設(shè)計并不是一個好的作法(因為String.intern()方法,可以動態(tài)的向常量池用添加字符串,如果永久代不清理的話/換句話說清理起來很難,就會導(dǎo)致內(nèi)存泄露。)
也是線程共享的
3 對象的創(chuàng)建
java是一門面向?qū)ο蟮恼Z言。在java程序運行過程中,無時無刻不在有對象被創(chuàng)建。在語言成面上,僅僅是一個new而已。但是在虛擬機中,又是一個怎樣的景象呢?
(1) 當虛擬機遇到一條new指令的適合,首先將去檢查這個指令的參數(shù)是否能在常量池中定位到一個類的符號引用,并且檢查這個符號引用代表的類是否已經(jīng)被加載、解析、初始化過的。如果沒有,那必須先進性相應(yīng)的類的初始化。
-
(2) 當類加載檢查后,接下來虛擬機為新生對象分配內(nèi)存,對象所需的內(nèi)存大小在類加載后便可以完全確定下來。然后分配對象的任務(wù)就等同于把一塊大小確定的內(nèi)存劃分出來:* 指針碰撞 * 如果內(nèi)存區(qū)域是卻對規(guī)整的,所有已經(jīng)分配的內(nèi)存在一起,沒有分配的內(nèi)存在一起。則就可以在中間設(shè)置一個指針,當創(chuàng)建新的對象時候,就把指針向沒有分配的地方移動即可。
- Serial、ParNew 等帶有Compact過程的收集器,則采用的是指針碰撞。
- 空閑列表: 如果內(nèi)存是不規(guī)整的,則用一張表記錄哪些是分配的,哪些是沒有分配的。在沒有分配的內(nèi)存中取出一塊比較大的內(nèi)存使用,并且記錄下情況。
- 使用CMS這種基于Mark-Sweep算法的收集器時。
(3) 上面的情況是在單線程的情況下,如果在多線程的情況下有如下的解決辦法:一種是對分配內(nèi)存的動作進行同步---采用CAS+失敗重試的方式保證操作的原子性。第二種是在每個線程上都分配一塊內(nèi)存,自己線程的對象,在自己的內(nèi)存上進行分配(TLAB)。
4 一個有意思的現(xiàn)象
public class RuntimeConstantPoolOOM{
public static void main(String[] args){
String str1 = new StringBuffer("計算機").append("軟件").toString();
System.out.println(str1.intern() == str1); String str2 = new StringBuffer("ja").append("va").toString();
System.out.println(str2.intern() == str2)
}
}
上面的這段代碼,如果在1.6執(zhí)行,則都輸出false:因為StringBuffer是在堆中申請的內(nèi)存,String.intern是將對象復(fù)制到方法區(qū)(常量池)中。所以兩個不是指向一個地方; 如果是在1.7中,則第一個輸出true, 第二個輸出false, 因為1.7中的intern方法不再是復(fù)制對象,而是記錄復(fù)制的引用。所以第一個輸出true,而java這里并不是第一次出現(xiàn),所以常量池中的引用并不是內(nèi)存中的引用,所以輸出false;
內(nèi)容學(xué)習(xí)自GC博客