JVM: JVM 內存划分


概述

如果在大學里學過或者在工作中使用過 C 或者 C++ 的讀者一定會發現這兩門語言的內存管理機制與 Java 的不同。在使用 C 或者 C++ 編程時,程序員需要手動的去管理和維護內存,就是說需要手動的清除那些不需要的對象,否則就會出現內存泄漏與內存溢出的問題。

如果你使用 Java 語言去開發,你就會發現大多數情況下你不用去關心無用對象的回收與內存的管理,因為這一切 JVM 虛擬機已經幫我們做好了。了解 JVM 內存的各個區域將有助於我們深入了解它的管理機制,避免出現內存相關的問題和高效的解決問題。

引出問題

在 Java 編程時我們會用到許多不同類型的數據,比如臨時變量、靜態變量、對象、方法、類等等。 那么他們的存儲方式有什么不同嗎?或者說他們存在哪?

運行時數據區域

Java 虛擬機在執行 Java 程序過程中會把它所管理的內存分為若干個不同的數據區域,各自有各自的用途。

img

這其中堆和方法區是線程之間共享的,而棧和程序計數器是線程私有的。

  • 程序計數器

    線程私有的,可以看作是當前線程所執行字節碼的行號指示器。字節碼解釋器工作時就是通過改變這個計數器的值來選取下一條需要執行的字節碼指令。分支、循環、異常處理、線程恢復等基礎功能都需要依賴這個計數器來完成。

    這時唯一一個沒有規定任何 OOM 異常的區域。

  • 虛擬機棧

    虛擬機棧也是線程私有的,生命周期與線程相同。棧里面存儲的是方法的局部變量對象的引用等等。

    在這片區域中,規定了兩種異常情況,當線程請求的棧深度大於虛擬機所允許的深度,將拋出 StackOverflowError 異常。當虛擬機棧動態擴展無法申請到足夠的內存時會拋出 OOM 異常。

  • 本地方法棧

    和虛擬機棧的作用相同,只不過它是為 Native 方法服務。HotSpot 虛擬機直接將虛擬機棧和本地方法棧合二為一了。

  • 堆是 Java 虛擬機所管理內存中最大的一塊。是所有線程共享的一塊內存區域,在虛擬機啟動時創建。這個區域唯一的作用就是存放對象實例,也就是 NEW 出來的對象。這個區域也是 Java 垃圾收集器的主要作用區域。

    當堆的大小再也無法擴展時,將會拋出 OOM 異常。

    可以說,此內存區域唯一的作用就是存放對象實例,幾乎所有的對象實例和數組都在這里分配內存

    Java 堆是垃圾收集管理的主要區域,因此也被稱為 GC 堆。垃圾收集都采用分代垃圾回收算法,所以 Java 堆還可以細分:新聲代(再細致一點分為 Eden,From Survivor,To Survivor)和老年代。進一步划分的目的是跟好地回收內存,或者更快地分配內存。

  • 方法區

    方法區也是線程共享的內存區域,用於存儲已經被虛擬機加載的類信息常量靜態變量等等。當方法區無法滿足內存分配需求時,會拋出 OOM 異常。這個區域也被稱為永久代。

補充

雖然上面的圖里沒有運行時常量池和直接內存,但是這兩部分也是我們開發時經常接觸的。所以給大家補充出來。

  • 運行時常量池

    運行時常量池是方法區的一部分,Class 文件中除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池,用於存放編譯期生成的各種字面量符號引用,這部分內容將在類加載后存放到方法區的運行時常量池中。也會拋出 OOM 異常。

  • 直接內存

    直接內存並不是虛擬機運行時數據區的一部分,也不是 Java 虛擬機規范中定義的內存區域,但是卻是NIO 操作時會直接使用的一塊內存,雖然不受虛擬機參數限制,但是還是會受到本機總內存的限制,會拋出 OOM 異常。

這里有一個概念希望大家能夠清除,堆中使用分代垃圾回收算法時的永久代表方法區,它並不在堆內存中,上面的圖片將其放在一起是為了說明分代垃圾回收算法會作用在這幾個區域

JDK 1.8 的改變

對於方法區,它是線程共享的,主要用於存儲類的信息,常量池,方法數據,方法代碼等。我們稱這個區域為永久代。它也是 JVM 垃圾回收作用的區域。

大部分程序員應該都見過 java.lang.OutOfMemoryError:PermGen space 異常,這里的 PermGen space 其實指的就是方法區。由於方法區主要存儲類的相關信息,所以對於動態生成類的情況比較容易出現永久代的內存溢出,典型的場景是在 JSP 頁面比較多的情況,容易出現永久代內存溢出。

在 JDK 1.8 中,HotSpot 虛擬機已經沒有 PermGen space 方法區這個地方了,取而代之的是一個叫 Metaspace(元空間)的東西。

img

元空間與方法區最大的區別是:元空間不再虛擬機中,而是使用本地內存。默認情況下,元空間的大小僅受本地內存限制。

常量區原本在方法區中,現在方法區被移除了,所以常量池被放倒了堆中。

這樣做的好處是:

這樣更改的好處:

  • 字符串常量存在方法區中,容易出現性能問題和內存溢出。
  • 類和方法的信息等比較難確定大小,因此對於方法區大小的指定比較困難,太小容易出現方法區溢出,太大容易導致堆的空間不足。
  • 方法區的垃圾回收會帶來不必要的復雜度,並且回收效率偏低(垃圾回收會在下一章給大家介紹)。

虛擬機對象揭秘

對象的創建過程,最好是能記住,並且能知道每一步在做什么。

  1. 類加載檢查:虛擬機遇到一條 new 指令的時候,首先去檢查這個指令的參數能否在常量池中定位這個類的符號飲用,檢查這個類的符號引用所代表的類是否已被加載,解析,初始化過。如果沒有,那必須先執行響應的類加載過程。簡單來說,就是要看對象的類是否已經被加載過了

  2. 分配內存:在類加載檢查通過后,接下來虛擬機將會為新生對象分配內存。對象所需的內存大小在類加載完畢后便可以確定了,為對象分配空間的任務相當於把一塊確定大小的內存從 Java 堆中划分出來。

    分配方式有指針碰撞空閑列表兩種。選擇那種方式由 Java 堆是否規整決定,而 Java 堆是否規整由垃圾收集器是否帶有壓縮功能決定(復制算法和標記整理算法是規整的,標記清除算法是不規整的)。

    內存分配並發問題

    • CAS 失敗重試,CAS 是客觀鎖的一種實現方式。
    • TLAB:為每一個線程預先在 Eden 分配一塊內存,JVM 在給線程中的對象分配內存時,首先在 TLAB 分配,如果不夠,使用 CAS 進行分配。
  3. 初始化零值:內存分配完畢后,虛擬機將要分配的內存空間都初始化為零值(不包括對象頭)。這一步保證了對象實例在 Java 中不賦初值就可以直接使用。

  4. 設置對象頭:初始化零值完成之后,虛擬機要對對象進行必要的設置。比如對象的哈希碼,對象的 GC 分代年齡信息,偏向鎖,這些信息放在對象頭中。

  5. 執行 init 方法:上面工作完成后,從虛擬機視角看,一個新的對象已經產生了。然后執行 init 方法,按照程序員的意願將對象進行初始化。

對象構成

HotSpot 虛擬機中,對象在內存中的布局可以分為三塊區域:對象頭,實例數據和對齊填充

對象頭中包含兩部分信息,第一部分用於存儲對象自身運行時數據(哈希碼,GC 分代年齡,鎖狀態標志),另一部分是類型指針,即指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。

實例數據部分存儲的對象的有效信息。

對其填充起到的是占位的作用。

對象的訪問定位

  1. 句柄。
  2. 直接指針。

補充

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

這兩種方式創建的對象是有差別的,第一種方式是在常量池中,第二種方式是在堆內存中。

  • 直接使用雙引號聲明創建出來的 String 對象會直接存儲在常量池中。

  • 如果不是使用常量池聲明的 String 對象,可以使用 String 提供的 intern 方String.intern() 是一個 Native 方法,它的作用是:如果運行時常量池中已經包含一個等於此 String 對象內容的字符串,則返回常量池中該字符串的引用;如果沒有,則在常量池中創建與此 String 內容相同的字符串,並返回常量池中創建的字符串的引用。

    String s1 = new String("計算機");
    String s2 = s1.intern();
    String s3 = "計算機";
    System.out.println(s2);//計算機
    System.out.println(s1 == s2);//false,因為一個是堆內存中的String對象一個是常量池中的String對象,
    System.out.println(s3 == s2);//true,因為兩個都是常量池中的String對
    
    String str1 = "str";
    String str2 = "ing";
    
    String str3 = "str" + "ing";//常量池中的對象
    String str4 = str1 + str2; //在堆上創建的新的對象	  
    String str5 = "string";//常量池中的對象
    System.out.println(str3 == str4);//false
    System.out.println(str3 == str5);//true
    System.out.println(str4 == str5);//false
    

String s1 = new String("abc"); 這句話創建了幾個對象?

先有字符串 “abc” 放入常量池,然后 new 了一個字符串 “abc” 放入 Java 堆。棧中的引用指向堆中的對象。

Java 基本類型的包裝類的大部分都實現了常量池技術,即 Byte,Short,Integer,Long,Character,Boolean。除了 Boolean 之外的 5 種包裝類都默認創建了 【-128 127】的緩存數據,超出此范圍仍然去創建新的對象。Float 和 Double 並沒有實現常量池技術。

Integer i1 = 33;
Integer i2 = 33;
System.out.println(i1 == i2);// 輸出true
Integer i11 = 333;
Integer i22 = 333;
System.out.println(i11 == i22);// 輸出false
Double i3 = 1.2;
Double i4 = 1.2;
System.out.println(i3 == i4);// 輸出false
  1. Integer i1=40;Java 在編譯的時候會直接將代碼封裝成Integer i1=Integer.valueOf(40);,從而使用常量池中的對象。
  2. Integer i1 = new Integer(40);這種情況下會創建新的對象。
Integer i1 = 40;
Integer i2 = new Integer(40);
System.out.println(i1==i2);//輸出false


免責聲明!

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



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