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

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

尼河口網(wǎng)站制作公司哪家好,找創(chuàng)新互聯(lián)建站!從網(wǎng)頁設計、網(wǎng)站建設、微信開發(fā)、APP開發(fā)、成都響應式網(wǎng)站建設公司等網(wǎng)站項目制作,到程序開發(fā),運營維護。創(chuàng)新互聯(lián)建站于2013年開始到現(xiàn)在10年的時間,我們擁有了豐富的建站經(jīng)驗和運維經(jīng)驗,來保證我們的工作的順利進行。專注于網(wǎng)站建設就選創(chuàng)新互聯(lián)建站。

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

我們在編寫程序時,經(jīng)常會遇到OOM(out of Memory)以及內(nèi)存泄漏等問題。為了避免出現(xiàn)這些問題,我們首先必須對JVM的內(nèi)存劃分有個具體的認識。JVM將內(nèi)存主要劃分為:方法區(qū)、虛擬機棧、本地方法棧、堆、程序計數(shù)器。JVM運行時數(shù)據(jù)區(qū)如下:

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

程序計數(shù)器

程序計數(shù)器是線程私有的區(qū)域,很好理解嘛~,每個線程當然得有個計數(shù)器記錄當前執(zhí)行到那個指令。占用的內(nèi)存空間小,可以把它看成是當前線程所執(zhí)行的字節(jié)碼的行號指示器。如果線程在執(zhí)行Java方法,這個計數(shù)器記錄的是正在執(zhí)行的虛擬機字節(jié)碼指令地址;如果執(zhí)行的是Native方法,這個計數(shù)器的值為空(Undefined)。此內(nèi)存區(qū)域是唯一一個在Java虛擬機規(guī)范中沒有規(guī)定任何OutOfMemoryError情況的區(qū)域。

Java虛擬機棧

與程序計數(shù)器一樣,Java虛擬機棧也是線程私有的。其生命周期與線程相同。如何理解虛擬機棧呢?本質(zhì)上來講,就是個棧。里面存放的元素叫棧幀,棧幀好像很復雜的樣子,其實它很簡單!它里面存放的是一個函數(shù)的上下文,具體存放的是執(zhí)行的函數(shù)的一些數(shù)據(jù)。執(zhí)行的函數(shù)需要的數(shù)據(jù)無非就是局部變量表(保存函數(shù)內(nèi)部的變量)、操作數(shù)棧(執(zhí)行引擎計算時需要),方法出口等等。

執(zhí)行引擎每調(diào)用一個函數(shù)時,就為這個函數(shù)創(chuàng)建一個棧幀,并加入虛擬機棧。換個角度理解,每個函數(shù)從調(diào)用到執(zhí)行結(jié)束,其實是對應一個棧幀的入棧和出棧。

注意這個區(qū)域可能出現(xiàn)的兩種異常:一種是StackOverflowError,當前線程請求的棧深度大于虛擬機所允許的深度時,會拋出這個異常。制造這種異常很簡單:將一個函數(shù)反復遞歸自己,最終會出現(xiàn)棧溢出錯誤(StackOverflowError)。另一種異常是OutOfMemoryError異常,當虛擬機??梢詣討B(tài)擴展時(當前大部分虛擬機都可以),如果無法申請足夠多的內(nèi)存就會拋出OutOfMemoryError,如何制作虛擬機棧OOM呢,參考一下代碼:


  1. public void stackLeakByThread(){ 

  2. while(true){ 

  3. new Thread(){ 

  4. public void run(){ 

  5. while(true){ 

  6. }.start() 

這段代碼有風險,可能會導致操作系統(tǒng)假死,請謹慎使用~~~

本地方法棧

本地方法棧與虛擬機棧所發(fā)揮的作用很相似,他們的區(qū)別在于虛擬機棧為執(zhí)行Java代碼方法服務,而本地方法棧是為Native方法服務。與虛擬機棧一樣,本地方法棧也會拋出StackOverflowError和OutOfMemoryError異常。

Java堆

Java堆可以說是虛擬機中最大一塊內(nèi)存了。它是所有線程所共享的內(nèi)存區(qū)域,幾乎所有的實例對象都是在這塊區(qū)域中存放。當然,睡著JIT編譯器的發(fā)展,所有對象在堆上分配漸漸變得不那么“絕對”了。

Java堆是垃圾收集器管理的主要區(qū)域。由于現(xiàn)在的收集器基本上采用的都是分代收集算法,所有Java堆可以細分為:新生代和老年代。在細致分就是把新生代分為:Eden空間、From Survivor空間、To Survivor空間。當堆無法再擴展時,會拋出OutOfMemoryError異常。

方法區(qū)

方法區(qū)存放的是類信息、常量、靜態(tài)變量等。方法區(qū)是各個線程共享區(qū)域,很容易理解,我們在寫Java代碼時,每個線程度可以訪問同一個類的靜態(tài)變量對象。由于使用反射機制的原因,虛擬機很難推測那個類信息不再使用,因此這塊區(qū)域的回收很難。另外,對這塊區(qū)域主要是針對常量池回收,值得注意的是JDK1.7已經(jīng)把常量池轉(zhuǎn)移到堆里面了。同樣,當方法區(qū)無法滿足內(nèi)存分配需求時,會拋出OutOfMemoryError。

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


  1. List list =new ArrayList(); 

  2. int i =0; 

  3. while(true){ 

  4. list.add(String.valueOf(i).intern()); 

運行后會拋出java.lang.OutOfMemoryError:PermGen space異常。

解釋一下,String的intern()函數(shù)作用是如果當前的字符串在常量池中不存在,則放入到常量池中。上面的代碼不斷將字符串添加到常量池,最終肯定會導致內(nèi)存不足,拋出方法區(qū)的OOM。

下面解釋一下,為什么必須將上面的代碼在JDK1.6之前運行。我們前面提到,JDK1.7后,把常量池放入到堆空間中,這導致intern()函數(shù)的功能不同,具體怎么個不同法,且看看下面代碼:


  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運行的結(jié)果不同。JDK1.6結(jié)果是:false,false ,JDK1.7結(jié)果是true, false。原因是:JDK1.6中,intern()方法會吧首次遇到的字符串實例復制到常量池中,返回的也是常量池中的字符串的引用,而StringBuilder創(chuàng)建的字符串實例是在堆上面,所以必然不是同一個引用,返回false。在JDK1.7中,intern不再復制實例,常量池中只保存首次出現(xiàn)的實例的引用,因此intern()返回的引用和由StringBuilder創(chuàng)建的字符串實例是同一個。為什么對str2比較返回的是false呢?這是因為,JVM中內(nèi)部在加載類的時候,就已經(jīng)有"java"這個字符串,不符合“首次出現(xiàn)”的原則,因此返回false。

垃圾回收(GC)

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

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

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

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

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

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

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

三種常見的垃圾收集算法

1.標記-清除算法

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

2.復制算法

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

但是,這代價實在是讓人無法接受,需要犧牲一般的內(nèi)存空間。研究發(fā)現(xiàn),大部分對象都是“朝生夕死”,所以不需要安裝1:1比例劃分內(nèi)存空間,而是將內(nèi)存分為一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden空間和一塊Survivor空間,默認比例為Eden:Survivor=8:1.新生代區(qū)域就是這么劃分,每次實例在Eden和一塊Survivor中分配,回收時,將存活的對象復制到剩下的另一塊Survivor。這樣只有10%的內(nèi)存會被浪費,但是帶來的效率卻很高。當剩下的Survivor內(nèi)存不足時,可以去老年代內(nèi)存進行分配擔保。如何理解分配擔保呢,其實就是,內(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)常相互復制。

3.標記-整理算法

標記整理算法很簡單,就是先標記需要回收的對象,然后把所有存活的對象移動到內(nèi)存的一端。這樣的好處是避免了內(nèi)存碎片。

類加載機制

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

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

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

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

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

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

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

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

另外要注意的是:通過子類來引用父類的靜態(tà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); 

最后只會打?。篠uperClass init!

對應靜態(tài)變量,只有直接定義這個字段的類才會被初始化,因此通過子類類引用父類中定義的靜態(tài)變量只會觸發(fā)父類初始化而不會觸發(fā)子類初始化。

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


  1. public class Test{ 

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

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

常量會在編譯階段存入調(diào)用者的常量池,本質(zhì)上并沒有直接引用到定義常量的類,因此不會觸發(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); 

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

加載

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

1.通過一個類的全限定名稱來獲取此類的二進制流

2.強這個字節(jié)流所代表的靜態(tài)存儲結(jié)構轉(zhuǎn)化為方法區(qū)的運行時數(shù)據(jù)結(jié)構

3.在內(nèi)存中生成一個代表這個類的java.lang.Class對象,作為方法區(qū)這個類的各種數(shù)據(jù)訪問入口。

驗證

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

準備

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


  1. public static int value=123; 

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

解析

解析階段是把虛擬機中常量池的符號引用替換為直接引用的過程。

初始化

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

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

()方法是由編譯器自動收集類中所有類變量的賦值動作和靜態(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; 

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

類加載器

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

感謝你能夠認真閱讀完這篇文章,希望小編分享的“JVM內(nèi)存區(qū)域的示例分析”這篇文章對大家有幫助,同時也希望大家多多支持創(chuàng)新互聯(lián),關注創(chuàng)新互聯(lián)行業(yè)資訊頻道,更多相關知識等著你來學習!

新聞標題:JVM內(nèi)存區(qū)域的示例分析
URL鏈接:http://muchs.cn/article30/jejgpo.html

成都網(wǎng)站建設公司_創(chuàng)新互聯(lián),為您提供軟件開發(fā)、做網(wǎng)站外貿(mào)網(wǎng)站建設、網(wǎng)站策劃、建站公司、網(wǎng)站維護

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權請盡快告知,我們將會在第一時間刪除。文章觀點不代表本網(wǎng)站立場,如需處理請聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時需注明來源: 創(chuàng)新互聯(lián)

h5響應式網(wǎng)站建設