JVM內存結構劃分

JVM內存結構劃分

數據區域劃分

運行時內存區域劃分:程序計數器、虛擬機棧、本地方法棧、堆、方法區java

程序計數器

  • 線程私有
  • 經過寄存器實現
  • 不會存在運行溢出

當前線程所執行的行號指示器,記住下一條JVM指令的執行地址數組

虛擬機棧

  • 垃圾回收不涉及棧內存
  • 棧內存是線程私有的,能夠理解爲線程運行須要的內存空間
  • 棧由棧幀組成,每一個棧幀表明一個方法執行時須要的內存(參數,局部變量,返回地址)
  • 每一個線程只能有一個活動棧幀,對應着當前正在執行的那個方法

棧內存分配過大隻能支撐必定的遞歸調用,並不會影響運行速度,還可能減小線程數量(由於物理內存是必定的)緩存

本地方法棧

爲運行本地方法時分配的內存(HotSpot把虛擬機棧和本地方法棧合二爲一了)安全

  • 有垃圾回收機制
  • 線程共享,須要考慮線程安全問題
  • 存儲的都是對象的實例(經過new關鍵字建立的對象)
  • 從內存分配的角度來講:堆中能夠劃分出多個線程私有的分配緩衝區(TLAB),以提高對象分配時的效率
  • Java堆能夠處於物理上不連續的內存空間,但在邏輯上應該視爲連續的(可是對於好比數組這種大對象,可能會要求連續的內存空間)

方法區

  • 線程共享區
  • 存儲已被虛擬機加載的類型信息,常量,靜態變量,即時編譯器編譯後的代碼緩存
  • 在虛擬機啓動時被建立,邏輯上屬於堆的一部分(不一樣JVM實現的方式不一樣)
    • JDK1.6使用永久代(PerGen)做爲方法區的實現
    • JDK1.8使用元空間(Metaspace)對方法區進行實現(包含Class ClassLoader 常量池三個部分,放在直接內存中)StringTable放在堆中(有助於垃圾回收管理)

使用場景:如Spring Mybatis使用的動態加載數據結構

運行時常量池

運行時常量池是方法區的一部分
二進制字節碼內容:類基本信息\常量池表\類方法定義,包含了虛擬機指令
其中,常量池表中存放編譯期間生成的各類字面量(好比各類基本數據類型)與符號引用(好比,類名\方法名\參數類型),這部份內容將在類加載後存放到方法區的運行時常量池中,並把符號地址變爲真實地址併發

StringTable

相似於hashTable結構,不能自動擴容
常量池中的字符串只是符號,第一次使用時才變爲對象
利用串池機制,避免重複建立字符對象app

  • 案例
    字符串拼接的原理是編譯期優化
    字符串拼接原理是StringBuilder(JDK1.8)
    使用intern()方法,主動將串池中尚未的字符串對象放入串池
// StringTable [ "a", "b" ,"ab" ]  hashtable 結構,不能擴容
public class Demo1_22 {
    // 常量池中的信息,都會被加載到運行時常量池中, 這時 a b ab 都是常量池中的符號,尚未變爲 java 字符串對象
    // ldc #2 會把 a 符號變爲 "a" 字符串對象
    // ldc #3 會把 b 符號變爲 "b" 字符串對象
    // ldc #4 會把 ab 符號變爲 "ab" 字符串對象

    public static void main(String[] args) {
        String s1 = "a"; // 懶惰的
        String s2 = "b";
        String s3 = "ab";
        String s4 = s1 + s2; // new StringBuilder().append("a").append("b").toString()  new String("ab")
        String s5 = "a" + "b";  // javac 在編譯期間的優化,結果已經在編譯期肯定爲ab

        System.out.println(s3 == s5);
    }
}

JDK1.7之後,利用intern()方法,會將字符串對象嘗試放入串池,若是有則並不會放入,若是沒有則放入串池, 會把串池中的對象返回;而JDK1.6調用intern()方法,是將對象拷貝一份到串池中,指向堆中的對象自己引用並不不改變jvm

public class Demo1_23 {

    //  ["ab", "a", "b"]
    public static void main(String[] args) {
        demo1();
        demo2();
    }

    static void demo1() {
        // 串池中事前沒有"ab",intern()以後,s返回的是串池中的對象
        String s = new String("a") + new String("b");

        String s1 = s.intern();

        System.out.println(s == "ab");  // true
        System.out.println(s1 == "ab");     //true
    }

    static void demo2() {
        // 串池中事前已有"ab",s返回的還是堆中的對象
        String x = "ab";
        String s = new String("a") + new String("b");

        // 堆  new String("a")   new String("b") new String("ab")
        String s2 = s.intern(); // 將這個字符串對象嘗試放入串池,若是有則並不會放入,若是沒有則放入串池, 會把串池中的對象返回

        System.out.println( s2 == x);   // true
        System.out.println( s == x );   //false
    }
}
  • 位置
    JDK1.6時,StringTable放在元空間內,屬於永久代的位置,可是StringTable佔用內存容易觸發full gc耗時較久;JDK1.7之後將StringTable放在堆內存中,隨着內存佔用增大首先觸發minor gc,耗時較短.

直接內存

使用Native函數直接分配堆外內存,而後經過Java堆裏的DirectByteBuffer對象做爲引用對這塊內存的引用進行操做.
原理說明:函數

使用Unsafe對象完成直接內存的分配和回收,回收時須要主動調用freeMemory方法
ByteBuffer的實現類內部使用了Cleaner(虛引用)來監測ByteBuffer(BB)對象,一旦BB對象被垃圾回收,會有ReferenceHandler線程經過Cleaner方法調用freeMemory來釋放內存佈局

建立新對象說明

HotSpot虛擬機在Java堆中對象分配、佈局和訪問的過程

對象的建立

  1. new字節碼指令
    虛擬機遇到new字節碼指令時,首先檢查可否在常量池中定位到一個類的符號引用,並檢查該符號引用的來是否已被加載、解析和初始化。若是沒有,則執行相應的類加載過程。
    類加載檢查後,虛擬機爲新生對象分配內存

  2. 內存分配
    對象所需的內存大小在類加載過程當中能夠肯定,在Java虛擬機中爲對象劃份內存時有兩種方式:指針碰撞空閒列表
    指針碰撞: 利用一個指針做爲已用內存未用內存的分界點的指示器,內存分配就僅僅是指針的移動。優勢在於不會形成內存碎片化,可是速度較慢
    空閒列表:虛擬機維護一個內存使用記錄表,使用時,從空閒的內存區域直接劃分一塊足夠大的空間給對象實例。

  3. 內存分配的線程安全問題
    劃分可用空間後仍要考慮併發狀況下對內存的使用,有兩種方式解決內存衝突的問題:CAS配上失敗重試、TLAB本地線程分配緩衝
    TLAB:把內存分配的動做按照線程劃分在不一樣的空間之中進行,即每一個線程在Java堆中預先分配了一小塊內存空間

對象的內存佈局

對象在堆內存中的佈局能夠劃分爲三個部分:對象頭實例數據對齊填充

對象頭

對象頭中包含兩類信息:Mark Word類型指針

  • Mark Word
    存儲對象自身運行時數據,考慮到虛擬機的空間效率,被設計成一個動態定義的數據結構,即根據對象的狀態複用本身的存儲空間(數據長度在32位和64位虛擬機上分別爲32個比特和64個比特)

  • 類型指針
    對象中指向它類型元數據的指針,Java虛擬機經過這個指針來肯定該對象是哪一個類的實例(不是全部虛擬機都必須在對象數據上保留類型指針)此外,若是對象是一個數組,對象頭中還必須擁有一塊記錄數據長度的數據

實例數據

即程序代碼裏定義的各類類型的字段內容,包括從父類繼承的或子類中定義的字段。各種數據存儲是按照必定順序的(long/double、ints...),而寬度相同的字段老是被分配到一塊兒存放,因此父類中定義的變量可能會出如今子類以前。

對齊填充

佔位符,無特殊意義
HotSpot虛擬機的自動內存管理系統要求對象的起始地址必須是8字節的整倍數,如有些對象的對象頭和示例數據內存設計不是8的倍數,則須要利用佔位符來進行填充。

對象的訪問定位

Java程序經過reference數據操做對上的具體對象,主流的訪問方式有兩種:句柄直接指針

  • 句柄
    Java堆中可能劃分出一塊內存做爲句柄池。reference中存儲對象的句柄地址,句柄中包含對象的實例數據和類型數據的具體地址信息。

  • 直接指針
    Java堆中對象的佈局須要考慮如何放置類型數據的相關信息(如訪問信息)。reference中存儲的直接就是對象地址,若是隻訪問對象自己,就不要多一次間接訪問的開銷

優缺點

使用句柄訪問, reference數據只需關乎句柄地址,當對象被回收或移動後只需改變句柄中的實例數據指針,而reference自己不用修改 使用直接指針省去了一次指針定位的時間開銷,速度更快,因爲Java中對象的訪問至關頻繁,因此效果可觀。 HotSpot使用直接指針的方式

相關文章
相關標籤/搜索