我的CSDN博客同步發(fā)布:JVM理解其實(shí)并不難丐箩!
在閱讀本文之前摇邦,先向大家強(qiáng)烈推薦一下周志明的《深入理解Java虛擬機(jī)》這本書(shū)。
前些天面試了阿里的實(shí)習(xí)生雏蛮,問(wèn)到關(guān)于Dalvik虛擬機(jī)能不能執(zhí)行class文件涎嚼,我當(dāng)時(shí)的回答是不能阱州,但是它執(zhí)行的是class轉(zhuǎn)換的dex文件挑秉。當(dāng)面試官繼續(xù)問(wèn),為什么不能執(zhí)行class文件時(shí)苔货,我卻只能回答Dalvik虛擬機(jī)內(nèi)部的優(yōu)化原因犀概,卻不能正確回答具體的原因。其實(shí)周志明的這本書(shū)就有回答:Dakvik并不是一個(gè)Java虛擬機(jī)夜惭,它沒(méi)有遵循Java虛擬機(jī)規(guī)范姻灶,不能執(zhí)行Java的class文件,使用的是寄存器架構(gòu)而不是JVM中常見(jiàn)的棧架構(gòu)诈茧,但是它與Java又有著千絲萬(wàn)縷的關(guān)系产喉,它執(zhí)行的dex文件可以通過(guò)class文件轉(zhuǎn)化而來(lái)。
其實(shí)在本科期間敢会,就有接觸過(guò)《深入理解Java虛擬機(jī)》曾沈,但是一直以來(lái)都沒(méi)去仔細(xì)研讀,現(xiàn)在回頭想想實(shí)在是覺(jué)得可惜鸥昏!研一期間花了不少時(shí)間研讀塞俱,現(xiàn)在準(zhǔn)備找工作了,發(fā)現(xiàn)好多內(nèi)容看了又忘吏垮。索性寫(xiě)一篇文章障涯,把這本書(shū)的知識(shí)點(diǎn)做一個(gè)總結(jié)。當(dāng)然了膳汪,如果你想看比較詳細(xì)的內(nèi)容唯蝶,可以翻看《深入理解Java虛擬機(jī)》。
JVM內(nèi)存區(qū)域
我們?cè)诰帉?xiě)程序時(shí)遗嗽,經(jīng)常會(huì)遇到OOM(out of Memory)以及內(nèi)存泄漏等問(wèn)題粘我。為了避免出現(xiàn)這些問(wèn)題,我們首先必須對(duì)JVM的內(nèi)存劃分有個(gè)具體的認(rèn)識(shí)媳谁。JVM將內(nèi)存主要?jiǎng)澐譃椋悍椒▍^(qū)涂滴、虛擬機(jī)棧、本地方法棧晴音、堆柔纵、程序計(jì)數(shù)器。JVM運(yùn)行時(shí)數(shù)據(jù)區(qū)如下:
程序計(jì)數(shù)器
程序計(jì)數(shù)器是線程私有的區(qū)域锤躁,很好理解嘛~搁料,每個(gè)線程當(dāng)然得有個(gè)計(jì)數(shù)器記錄當(dāng)前執(zhí)行到那個(gè)指令。占用的內(nèi)存空間小,可以把它看成是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號(hào)指示器郭计。如果線程在執(zhí)行Java方法霸琴,這個(gè)計(jì)數(shù)器記錄的是正在執(zhí)行的虛擬機(jī)字節(jié)碼指令地址;如果執(zhí)行的是Native方法昭伸,這個(gè)計(jì)數(shù)器的值為空(Undefined)梧乘。此內(nèi)存區(qū)域是唯一一個(gè)在Java虛擬機(jī)規(guī)范中沒(méi)有規(guī)定任何OutOfMemoryError情況的區(qū)域。
Java虛擬機(jī)棧
與程序計(jì)數(shù)器一樣庐杨,Java虛擬機(jī)棧也是線程私有的选调。其生命周期與線程相同。如何理解虛擬機(jī)棧呢灵份?本質(zhì)上來(lái)講仁堪,就是個(gè)棧。里面存放的元素叫棧幀填渠,棧幀好像很復(fù)雜的樣子弦聂,其實(shí)它很簡(jiǎn)單!它里面存放的是一個(gè)函數(shù)的上下文氛什,具體存放的是執(zhí)行的函數(shù)的一些數(shù)據(jù)莺葫。執(zhí)行的函數(shù)需要的數(shù)據(jù)無(wú)非就是局部變量表(保存函數(shù)內(nèi)部的變量)、操作數(shù)棧(執(zhí)行引擎計(jì)算時(shí)需要)屉更,方法出口等等徙融。
執(zhí)行引擎每調(diào)用一個(gè)函數(shù)時(shí),就為這個(gè)函數(shù)創(chuàng)建一個(gè)棧幀瑰谜,并加入虛擬機(jī)棧欺冀。換個(gè)角度理解,每個(gè)函數(shù)從調(diào)用到執(zhí)行結(jié)束萨脑,其實(shí)是對(duì)應(yīng)一個(gè)棧幀的入棧和出棧隐轩。
注意這個(gè)區(qū)域可能出現(xiàn)的兩種異常:一種是StackOverflowError,當(dāng)前線程請(qǐng)求的棧深度大于虛擬機(jī)所允許的深度時(shí)渤早,會(huì)拋出這個(gè)異常职车。制造這種異常很簡(jiǎn)單:將一個(gè)函數(shù)反復(fù)遞歸自己,最終會(huì)出現(xiàn)棧溢出錯(cuò)誤(StackOverflowError)鹊杖。另一種異常是OutOfMemoryError異常悴灵,當(dāng)虛擬機(jī)棧可以動(dòng)態(tài)擴(kuò)展時(shí)(當(dāng)前大部分虛擬機(jī)都可以)骂蓖,如果無(wú)法申請(qǐng)足夠多的內(nèi)存就會(huì)拋出OutOfMemoryError积瞒,如何制作虛擬機(jī)棧OOM呢,參考一下代碼:
public void stackLeakByThread(){
while(true){
new Thread(){
public void run(){
while(true){
}
}
}.start()
}
}
這段代碼有風(fēng)險(xiǎn)登下,可能會(huì)導(dǎo)致操作系統(tǒng)假死茫孔,請(qǐng)謹(jǐn)慎使用~~~
本地方法棧
本地方法棧與虛擬機(jī)棧所發(fā)揮的作用很相似叮喳,他們的區(qū)別在于虛擬機(jī)棧為執(zhí)行Java代碼方法服務(wù),而本地方法棧是為Native方法服務(wù)缰贝。與虛擬機(jī)棧一樣馍悟,本地方法棧也會(huì)拋出StackOverflowError和OutOfMemoryError異常。
Java堆
Java堆可以說(shuō)是虛擬機(jī)中最大一塊內(nèi)存了剩晴。它是所有線程所共享的內(nèi)存區(qū)域锣咒,幾乎所有的實(shí)例對(duì)象都是在這塊區(qū)域中存放。當(dāng)然李破,睡著JIT編譯器的發(fā)展宠哄,所有對(duì)象在堆上分配漸漸變得不那么“絕對(duì)”了。
Java堆是垃圾收集器管理的主要區(qū)域嗤攻。由于現(xiàn)在的收集器基本上采用的都是分代收集算法,所有Java堆可以細(xì)分為:新生代和老年代诽俯。在細(xì)致分就是把新生代分為:Eden空間妇菱、From Survivor空間、To Survivor空間暴区。當(dāng)堆無(wú)法再擴(kuò)展時(shí)闯团,會(huì)拋出OutOfMemoryError異常。
方法區(qū)
方法區(qū)存放的是類(lèi)信息仙粱、常量房交、靜態(tài)變量等。方法區(qū)是各個(gè)線程共享區(qū)域伐割,很容易理解候味,我們?cè)趯?xiě)Java代碼時(shí),每個(gè)線程度可以訪問(wèn)同一個(gè)類(lèi)的靜態(tài)變量對(duì)象隔心。由于使用反射機(jī)制的原因白群,虛擬機(jī)很難推測(cè)那個(gè)類(lèi)信息不再使用,因此這塊區(qū)域的回收很難硬霍。另外帜慢,對(duì)這塊區(qū)域主要是針對(duì)常量池回收,值得注意的是JDK1.7已經(jīng)把常量池轉(zhuǎn)移到堆里面了唯卖。同樣粱玲,當(dāng)方法區(qū)無(wú)法滿足內(nèi)存分配需求時(shí),會(huì)拋出OutOfMemoryError拜轨。
制造方法區(qū)內(nèi)存溢出抽减,注意,必須在JDK1.6及之前版本才會(huì)導(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)行后會(huì)拋出java.lang.OutOfMemoryError:PermGen space
異常。
解釋一下偎箫,String
的intern()
函數(shù)作用是如果當(dāng)前的字符串在常量池中不存在木柬,則放入到常量池中。上面的代碼不斷將字符串添加到常量池淹办,最終肯定會(huì)導(dǎo)致內(nèi)存不足眉枕,拋出方法區(qū)的OOM。
下面解釋一下怜森,為什么必須將上面的代碼在JDK1.6之前運(yùn)行速挑。我們前面提到,JDK1.7后副硅,把常量池放入到堆空間中姥宝,這導(dǎo)致intern()
函數(shù)的功能不同,具體怎么個(gè)不同法恐疲,且看看下面代碼:
String str1 =new StringBuilder("hua").append("chao").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()
方法會(huì)吧首次遇到的字符串實(shí)例復(fù)制到常量池中,返回的也是常量池中的字符串的引用省咨,而StringBuilder
創(chuàng)建的字符串實(shí)例是在堆上面肃弟,所以必然不是同一個(gè)引用,返回false
零蓉。在JDK1.7中笤受,intern
不再?gòu)?fù)制實(shí)例,常量池中只保存首次出現(xiàn)的實(shí)例的引用壁公,因此intern()
返回的引用和由StringBuilder
創(chuàng)建的字符串實(shí)例是同一個(gè)感论。為什么對(duì)str2比較返回的是false呢?這是因?yàn)槲刹幔琂VM中內(nèi)部在加載類(lèi)的時(shí)候比肄,就已經(jīng)有"java"
這個(gè)字符串,不符合“首次出現(xiàn)”的原則囊陡,因此返回false
芳绩。
垃圾回收(GC)
JVM的垃圾回收機(jī)制中,判斷一個(gè)對(duì)象是否死亡撞反,并不是根據(jù)是否還有對(duì)象對(duì)其有引用妥色,而是通過(guò)可達(dá)性分析。對(duì)象之間的引用可以抽象成樹(shù)形結(jié)構(gòu)遏片,通過(guò)樹(shù)根(GC Roots)作為起點(diǎn)嘹害,從這些樹(shù)根往下搜索撮竿,搜索走過(guò)的鏈稱為引用鏈,當(dāng)一個(gè)對(duì)象到GC Roots沒(méi)有任何引用鏈相連時(shí)笔呀,則證明這個(gè)對(duì)象是不可用的幢踏,該對(duì)象會(huì)被判定為可回收的對(duì)象。
那么那些對(duì)象可作為GC Roots呢许师?主要有以下幾種:
1.虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象房蝉。
2.方法區(qū)中類(lèi)靜態(tài)屬性引用的對(duì)象。
3.方法區(qū)中常量引用的對(duì)象
4.本地方法棧中JNI(即一般說(shuō)的Native方法)引用的對(duì)象微渠。
另外搭幻,Java還提供了軟引用和弱引用,這兩個(gè)引用是可以隨時(shí)被虛擬機(jī)回收的對(duì)象逞盆,我們將一些比較占內(nèi)存但是又可能后面用的對(duì)象檀蹋,比如Bitmap對(duì)象,可以聲明為軟引用貨弱引用纳击。但是注意一點(diǎn)续扔,每次使用這個(gè)對(duì)象時(shí)候,需要顯示判斷一下是否為null
焕数,以免出錯(cuò)。
三種常見(jiàn)的垃圾收集算法
1.標(biāo)記-清除算法
首先刨啸,通過(guò)可達(dá)性分析將可回收的對(duì)象進(jìn)行標(biāo)記堡赔,標(biāo)記后再統(tǒng)一回收所有被標(biāo)記的對(duì)象,標(biāo)記過(guò)程其實(shí)就是可達(dá)性分析的過(guò)程设联。這種方法有2個(gè)不足點(diǎn):效率問(wèn)題善已,標(biāo)記和清除兩個(gè)過(guò)程的效率都不高;另一個(gè)是空間問(wèn)題离例,標(biāo)記清除之后會(huì)產(chǎn)生大量的不連續(xù)的內(nèi)存碎片换团。
2.復(fù)制算法
為了解決效率問(wèn)題,復(fù)制算法是將內(nèi)存分為大小相同的兩塊宫蛆,每次只使用其中一塊艘包。當(dāng)這塊內(nèi)存用完了,就將還存活的對(duì)象復(fù)制到另一塊內(nèi)存上面耀盗。然后再把已經(jīng)使用過(guò)的內(nèi)存一次清理掉想虎。這使得每次只對(duì)半個(gè)區(qū)域進(jìn)行垃圾回收,內(nèi)存分配時(shí)也不用考慮內(nèi)存碎片情況叛拷。
但是舌厨,這代價(jià)實(shí)在是讓人無(wú)法接受,需要犧牲一般的內(nèi)存空間忿薇。研究發(fā)現(xiàn)裙椭,大部分對(duì)象都是“朝生夕死”躏哩,所以不需要安裝1:1比例劃分內(nèi)存空間,而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間揉燃,每次使用Eden空間和一塊Survivor空間扫尺,默認(rèn)比例為Eden:Survivor=8:1.新生代區(qū)域就是這么劃分,每次實(shí)例在Eden和一塊Survivor中分配你雌,回收時(shí)器联,將存活的對(duì)象復(fù)制到剩下的另一塊Survivor。這樣只有10%的內(nèi)存會(huì)被浪費(fèi)婿崭,但是帶來(lái)的效率卻很高拨拓。當(dāng)剩下的Survivor內(nèi)存不足時(shí),可以去老年代內(nèi)存進(jìn)行分配擔(dān)保氓栈。如何理解分配擔(dān)保呢渣磷,其實(shí)就是,內(nèi)存不足時(shí)授瘦,去老年代內(nèi)存空間分配醋界,然后等新生代內(nèi)存緩過(guò)來(lái)了之后,把內(nèi)存歸還給老年代提完,保持新生代中的Eden:Survivor=8:1.另外形纺,兩個(gè)Survivor分別有自己的名稱:From Survivor、To Survivor徒欣。二者身份經(jīng)常調(diào)換逐样,即有時(shí)這塊內(nèi)存與Eden一起參與分配,有時(shí)是另一塊打肝。因?yàn)樗麄冎g經(jīng)常相互復(fù)制脂新。
3.標(biāo)記-整理算法
標(biāo)記整理算法很簡(jiǎn)單,就是先標(biāo)記需要回收的對(duì)象粗梭,然后把所有存活的對(duì)象移動(dòng)到內(nèi)存的一端争便。這樣的好處是避免了內(nèi)存碎片。
類(lèi)加載機(jī)制
類(lèi)從被加載到虛擬機(jī)內(nèi)存開(kāi)始断医,到卸載出內(nèi)存為止滞乙,整個(gè)生命周期包括:加載、驗(yàn)證孩锡、準(zhǔn)備酷宵、解析、初始化躬窜、使用和卸載七個(gè)階段浇垦。
其中加載、驗(yàn)證荣挨、準(zhǔn)備男韧、初始化朴摊、和卸載這5個(gè)階段的順序是確定的。而解析階段不一定:它在某些情況下可以在初始化階段之后再開(kāi)始此虑,這是為了支持Java的運(yùn)行時(shí)綁定甚纲。
關(guān)于初始化:JVM規(guī)范明確規(guī)定,有且只有5中情況必須執(zhí)行對(duì)類(lèi)的初始化(加載朦前、驗(yàn)證介杆、準(zhǔn)備自然再此之前要發(fā)生):
1.遇到new、getstatic韭寸、putstatic春哨、invokestatic,如果類(lèi)沒(méi)有初始化恩伺,則必須初始化赴背,這幾條指令分別是指:new新對(duì)象、讀取靜態(tài)變量晶渠、設(shè)置靜態(tài)變量凰荚,調(diào)用靜態(tài)函數(shù)。
2.使用java.lang.reflect包的方法對(duì)類(lèi)進(jìn)行反射調(diào)用時(shí)褒脯,如果類(lèi)沒(méi)初始化便瑟,則需要初始化
3.當(dāng)初始化一個(gè)類(lèi)時(shí),如果發(fā)現(xiàn)父類(lèi)沒(méi)有初始化番川,則需要先觸發(fā)父類(lèi)初始化胳徽。
4.當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶需要制定一個(gè)執(zhí)行的主類(lèi)(包含main函數(shù)的類(lèi))爽彤,虛擬機(jī)會(huì)先初始化這個(gè)類(lèi)。
5.但是用JDK1.7啟的動(dòng)態(tài)語(yǔ)言支持時(shí)缚陷,如果一個(gè)MethodHandle實(shí)例最后解析的結(jié)果是REF_getStatic
适篙、REF_putStatic
、Ref_invokeStatic
的方法句柄時(shí)箫爷,并且這個(gè)方法句柄所對(duì)應(yīng)的類(lèi)沒(méi)有進(jìn)行初始化嚷节,則要先觸發(fā)其初始化。
另外要注意的是:通過(guò)子類(lèi)來(lái)引用父類(lèi)的靜態(tài)字段虎锚,不會(huì)導(dǎo)致子類(lèi)初始化:
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);
}
}
最后只會(huì)打恿蛱怠:SuperClass init!
對(duì)應(yīng)靜態(tài)變量,只有直接定義這個(gè)字段的類(lèi)才會(huì)被初始化窜护,因此通過(guò)子類(lèi)類(lèi)引用父類(lèi)中定義的靜態(tài)變量只會(huì)觸發(fā)父類(lèi)初始化而不會(huì)觸發(fā)子類(lèi)初始化效斑。
通過(guò)數(shù)組定義來(lái)引用類(lèi),不會(huì)觸發(fā)此類(lèi)的初始化:
public class Test{
public static void main(String[] args){
SuperClass[] sca=new SuperClass[10];
}
}
常量會(huì)在編譯階段存入調(diào)用者的常量池柱徙,本質(zhì)上并沒(méi)有直接引用到定義常量的類(lèi)缓屠,因此不會(huì)觸發(fā)定義常量的類(lèi)初始化奇昙,示例代碼如下:
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);
}
}
上面代碼不會(huì)出現(xiàn)ConstClass init!
加載
加載過(guò)程主要做以下3件事
1.通過(guò)一個(gè)類(lèi)的全限定名稱來(lái)獲取此類(lèi)的二進(jìn)制流
2.強(qiáng)這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
3.在內(nèi)存中生成一個(gè)代表這個(gè)類(lèi)的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類(lèi)的各種數(shù)據(jù)訪問(wèn)入口。
驗(yàn)證
這個(gè)階段主要是為了確保Class文件字節(jié)流中包含信息符合當(dāng)前虛擬機(jī)的要求敌完,并且不會(huì)出現(xiàn)危害虛擬機(jī)自身的安全储耐。
準(zhǔn)備
準(zhǔn)備階段是正式為類(lèi)變量分配內(nèi)存并設(shè)置類(lèi)變量初始值的階段,這些變量所使用的內(nèi)存都在方法區(qū)中分配滨溉。首先什湘,這個(gè)時(shí)候分配內(nèi)存僅僅包括類(lèi)變量(被static修飾的變量),而不包括實(shí)例變量晦攒。實(shí)例變量會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在java堆中闽撤。其次這里所說(shuō)的初始值“通常情況下”是數(shù)據(jù)類(lèi)型的零值,假設(shè)一個(gè)類(lèi)變量定義為
public static int value=123;
那變量value在準(zhǔn)備階段后的初始值是0勤家,而不是123腹尖,因?yàn)檫€沒(méi)有執(zhí)行任何Java方法,而把value賦值為123是在程序編譯后伐脖,存放在類(lèi)構(gòu)造函數(shù)<clinit>()
方法中热幔。
解析
解析階段是把虛擬機(jī)中常量池的符號(hào)引用替換為直接引用的過(guò)程。
初始化
類(lèi)初始化時(shí)類(lèi)加載的最后一步讼庇,前面類(lèi)加載過(guò)程中绎巨,除了加載階段用戶可以通過(guò)自定義類(lèi)加載器參與以外,其余動(dòng)作都是虛擬機(jī)主導(dǎo)和控制蠕啄。到了初始化階段场勤,才是真正執(zhí)行類(lèi)中定義Java程序代碼。
準(zhǔn)備階段中歼跟,變量已經(jīng)賦過(guò)一次系統(tǒng)要求的初始值和媳,而在初始化階段,根據(jù)程序員通過(guò)程序制定的主觀計(jì)劃初始化類(lèi)變量哈街。初始化過(guò)程其實(shí)是執(zhí)行類(lèi)構(gòu)造器<clinit>()
方法的過(guò)程留瞳。
<clinit>()
方法是由編譯器自動(dòng)收集類(lèi)中所有類(lèi)變量的賦值動(dòng)作和靜態(tài)語(yǔ)句塊中的語(yǔ)句合并產(chǎn)生的。收集的順序是按照語(yǔ)句在源文件中出現(xiàn)的順序骚秦。靜態(tài)語(yǔ)句塊中只能訪問(wèn)定義在靜態(tài)語(yǔ)句塊之前的變量她倘,定義在它之后的變量可以賦值,但不能訪問(wèn)作箍。如下所示:
public class Test{
static{
i=0;//給變量賦值硬梁,可以通過(guò)編譯
System.out.print(i);//這句編譯器會(huì)提示:“非法向前引用”
}
static int i=1;
}
<clinit>()
方法與類(lèi)構(gòu)造函數(shù)(或者說(shuō)實(shí)例構(gòu)造器<init>()
)不同,他不需要顯式地調(diào)用父類(lèi)構(gòu)造器胞得,虛擬機(jī)會(huì)保證子類(lèi)的<clinit>()
方法執(zhí)行之前荧止,父類(lèi)的<clinit>()
已經(jīng)執(zhí)行完畢。
類(lèi)加載器
關(guān)于自定義類(lèi)加載器,和雙親委派模型罩息,這里不再提嗤详,寫(xiě)了幾個(gè)小時(shí)了,該洗洗睡了~