溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

JVM內(nèi)存區(qū)域的示例分析

發(fā)布時(shí)間:2022-01-14 11:03:56 來源:億速云 閱讀:121 作者:小新 欄目:軟件技術(shù)

這篇文章主要介紹了JVM內(nèi)存區(qū)域的示例分析,具有一定借鑒價(jià)值,感興趣的朋友可以參考下,希望大家閱讀完這篇文章之后大有收獲,下面讓小編帶著大家一起了解一下。

JVM內(nèi)存區(qū)域

我們?cè)诰帉懗绦驎r(shí),經(jīng)常會(huì)遇到OOM(out of Memory)以及內(nèi)存泄漏等問題。為了避免出現(xià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ū)如下:

JVM內(nèi)存區(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ī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。

Java虛擬機(jī)棧

與程序計(jì)數(shù)器一樣,Java虛擬機(jī)棧也是線程私有的。其生命周期與線程相同。如何理解虛擬機(jī)棧呢?本質(zhì)上來講,就是個(gè)棧。里面存放的元素叫棧幀,棧幀好像很復(fù)雜的樣子,其實(shí)它很簡(jiǎn)單!它里面存放的是一個(gè)函數(shù)的上下文,具體存放的是執(zhí)行的函數(shù)的一些數(shù)據(jù)。執(zhí)行的函數(shù)需要的數(shù)據(jù)無非就是局部變量表(保存函數(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ī)都可以),如果無法申請(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堆可以說是虛擬機(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)堆無法再擴(kuò)展時(shí),會(huì)拋出OutOfMemoryError異常。

方法區(qū)

方法區(qū)存放的是類信息、常量、靜態(tài)變量等。方法區(qū)是各個(gè)線程共享區(qū)域,很容易理解,我們?cè)趯慗ava代碼時(shí),每個(gè)線程度可以訪問同一個(gè)類的靜態(tài)變量對(duì)象。由于使用反射機(jī)制的原因,虛擬機(jī)很難推測(cè)那個(gè)類信息不再使用,因此這塊區(qū)域的回收很難。另外,對(duì)這塊區(qū)域主要是針對(duì)常量池回收,值得注意的是JDK1.7已經(jīng)把常量池轉(zhuǎn)移到堆里面了。同樣,當(dāng)方法區(qū)無法滿足內(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)?,JVM中內(nèi)部在加載類的時(shí)候,就已經(jīng)有"java"這個(gè)字符串,不符合“首次出現(xiàn)”的原則,因此返回false。

垃圾回收(GC)

JVM的垃圾回收機(jī)制中,判斷一個(gè)對(duì)象是否死亡,并不是根據(jù)是否還有對(duì)象對(duì)其有引用,而是通過可達(dá)性分析。對(duì)象之間的引用可以抽象成樹形結(jié)構(gòu),通過樹根(GC Roots)作為起點(diǎn),從這些樹根往下搜索,搜索走過的鏈稱為引用鏈,當(dāng)一個(gè)對(duì)象到GC Roots沒有任何引用鏈相連時(shí),則證明這個(gè)對(duì)象是不可用的,該對(duì)象會(huì)被判定為可回收的對(duì)象。

那么那些對(duì)象可作為GC Roots呢?主要有以下幾種:

1.虛擬機(jī)棧(棧幀中的本地變量表)中引用的對(duì)象。

2.方法區(qū)中類靜態(tài)屬性引用的對(duì)象。

3.方法區(qū)中常量引用的對(duì)象

4.本地方法棧中JNI(即一般說的Native方法)引用的對(duì)象。

另外,Java還提供了軟引用和弱引用,這兩個(gè)引用是可以隨時(shí)被虛擬機(jī)回收的對(duì)象,我們將一些比較占內(nèi)存但是又可能后面用的對(duì)象,比如Bitmap對(duì)象,可以聲明為軟引用貨弱引用。但是注意一點(diǎn),每次使用這個(gè)對(duì)象時(shí)候,需要顯示判斷一下是否為null,以免出錯(cuò)。

三種常見的垃圾收集算法

1.標(biāo)記-清除算法

首先,通過可達(dá)性分析將可回收的對(duì)象進(jìn)行標(biāo)記,標(biāo)記后再統(tǒng)一回收所有被標(biāo)記的對(duì)象,標(biāo)記過程其實(shí)就是可達(dá)性分析的過程。這種方法有2個(gè)不足點(diǎn):效率問題,標(biāo)記和清除兩個(gè)過程的效率都不高;另一個(gè)是空間問題,標(biāo)記清除之后會(huì)產(chǎn)生大量的不連續(xù)的內(nèi)存碎片。

2.復(fù)制算法

為了解決效率問題,復(fù)制算法是將內(nèi)存分為大小相同的兩塊,每次只使用其中一塊。當(dāng)這塊內(nèi)存用完了,就將還存活的對(duì)象復(fù)制到另一塊內(nèi)存上面。然后再把已經(jīng)使用過的內(nèi)存一次清理掉。這使得每次只對(duì)半個(gè)區(qū)域進(jìn)行垃圾回收,內(nèi)存分配時(shí)也不用考慮內(nèi)存碎片情況。

但是,這代價(jià)實(shí)在是讓人無法接受,需要犧牲一般的內(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),但是帶來的效率卻很高。當(dāng)剩下的Survivor內(nèi)存不足時(shí),可以去老年代內(nèi)存進(jìn)行分配擔(dān)保。如何理解分配擔(dān)保呢,其實(shí)就是,內(nèi)存不足時(shí),去老年代內(nèi)存空間分配,然后等新生代內(nè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)存碎片。

類加載機(jī)制

類從被加載到虛擬機(jī)內(nèi)存開始,到卸載出內(nèi)存為止,整個(gè)生命周期包括:加載、驗(yàn)證、準(zhǔn)備、解析、初始化、使用和卸載七個(gè)階段。

其中加載、驗(yàn)證、準(zhǔn)備、初始化、和卸載這5個(gè)階段的順序是確定的。而解析階段不一定:它在某些情況下可以在初始化階段之后再開始,這是為了支持Java的運(yùn)行時(shí)綁定。

關(guān)于初始化:JVM規(guī)范明確規(guī)定,有且只有5中情況必須執(zhí)行對(duì)類的初始化(加載、驗(yàn)證、準(zhǔn)備自然再此之前要發(fā)生):

1.遇到new、getstatic、putstatic、invokestatic,如果類沒有初始化,則必須初始化,這幾條指令分別是指:new新對(duì)象、讀取靜態(tài)變量、設(shè)置靜態(tài)變量,調(diào)用靜態(tài)函數(shù)。

2.使用java.lang.reflect包的方法對(duì)類進(jìn)行反射調(diào)用時(shí),如果類沒初始化,則需要初始化

3.當(dāng)初始化一個(gè)類時(shí),如果發(fā)現(xiàn)父類沒有初始化,則需要先觸發(fā)父類初始化。

4.當(dāng)虛擬機(jī)啟動(dòng)時(shí),用戶需要制定一個(gè)執(zhí)行的主類(包含main函數(shù)的類),虛擬機(jī)會(huì)先初始化這個(gè)類。

5.但是用JDK1.7啟的動(dòng)態(tài)語言支持時(shí),如果一個(gè)MethodHandle實(shí)例最后解析的結(jié)果是REF_getStatic、REF_putStatic、Ref_invokeStatic的方法句柄時(shí),并且這個(gè)方法句柄所對(duì)應(yīng)的類沒有進(jìn)行初始化,則要先觸發(fā)其初始化。

另外要注意的是:通過子類來引用父類的靜態(tài)字段,不會(huì)導(dǎo)致子類初始化:


  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è)字段的類才會(huì)被初始化,因此通過子類類引用父類中定義的靜態(tài)變量只會(huì)觸發(fā)父類初始化而不會(huì)觸發(fā)子類初始化。

通過數(shù)組定義來引用類,不會(huì)觸發(fā)此類的初始化:


  1. public class Test{ 

  2. public static void main(String[] args){ 

  3. SuperClass[] sca=new SuperClass[10]; 

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


  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!

加載

加載過程主要做以下3件事

1.通過一個(gè)類的全限定名稱來獲取此類的二進(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è)類的java.lang.Class對(duì)象,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)訪問入口。

驗(yàn)證

這個(gè)階段主要是為了確保Class文件字節(jié)流中包含信息符合當(dāng)前虛擬機(jī)的要求,并且不會(huì)出現(xiàn)危害虛擬機(jī)自身的安全。

準(zhǔn)備

準(zhǔn)備階段是正式為類變量分配內(nèi)存并設(shè)置類變量初始值的階段,這些變量所使用的內(nèi)存都在方法區(qū)中分配。首先,這個(gè)時(shí)候分配內(nèi)存僅僅包括類變量(被static修飾的變量),而不包括實(shí)例變量。實(shí)例變量會(huì)在對(duì)象實(shí)例化時(shí)隨著對(duì)象一起分配在java堆中。其次這里所說的初始值“通常情況下”是數(shù)據(jù)類型的零值,假設(shè)一個(gè)類變量定義為


  1. public static int value=123; 

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

解析

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

初始化

類初始化時(shí)類加載的最后一步,前面類加載過程中,除了加載階段用戶可以通過自定義類加載器參與以外,其余動(dòng)作都是虛擬機(jī)主導(dǎo)和控制。到了初始化階段,才是真正執(zhí)行類中定義Java程序代碼。

準(zhǔn)備階段中,變量已經(jīng)賦過一次系統(tǒng)要求的初始值,而在初始化階段,根據(jù)程序員通過程序制定的主觀計(jì)劃初始化類變量。初始化過程其實(shí)是執(zhí)行類構(gòu)造器()方法的過程。

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


  1. public class Test{ 

  2. static{ 

  3. i=0; 

  4. System.out.print(i); 

  5. static int i=1; 

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

類加載器

關(guān)于自定義類加載器,和雙親委派模型,這里不再提。

感謝你能夠認(rèn)真閱讀完這篇文章,希望小編分享的“JVM內(nèi)存區(qū)域的示例分析”這篇文章對(duì)大家有幫助,同時(shí)也希望大家多多支持億速云,關(guān)注億速云行業(yè)資訊頻道,更多相關(guān)知識(shí)等著你來學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

jvm
AI