日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢(xún)
選擇下列產(chǎn)品馬上在線(xiàn)溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問(wèn)題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷(xiāo)解決方案
JVM理解其實(shí)并不難!

JVM 理解其實(shí)并不難!

作者:Javaspring思維導(dǎo)圖 2019-03-11 16:24:04

云計(jì)算

虛擬化 前些天面試了阿里的實(shí)習(xí)生,問(wèn)到關(guān)于Dalvik虛擬機(jī)能不能執(zhí)行class文件,我當(dāng)時(shí)的回答是不能,但是它執(zhí)行的是class轉(zhuǎn)換的dex文件。

前些天面試了阿里的實(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)。

[[259214]]

其實(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ù)器是線(xiàn)程私有的區(qū)域,很好理解嘛~,每個(gè)線(xiàn)程當(dāng)然得有個(gè)計(jì)數(shù)器記錄當(dāng)前執(zhí)行到那個(gè)指令。占用的內(nèi)存空間小,可以把它看成是當(dāng)前線(xiàn)程所執(zhí)行的字節(jié)碼的行號(hào)指示器。如果線(xiàn)程在執(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ī)棧也是線(xiàn)程私有的。其生命周期與線(xiàn)程相同。如何理解虛擬機(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)前線(xiàn)程請(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呢,參考一下代碼:

  
 
 
 
  1. public void stackLeakByThread(){ 
  2. while(true){ 
  3. new Thread(){ 
  4. public void run(){ 
  5. while(true){ 
  6. }.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)存了。它是所有線(xiàn)程所共享的內(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è)線(xiàn)程共享區(qū)域,很容易理解,我們?cè)趯?xiě)Java代碼時(shí),每個(gè)線(xiàn)程度可以訪(fǎng)問(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ú)法滿(mǎn)足內(nèi)存分配需求時(shí),會(huì)拋出OutOfMemoryError。

制造方法區(qū)內(nèi)存溢出,注意,必須在JDK1.6及之前版本才會(huì)導(dǎo)致方法區(qū)溢出,原因后面解釋,執(zhí)行之前,可以把虛擬機(jī)的參數(shù)-XXpermSize和-XX:MaxPermSize限制方法區(qū)大小。

  
 
 
 
  1. List list =new ArrayList(); 
  2. int i =0; 
  3. while(true){ 
  4. 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è)不同法,且看看下面代碼:

  
 
 
 
  1. String str1 =new StringBuilder("hua").append("chao").toString(); 
  2. System.out.println(str1.intern()==str1); 
  3. String str2=new StringBuilder("ja").append("va").toString(); 
  4. 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ò)的鏈稱(chēng)為引用鏈,當(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分別有自己的名稱(chēng):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í),用戶(hù)需要制定一個(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)初始化:

  
 
 
 
  1. public class SuperClass{ 
  2. public static int value=123; 
  3. static{ 
  4. System.out.printLn("SuperClass init!"); 
  5. public class SubClass extends SuperClass{ 
  6. static{ 
  7. System.out.println("SubClass init!"); 
  8. public class Test{ 
  9. public static void main(String[] args){ 
  10. System.out.println(SubClass.value); 

***只會(huì)打?。篠uperClass 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)的初始化:

  
 
 
 
  1. public class Test{ 
  2. public static void main(String[] args){ 
  3. SuperClass[] sca=new SuperClass[10]; 

常量會(huì)在編譯階段存入調(diào)用者的常量池,本質(zhì)上并沒(méi)有直接引用到定義常量的類(lèi),因此不會(huì)觸發(fā)定義常量的類(lèi)初始化,示例代碼如下:

  
 
 
 
  1. public class ConstClass{ 
  2. public static final String HELLO_WORLD="hello world"; 
  3. static { 
  4. System.out.println("ConstClass init!"); 
  5. public class Test{ 
  6. public static void main(String[] args){ 
  7. System.out.print(ConstClass.HELLO_WORLD); 

上面代碼不會(huì)出現(xiàn)ConstClass init!

加載

加載過(guò)程主要做以下3件事

1.通過(guò)一個(gè)類(lèi)的全限定名稱(chēng)來(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ù)訪(fǎng)問(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)變量定義為

  
 
 
 
  1. public static int value=123; 

那變量value在準(zhǔn)備階段后的初始值是0,而不是123,因?yàn)檫€沒(méi)有執(zhí)行任何Java方法,而把value賦值為123是在程序編譯后,存放在類(lèi)構(gòu)造函數(shù)()方法中。

解析

解析階段是把虛擬機(jī)中常量池的符號(hào)引用替換為直接引用的過(guò)程。

初始化

類(lèi)初始化時(shí)類(lèi)加載的***一步,前面類(lèi)加載過(guò)程中,除了加載階段用戶(hù)可以通過(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)造器()方法的過(guò)程。

()方法是由編譯器自動(dòng)收集類(lèi)中所有類(lèi)變量的賦值動(dòng)作和靜態(tài)語(yǔ)句塊中的語(yǔ)句合并產(chǎn)生的。收集的順序是按照語(yǔ)句在源文件中出現(xiàn)的順序。靜態(tài)語(yǔ)句塊中只能訪(fǎng)問(wèn)定義在靜態(tài)語(yǔ)句塊之前的變量,定義在它之后的變量可以賦值,但不能訪(fǎng)問(wèn)。如下所示:

  
 
 
 
  1. public class Test{ 
  2. static{ 
  3. i=0; 
  4. System.out.print(i); 
  5. static int i=1; 

()方法與類(lèi)構(gòu)造函數(shù)(或者說(shuō)實(shí)例構(gòu)造器())不同,他不需要顯式地調(diào)用父類(lèi)構(gòu)造器,虛擬機(jī)會(huì)保證子類(lèi)的()方法執(zhí)行之前,父類(lèi)的()已經(jīng)執(zhí)行完畢。

類(lèi)加載器

關(guān)于自定義類(lèi)加載器,和雙親委派模型,這里不再提,寫(xiě)了幾個(gè)小時(shí)了,該洗洗睡了~


當(dāng)前題目:JVM理解其實(shí)并不難!
網(wǎng)站路徑:http://www.5511xx.com/article/dhdjdjg.html