Java虛擬機內存詳解


概述

Java虛擬機會自動管理內存,不容易出現內存泄漏和內存溢出問題。Java虛擬機會在執行過程中將管理的內存分為若干個不同的數據區域。

運行時數據區域

在jdk1.8之前的版本與1.8版本略有不同,在jdk1.8之前:

jdk1.8:

以上圖片來源:https://github.com/LikFre/JavaGuide

 線程共享區域:

    1.堆

    2.方法區

    3.直接內存(非運行時數據區)

線程私有區域:

    1.虛擬機棧

    2.本地方法棧

    3.程序計數器

線程私有區域:

1.虛擬機棧

  1.它的生命周期隨着線程的創建而創建,隨着線程的結束而死亡;

  2.描述的是java方法執行的內存模型,java虛擬機棧是由一個個棧幀組成,每個棧幀都有局部變量表、動態鏈接、操作數棧、方法出口信息。

  3.局部變量表主要存放可知的各種數據類型(8種基本數據類型變量,對象引用變量);

  4.Java虛擬機棧是線程私有的,每個線程都有自己的虛擬機棧;

  5.java虛擬機棧中主要存放的是一個個棧幀,每調用一次方法都會有對應的棧幀壓入虛擬機棧,每一個方法執行結束后,都會有一個棧幀彈出。

    Java方法有兩種返回方式:1、return;

                 2、拋出異常;

    這兩種方式都會導致棧幀彈出;

  java虛擬機棧會出現兩種異常:StackOverFlowError和OutOfMemoryError :

    1.StackOverFlowError:如果java虛擬機棧的內存大小不允許動態擴展,當虛擬機棧請求的內存超過棧的最大內存時就會出現StackOverFlowError異常;

    2.OutOfMemoryError:如果java虛擬機棧的內存大小允許動態擴展,當線程請求棧的內存已用完,且無法再動態擴展時,拋出OutOfMemoryError異常;

2.本地方法棧

  1.與java虛擬機棧的生命周期相同,都是線程私有,隨着線程的創建而創建,線程的結束而死亡。

  2.描述的是Native方法執行的內存模型,也是由棧幀組成,每個棧幀用於存放本地方法的局部變量表,操作數棧、動態鏈接、方法出口信息;

  3.也會出現兩種異常:StackOverFlowError和OutOfMemoryError

  在HotSpot虛擬機中,虛擬機棧與本地方法棧合二為一;

3.程序計數器

  1.程序計數器是一塊較小的內存空間,字節碼解釋器通過改變計數器的值來選取下一條字節碼指令。分支、跳轉、循環、異常處理、線程恢復都需要依賴程序計數器;

  2.程序計數器也是線程私有的,每個線程都有自己的程序計數器;

  3.程序計數器主要有兩個作用:

    1.字節碼解釋器通過改變程序計數器的值來順序的執行字節碼指令;如:順序執行、循環、跳轉等;

    2.在多線程環境下,程序計數器用於記錄當前線程執行的位置,當CPU切換再次執行當前線程時從程序計數器記錄的位置繼續執行;

  注意:程序計數器是唯一一個不會出現OutOfMemoryError異常的內存區域,它的生命周期隨着線程的創建而創建,線程的結束而死亡;

線程共享區域:

1.堆

  1.java虛擬機管理的最大的一塊內存區域,是所有線程共享的內存區域,隨着虛擬機的啟動而創建,主要用於存放對象實例,幾乎所有的對象實例和數組都在堆中存儲

  2.堆也是垃圾收集器主要管理的區域,因此也被稱為GC堆(Garbage Collected Heap);從垃圾回收的角度:由於現在的收集器都采用分代收集算法,堆又被分為:新生代和老年代,進一步分為Eden空間、From  Survivor、ToSurvivor。進一步划分目的是為了更好的回收內存或者更好的分配內存。

  

  3.上圖中,Eden、S0、S1是新生代,TenTired是老年代。大部分情況,一個對象實例創建后存儲在Eden區域,經歷過一次垃圾回收之后,如果對象還存活,對象的年齡加+1(由Eden區進入到Survivor區),進入S0或者S1,當對象的年齡達到一定的閾值(默認是15),這個對象才會進入老年代區域,對象的閾值可以通過參數(-XX:MaxTenuringThreshold)來設置;

2.方法區

  1.與java堆一樣,是線程共享的內存區域,主要用於存儲加載過的類信息、被final修飾的常量、靜態變量以及即時編譯器編譯的代碼,雖然java虛擬機規范把方法區列為堆的一個邏輯部分,但是它卻有一個別名NON-Heap(非堆),可能就是為了和堆區分開。

  2.方法區也被稱為永久代,方法區是java虛擬機中的一種規范,並沒有去實現它,在不同的JVM方法區的實現也不同,永久代是HotSpot的概念,在HotSpot虛擬機中永久代是對方法區的一種實現,其他的虛擬機並沒有永久代這一說法。

  3.在jdk1.8之前,方法區還沒有被移除,通過兩個參數可以設置永久代的大小:

    -XX:PermSize=N //方法區 (永久代) 初始大小

    -XX:MaxPermSize=N //方法區 (永久代) 最大大小,超過這個值將會拋出 OutOfMemoryError 異常

  垃圾收集行為在這個區域很少出現,但並不是數據進入方法區就一直存在

  4.jdk1.8版本,方法區(永久代)被移除,取而代之的時元空間,使用的是直接內存,可以通過以下兩個參數設置

     -XX:MetaspaceSize=N //設置 Metaspace 的初始(和最小大小)

     -XX:MaxMetaspaceSize=//設置 Metaspace 的最大大小

        與方法區最大的的不同就是,如果不指定大小,隨着更多的類創建,虛擬機可能會耗盡所有可用的系統內存  

    5.為什么要用元空間替換永久代?

  整個永久代有JVM設置的固定大小上限,無法進行調整,而元空間使用的是直接內存,只受本機可用內存大小的限制,如果不指定最大元空間大小,-XX:MaxMetaspaceSize默認大小是unlimited,只受到本機可用內存大小限制,如果不指定初始大小,JVM會動態的分配應用程序所需要的大小。這只是其中一個原因,有興趣的可以去查閱資料。

3.運行時常量池

  1.是方法區的一部分,主要用來存放編譯器生成的各種字面量和引用。字面量(文本字符串、基本數據類型的值、聲明為final的常量值、其他)

  2.常量池的大小也受到方法區大小的限制,超過常量池的大小會報OutOfMemoryError異常。

  3.jdk1.7及以后JVM將運行時常量池從方法區移出,在堆中開辟了一塊區域用來存放運行時常量池。

直接內存:

  不屬於運行時數據區,也不是虛擬機規范中定義的內存區域,但是被頻繁的使用,也有可能引起OutOfMemoryError異常。

  jdk1.4中新加入的NIO,使用一種基於通道(Channel)和緩存(Buffer)的方式,直接使用Native方法分配堆外內存,通過在java堆中一個DirectByteBuffer對象作為這塊內存的引用來操作,避免了java堆與Native堆之間來回復制數據,提高了性能。

HotSpot虛擬機:

1.對象的創建:

Step1:類加載檢查,虛擬機遇到一條new指令時,首先檢查指令的參數是否能在常量池中定位到這個類的符號引用,然后檢查符號引用代表的類是否被加載、解析和初始化過,沒有則執行相應的類加載過程。

Step2:分配內存,類加載檢查通過后,虛擬機為新生的對象分配內存,在類加載完成后已經確定了對象的大小,分配內存實際就是在堆中分配一塊確定大小的內存,分配內存有兩種方式:指針碰撞空閑列表

    分配內存的方式取決於java堆內存是否規整,java堆內存是否規整取決於GC收集算法是“標記-清理”還是“標記-整理”;

    1.指針碰撞:堆內存規整,使用過的內存全部整合到一邊,沒有使用的內存在另一邊,中間有一個分界值指針,只需要將指針往沒有使用的內存方向移動對象內存大小即可。GC收集器:Serial、ParNew;

    2.空閑列表:堆內存不規整,虛擬機會維護一個列表,列表中記錄未使用的內存大小,選擇一塊足夠的內存來為對象分配,更新列表的記錄。GC收集器:CMS

     分配內存的並發問題:對象的創建是很頻繁的,虛擬機要保證線程的安全性,采用兩種方式:

    1.CAS+失敗重試:CAS是樂觀鎖的一種方式,每次認為不會有沖突的去執行某項操作,遇到沖突一直重試,知道成功為止。虛擬機使用CAS加失敗重試的方式來保證更新操作的原子性。

    2.TLAB:為每一個線程預先在Eden區分配一塊內存,線程為對象分配內存時,首先在TLAB中分配,TLAB中內存不夠時或者已經用盡時,再使用CAS上述方式。

Step3:初始化零值,內存分配完成后,需要將分配到的內存空間初始化為零值,這一步保證了對象在不初始化的情況下,對象的實例字段在java代碼中可以不賦初始值就使用。

Step4:設置對象頭,初始化零值完成之后,虛擬機要對對象進行必要的設置,列如:這個對象是哪個類的實例、如何才能找到對象的元數據、對象的哈希碼、對象的GC分代年齡等信息,這些信息存儲在對象頭中。

Step5:執行init方法,在new出對象之后,把對象按照程序員的意願進行初始化,執行init方法。

2.對象在內存中的布局:

對象在內存中的布局可分為三個區域:對象頭、實例數據和對齊填充

1.對象頭分為兩部分:一部分存儲自身的運行時數據(哈希碼、GC分代年齡等),另一部分類型指針,通過指針來確定這個對象是哪個類的實例

2.實例數據:是真正存儲對象的有效信息,對象的各種類型的字段內容。

3.對齊填充:不是必然存在的只是起到占位作用。HotSpot虛擬機的自動內存管理系統要求對象的起始地址必須是8字節的整數倍,前兩部分不足8字節的整數倍時,使用對齊填充補足。

3.對象的訪問定位:

創建對象就是為了使用對象,通過在棧上存儲的引用類型變量來操作堆上的具體類型變量,對象的訪問方式由虛擬機決定,目前主流的有兩種:①使用句柄 ②直接指針

1.使用句柄java堆中會有一塊內存作為句柄池,棧中的引用類型變量中保存的是句柄池的句柄,句柄池中有對象的實例數據和對象類型信息的地址,也就是引用變量訪問句柄池,句柄池再訪問對象。

2.直接指針:棧中的引用類型變量中直接保存的是對象的地址,可以直接訪問。

使用句柄訪問的好處是,變量中保存的是穩定的句柄池的地址,在對象被移動時改變句柄池中的地址即可,變量保存的地址不需要改變。

使用直接指針的好處是:速度快,節省了一次指針定位的時間。

內容補充:

String類與常量池

String對象的兩種創建方式:

String str1 = "Hello" ;
String str2 = new String("Hello");
String str3 = new String("Hello");
System.out.println(str1 == str2);//false
System.out.println(str2 == str3);//false

 

 str1.先檢查常量池中有沒有“Hello”,沒有就在常量池中創建,人后str1指向常量池中“Hello”的地址,常量池中已經有的話,str1直接指向常量池中“Hello”的地址。

str2,在堆中重新創建一個新的對象

第一種方式:在常量池中拿對象,

第二種方式:直接在堆內存中創建新的對象

String類型的常量池有兩種使用方式

 

        String str1 = new String("Hello" );
        String str2 = str1.intern();
        String str3 = "Hello";
        System.out.println(str1 == str2);//true
        System.out.println(str2 == str3);//false    

 

 

 

上述代碼中 str2和str3指向的都是字符串常量池中的“Hello”

第一種方式:使用雙引號聲明的String對象會直接存儲在常量池中

第二種方式:使用String提供的intern方法,intern()方法是一個本地方法,它的作用是:如果當前字符串對象的字符串已經在常量池中,那么直接返回常量池中該字符串的引用,如果當前字符串對象的字符串內容不在常量池中,那么在常量池中創建一個字符串,返回該字符串的引用。

字符串拼接:

        String str1 = "hel";
        String str2 = "lo";
        String str3 = "hel"+"lo";
        String str4 = str1 + str2;
        String str5 = "hello";
        System.out.println(str3 == str4);//false
        System.out.println(str4 == str5);//false
        System.out.println(str3 == str5);//true 

 

str4是在堆上新建的對象,str3與str5都是常量池中的“hello”

8種基本類型的包裝類與常量池

java基本類型的包裝類中有六種實現的常量池技術:Byte、Short、Integer、Long、Character、Boolean,前五種默認使用[-128,127]的緩存數據,超出這個范圍才會創建對象。Double和Float沒有實現常量池技術

Integer  i1 = 44; 默認使用Integer.valueOf(44),從而使用常量池中的數據,只有超過【-128,127】才會在對中創建對象。  

Integer i2 = new Integer(44);這種直接在堆中創建對象,

 Integer i1 = 40;
  Integer i2 = 40;
  Integer i3 = 0;
  Integer i4 = new Integer(40);
  Integer i5 = new Integer(40);
  Integer i6 = new Integer(0);
  
  System.out.println("i1=i2   " + (i1 == i2));//true
  System.out.println("i1=i2+i3   " + (i1 == i2 + i3));//true
  System.out.println("i1=i4   " + (i1 == i4));//false
  System.out.println("i4=i5   " + (i4 == i5));//false
  System.out.println("i4=i5+i6   " + (i4 == i5 + i6));//true   
  System.out.println("40=i5+i6   " + (40 == i5 + i6));//true

 i5+i6這個操作符“+”不適用於Integer對象,所以i5與i6自動拆箱,加之后就變為:i4=40,又因為i4是Integer對象,無法與int對象比較,i4自動拆箱為int值為40,所以相等

 原文地址:https://github.com/LikFre/JavaGuide/blob/master/docs/java/jvm/Java%E5%86%85%E5%AD%98%E5%8C%BA%E5%9F%9F.md


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM