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