JVM內(nèi)存區(qū)域
我們在編寫程序時潘拨,經(jīng)常會遇到OOM(out of Memory)以及內(nèi)存泄漏等問題。為了避免出現(xiàn)這些問題惯裕,我們首先必須對JVM的內(nèi)存劃分有個具體的認(rèn)識秦爆。JVM將內(nèi)存主要劃分為:方法區(qū)、虛擬機(jī)棧宙彪、本地方法棧矩动、堆、程序計數(shù)器释漆。JVM運(yùn)行時數(shù)據(jù)區(qū)如下
程序計數(shù)器
程序計數(shù)器是線程私有的區(qū)域悲没,很好理解嘛~,每個線程當(dāng)然得有個計數(shù)器記錄當(dāng)前執(zhí)行到那個指令男图。占用的內(nèi)存空間小示姿,可以把它看成是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器。如果線程在執(zhí)行Java方法逊笆,這個計數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令地址栈戳;如果執(zhí)行的是Native方法,這個計數(shù)器的值為空(Undefined)难裆。此內(nèi)存區(qū)域是唯一一個在Java虛擬機(jī)規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域子檀。
Java虛擬機(jī)棧
與程序計數(shù)器一樣,Java虛擬機(jī)棧也是線程私有的乃戈。其生命周期與線程相同褂痰。如何理解虛擬機(jī)棧呢?本質(zhì)上來講症虑,就是個棧脐恩。里面存放的元素叫棧幀,棧幀好像很復(fù)雜的樣子侦讨,其實它很簡單驶冒!它里面存放的是一個函數(shù)的上下文苟翻,具體存放的是執(zhí)行的函數(shù)的一些數(shù)據(jù)。執(zhí)行的函數(shù)需要的數(shù)據(jù)無非就是局部變量表(保存函數(shù)內(nèi)部的變量)骗污、操作數(shù)棧(執(zhí)行引擎計算時需要)崇猫,方法出口等等。
執(zhí)行引擎每調(diào)用一個函數(shù)時需忿,就為這個函數(shù)創(chuàng)建一個棧幀诅炉,并加入虛擬機(jī)棧。換個角度理解屋厘,每個函數(shù)從調(diào)用到執(zhí)行結(jié)束涕烧,其實是對應(yīng)一個棧幀的入棧和出棧。
注意這個區(qū)域可能出現(xiàn)的兩種異常:一種是StackOverflowError汗洒,當(dāng)前線程請求的棧深度大于虛擬機(jī)所允許的深度時议纯,會拋出這個異常。制造這種異常很簡單:將一個函數(shù)反復(fù)遞歸自己溢谤,最終會出現(xiàn)棧溢出錯誤(StackOverflowError)瞻凤。另一種異常是OutOfMemoryError異常,當(dāng)虛擬機(jī)検郎保可以動態(tài)擴(kuò)展時(當(dāng)前大部分虛擬機(jī)都可以)阀参,如果無法申請足夠多的內(nèi)存就會拋出OutOfMemoryError,如何制作虛擬機(jī)棧OOM呢瞻坝,參考一下代碼
public void stackLeakByThread(){
while(true){
new Thread(){
public void run(){
while(true){
}
}
}.start()
}
}
這段代碼有風(fēng)險蛛壳,可能會導(dǎo)致操作系統(tǒng)假死,請謹(jǐn)慎使用~~~
本地方法棧
本地方法棧與虛擬機(jī)棧所發(fā)揮的作用很相似所刀,他們的區(qū)別在于虛擬機(jī)棧為執(zhí)行Java代碼方法服務(wù)衙荐,而本地方法棧是為Native方法服務(wù)。與虛擬機(jī)棧一樣勉痴,本地方法棧也會拋出StackOverflowError和OutOfMemoryError異常赫模。
Java堆
Java堆可以說是虛擬機(jī)中最大一塊內(nèi)存了。它是所有線程所共享的內(nèi)存區(qū)域蒸矛,幾乎所有的實例對象都是在這塊區(qū)域中存放瀑罗。當(dāng)然,隨著JIT編譯器的發(fā)展雏掠,所有對象在堆上分配漸漸變得不那么“絕對”了斩祭。
Java堆是垃圾收集器管理的主要區(qū)域。由于現(xiàn)在的收集器基本上采用的都是分代收集算法乡话,所有Java堆可以細(xì)分為:新生代和老年代摧玫。在細(xì)致分就是把新生代分為:Eden空間、From Survivor空間、To Survivor空間诬像。當(dāng)堆無法再擴(kuò)展時屋群,會拋出OutOfMemoryError異常。
方法區(qū)
方法區(qū)存放的是類信息坏挠、常量芍躏、靜態(tài)變量等。方法區(qū)是各個線程共享區(qū)域降狠,很容易理解对竣,我們在寫Java代碼時,每個線程度可以訪問同一個類的靜態(tài)變量對象榜配。由于使用反射機(jī)制的原因否纬,虛擬機(jī)很難推測那個類信息不再使用,因此這塊區(qū)域的回收很難蛋褥。另外临燃,對這塊區(qū)域主要是針對常量池回收,值得注意的是JDK1.7已經(jīng)把常量池轉(zhuǎn)移到堆里面了壁拉。同樣谬俄,當(dāng)方法區(qū)無法滿足內(nèi)存分配需求時柏靶,會拋出OutOfMemoryError弃理。
制造方法區(qū)內(nèi)存溢出,注意屎蜓,必須在JDK1.6及之前版本才會導(dǎo)致方法區(qū)溢出痘昌,原因后面解釋,執(zhí)行之前,可以把虛擬機(jī)的參數(shù)-XXpermSize和-XX:MaxPermSize限制方法區(qū)大小
List<String> list =new ArrayList<String>();
int i =0;
while(true){
list.add(String.valueOf(i).intern());
}
運(yùn)行后會拋出java.lang.OutOfMemoryError:PermGen space異常炬转。
解釋一下辆苔,String的intern()函數(shù)作用是如果當(dāng)前的字符串在常量池中不存在,則放入到常量池中扼劈。上面的代碼不斷將字符串添加到常量池驻啤,最終肯定會導(dǎo)致內(nèi)存不足,拋出方法區(qū)的OOM荐吵。
下面解釋一下骑冗,為什么必須將上面的代碼在JDK1.6之前運(yùn)行。我們前面提到先煎,JDK1.7后贼涩,把常量池放入到堆空間中,這導(dǎo)致intern()函數(shù)的功能不同薯蝎,具體怎么個不同法遥倦,且看看下面代碼:
String str1 =new StringBuilder("ad").append("dc").toString();
System.out.println(str1.intern()==str1);
String str2=new StringBuilder("ja").append("va").toString();
System.out.println(str2.intern()==str2);
這段代碼在JDK1.6和JDK1.7運(yùn)行的結(jié)果不同。JDK1.6結(jié)果是:false,false 占锯,JDK1.7結(jié)果是true, false袒哥。原因是:JDK1.6中缩筛,intern()方法會吧首次遇到的字符串實例復(fù)制到常量池中,返回的也是常量池中的字符串的引用堡称,而StringBuilder創(chuàng)建的字符串實例是在堆上面歪脏,所以必然不是同一個引用,返回false粮呢。在JDK1.7中婿失,intern不再復(fù)制實例,常量池中只保存首次出現(xiàn)的實例的引用啄寡,因此intern()返回的引用和由StringBuilder創(chuàng)建的字符串實例是同一個豪硅。為什么對str2比較返回的是false呢?這是因為挺物,JVM中內(nèi)部在加載類的時候懒浮,就已經(jīng)有"java"這個字符串,不符合“首次出現(xiàn)”的原則识藤,因此返回false砚著。
垃圾回收(GC)
JVM的垃圾回收機(jī)制中,判斷一個對象是否死亡痴昧,并不是根據(jù)是否還有對象對其有引用稽穆,而是通過可達(dá)性分析。對象之間的引用可以抽象成樹形結(jié)構(gòu)赶撰,通過樹根(GC Roots)作為起點(diǎn)舌镶,從這些樹根往下搜索,搜索走過的鏈稱為引用鏈豪娜,當(dāng)一個對象到GC Roots沒有任何引用鏈相連時餐胀,則證明這個對象是不可用的,該對象會被判定為可回收的對象瘤载。
那么那些對象可作為GC Roots呢否灾?主要有以下幾種:
1.虛擬機(jī)棧(棧幀中的本地變量表)中引用的對象。
2.方法區(qū)中類靜態(tài)屬性引用的對象鸣奔。
3.方法區(qū)中常量引用的對象
4.本地方法棧中JNI(即一般說的Native方法)引用的對象墨技。
另外,Java還提供了軟引用和弱引用溃蔫,這兩個引用是可以隨時被虛擬機(jī)回收的對象健提,我們將一些比較占內(nèi)存但是又可能后面用的對象,比如Bitmap對象伟叛,可以聲明為軟引用貨弱引用私痹。但是注意一點(diǎn),每次使用這個對象時候,需要顯示判斷一下是否為null紊遵,以免出錯账千。
三種常見的垃圾收集算法
1.標(biāo)記-清除算法
首先,通過可達(dá)性分析將可回收的對象進(jìn)行標(biāo)記暗膜,標(biāo)記后再統(tǒng)一回收所有被標(biāo)記的對象匀奏,標(biāo)記過程其實就是可達(dá)性分析的過程。這種方法有2個不足點(diǎn):效率問題学搜,標(biāo)記和清除兩個過程的效率都不高娃善;另一個是空間問題,標(biāo)記清除之后會產(chǎn)生大量的不連續(xù)的內(nèi)存碎片瑞佩。
2.復(fù)制算法
為了解決效率問題聚磺,復(fù)制算法是將內(nèi)存分為大小相同的兩塊,每次只使用其中一塊炬丸。當(dāng)這塊內(nèi)存用完了瘫寝,就將還存活的對象復(fù)制到另一塊內(nèi)存上面。然后再把已經(jīng)使用過的內(nèi)存一次清理掉稠炬。這使得每次只對半個區(qū)域進(jìn)行垃圾回收焕阿,內(nèi)存分配時也不用考慮內(nèi)存碎片情況。
但是首启,這代價實在是讓人無法接受暮屡,需要犧牲一般的內(nèi)存空間。研究發(fā)現(xiàn)闽坡,大部分對象都是“朝生夕死”栽惶,所以不需要安裝1:1比例劃分內(nèi)存空間愁溜,而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間疾嗅,每次使用Eden空間和一塊Survivor空間,默認(rèn)比例為Eden:Survivor=8:1.新生代區(qū)域就是這么劃分冕象,每次實例在Eden和一塊Survivor中分配代承,回收時,將存活的對象復(fù)制到剩下的另一塊Survivor渐扮。這樣只有10%的內(nèi)存會被浪費(fèi)论悴,但是帶來的效率卻很高。當(dāng)剩下的Survivor內(nèi)存不足時墓律,可以去老年代內(nèi)存進(jìn)行分配擔(dān)保膀估。如何理解分配擔(dān)保呢,其實就是耻讽,內(nèi)存不足時察纯,去老年代內(nèi)存空間分配,然后等新生代內(nèi)存緩過來了之后,把內(nèi)存歸還給老年代饼记,保持新生代中的Eden:Survivor=8:1.另外香伴,兩個Survivor分別有自己的名稱:From Survivor、To Survivor具则。二者身份經(jīng)常調(diào)換即纲,即有時這塊內(nèi)存與Eden一起參與分配,有時是另一塊博肋。因為他們之間經(jīng)常相互復(fù)制低斋。
3.標(biāo)記-整理算法
標(biāo)記整理算法很簡單,就是先標(biāo)記需要回收的對象匪凡,然后把所有存活的對象移動到內(nèi)存的一端拔稳。這樣的好處是避免了內(nèi)存碎片。
類加載機(jī)制
類從被加載到虛擬機(jī)內(nèi)存開始锹雏,到卸載出內(nèi)存為止巴比,整個生命周期包括:加載、驗證、準(zhǔn)備、解析泥彤、初始化幻捏、使用和卸載七個階段。
其中加載丈氓、驗證、準(zhǔn)備、初始化奸远、和卸載這5個階段的順序是確定的。而解析階段不一定:它在某些情況下可以在初始化階段之后再開始讽挟,這是為了支持Java的運(yùn)行時綁定懒叛。
關(guān)于初始化:JVM規(guī)范明確規(guī)定,有且只有5中情況必須執(zhí)行對類的初始化(加載耽梅、驗證薛窥、準(zhǔn)備自然再此之前要發(fā)生):
1.遇到new、getstatic眼姐、putstatic诅迷、invokestatic,如果類沒有初始化众旗,則必須初始化罢杉,這幾條指令分別是指:new新對象、讀取靜態(tài)變量贡歧、設(shè)置靜態(tài)變量滩租,調(diào)用靜態(tài)函數(shù)拱镐。
2.使用java.lang.reflect包的方法對類進(jìn)行反射調(diào)用時,如果類沒初始化持际,則需要初始化
3.當(dāng)初始化一個類時沃琅,如果發(fā)現(xiàn)父類沒有初始化,則需要先觸發(fā)父類初始化蜘欲。
4.當(dāng)虛擬機(jī)啟動時益眉,用戶需要制定一個執(zhí)行的主類(包含main函數(shù)的類),虛擬機(jī)會先初始化這個類姥份。
5.但是用JDK1.7啟的動態(tài)語言支持時郭脂,如果一個MethodHandle實例最后解析的結(jié)果是REF_getStatic、REF_putStatic澈歉、Ref_invokeStatic的方法句柄時展鸡,并且這個方法句柄所對應(yīng)的類沒有進(jìn)行初始化,則要先觸發(fā)其初始化埃难。
另外要注意的是:通過子類來引用父類的靜態(tài)字段莹弊,不會導(dǎo)致子類初始化:
public class SuperClass{
public static int value=123;
static{
System.out.printLn("SuperClass init!");
}
}
public class SubClass extends SuperClass{
static{
System.out.println("SubClass init!");
}
}
public class Test{
public static void main(String[] args){
System.out.println(SubClass.value);
}
}
最后只會打印:SuperClass init!
對應(yīng)靜態(tài)變量涡尘,只有直接定義這個字段的類才會被初始化忍弛,因此通過子類類引用父類中定義的靜態(tài)變量只會觸發(fā)父類初始化而不會觸發(fā)子類初始化。
通過數(shù)組定義來引用類考抄,不會觸發(fā)此類的初始化:
public class Test{
public static void main(String[] args){
SuperClass[] sca=new SuperClass[10];
}
}
常量會在編譯階段存入調(diào)用者的常量池细疚,本質(zhì)上并沒有直接引用到定義常量的類,因此不會觸發(fā)定義常量的類初始化川梅,示例代碼如下:
public class ConstClass{
public static final String HELLO_WORLD="hello world";
static {
System.out.println("ConstClass init!");
}
}
public class Test{
public static void main(String[] args){
System.out.print(ConstClass.HELLO_WORLD);
}
}
上面代碼不會出現(xiàn)ConstClass init!
加載
加載過程主要做以下3件事
1.通過一個類的全限定名稱來獲取此類的二進(jìn)制流
2.將這個字節(jié)流所代表的靜態(tài)存儲結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時數(shù)據(jù)結(jié)構(gòu)
3.在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)訪問入口疯兼。
驗證
這個階段主要是為了確保Class文件字節(jié)流中包含信息符合當(dāng)前虛擬機(jī)的要求,并且不會出現(xiàn)危害虛擬機(jī)自身的安全
準(zhǔn)備
準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段贫途,這些變量所使用的內(nèi)存都在方法區(qū)中分配吧彪。首先,這個時候分配內(nèi)存僅僅包括類變量(被static修飾的變量)潮饱,而不包括實例變量来氧。實例變量會在對象實例化時隨著對象一起分配在java堆中。其次這里所說的初始值“通常情況下”是數(shù)據(jù)類型的零值香拉,假設(shè)一個類變量定義為
public static int value=123;
那變量value在準(zhǔn)備階段后的初始值是0,而不是123中狂,因為還沒有執(zhí)行任何Java方法凫碌,而把value賦值為123是在程序編譯后,存放在類構(gòu)造函數(shù)<clinit>()方法中胃榕。
解析
解析階段是把虛擬機(jī)中常量池的符號引用替換為直接引用的過程盛险。
初始化
類初始化時類加載的最后一步瞄摊,前面類加載過程中,除了加載階段用戶可以通過自定義類加載器參與以外苦掘,其余動作都是虛擬機(jī)主導(dǎo)和控制换帜。到了初始化階段,才是真正執(zhí)行類中定義Java程序代碼鹤啡。
準(zhǔn)備階段中惯驼,變量已經(jīng)賦過一次系統(tǒng)要求的初始值,而在初始化階段递瑰,根據(jù)程序員通過程序制定的主觀計劃初始化類變量祟牲。初始化過程其實是執(zhí)行類構(gòu)造器<clinit>()方法的過程。
<clinit>()方法是由編譯器自動收集類中所有類變量的賦值動作和靜態(tài)語句塊中的語句合并產(chǎn)生的抖部。收集的順序是按照語句在源文件中出現(xiàn)的順序说贝。靜態(tài)語句塊中只能訪問定義在靜態(tài)語句塊之前的變量,定義在它之后的變量可以賦值慎颗,但不能訪問乡恕。如下所示:
public class Test{
static{
i=0;//給變量賦值,可以通過編譯
System.out.print(i);//這句編譯器會提示:“非法向前引用”
}
static int i=1;
}
<clinit>()方法與類構(gòu)造函數(shù)(或者說實例構(gòu)造器<init>())不同俯萎,他不需要顯式地調(diào)用父類構(gòu)造器几颜,虛擬機(jī)會保證子類的<clinit>()方法執(zhí)行之前,父類的<clinit>()已經(jīng)執(zhí)行完畢讯屈。