1. 程式人生 > 程式設計 >JVM—【01】認識JVM的記憶體佈局和執行時資料區

JVM—【01】認識JVM的記憶體佈局和執行時資料區

1. Java 記憶體區域

1.1. JVM 記憶體佈局 與 執行時資料區

JVM 記憶體佈局 與 執行時資料區


1.2. Heap 堆

  • 它的唯一目的就是存放物件例項;幾乎所有物件例項和陣列,分配記憶體的區域

  • 堆記憶體區域是執行緒共享區域,併發程式設計時需要考慮執行緒安全問題。

  • 可以通過-Xms256M -Xmx1024M 設定堆記憶體大小。

    注意: Java程式在執行中,堆空間會不斷擴容與減少,會造成系統壓力,所以一般設定為同樣大小

    -X: 表示執行引數

    ms: 表示memory start,即起始大小

    mx: 表示memory max ,即最大記憶體

  • 堆分成:新生代老年代兩大塊,如名字一樣,物件初生在新,有一例外是新生代無法接納的超大物件會在老年代建立

  • 新生代:物件主要分配在新生代的Eden區域

    如果在新生代分配失敗且物件是一個不含任何物件引用的大陣列,可被直接分配到老年代。

  • 可以設定分配在老年代大物件的閾值:-XX:PretenureSizeThreshold

    預設為0不生效,意味著任何物件都會現在新生代分配記憶體。

  • 可以通過-Xmn256M 設定新生代區域大小為256M。此處的大小是(eden + 2 survivor space),

  • 可以通過-XX:ServivorRatio=8 決定eden與Survivor的記憶體空間佔比為8:1

  • 長期存活的物件會進入老年代:虛擬機器器給每個初生物件都設定了一個age,當age>=15時就會晉升到老年代。

    當物件出現在Eden,經過YGC而存活,被移到Servivor區,此時年齡變為1。每次YGC過後,存活的物件age就會+1.直到被回收或者晉升老年代。

    另外如果在YGC中,要移動的物件大於Survivor的容量上限,則直接進入老年代。

  • 可以設定這個age的閾值:-XX:MaxTenuringThreshold,當age達到這個值就會進入到老年代。

    物件的年齡並不是必須達到了MaxTenuringThreshold才晉升老年代,如果在Survivor中相同年齡所有物件大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的物件就可以直接進入老年代。

  • 堆的OutOfMemoryRrror

    (簡稱OOM)如果一個新生物件或者在晉升的物件,分配的區域放不下了就會丟擲OOM。

    當一個新生物件分配給Eden時,如果Eden不夠,則會觸發Minor GC。

    當一個物件在晉升的時候JVM發現記憶體空間不夠,如果Survivor區中無法放下,或者是超大物件的閾值超過上限,則嘗試在老年代分配,如果老年代也無法分配,則觸發Full Garbage Collection(FGC),如果依然無法放下,則丟擲OOM。

    要分析OOM我們可以使用-XX:+HeapDumpOnOutOfMemory,讓JVM列印OOM資訊。


1.2. 方法區Method Area(PermGen & Metaspace)

  • 方法區主要用於存放:類元資訊、欄位、靜態屬性、方法、常量、JIT編譯後的程式碼等資料。

    永久帶(PerGen)和元空間(Metaspace)分別方法區的具體實現。

  • PermGen是Hotspot中(<=JDK1.7)特有的區域,稱為永久代。

    在該區域,如果動態載入過多的類,容易產生Perm的OOM。java.lang.OutOfMemory: PermGen space 錯誤。

    上述錯誤可以通過設定-XX:PermSize=1024M解決。

    另外還可以設定-XX:MaxPermSize=1024m 最大永久代大小。 預設是64M

    但是JDK8及以後,由於用元空間替換了PermGen所以在JDK8及以後的版本中HotSpot會提示:Java Hotspot 64Bit Server VM warning ignoring option MaxPermSize=1024M; support was removed in 8.0。

  • Metaspace是為瞭解決永久帶的缺陷而優化設計的新實現,它分配記憶體在本地記憶體,並且它把以前Perm中的字串常量全部移到了堆記憶體。而其他的包括類元資訊、欄位、靜態屬性、方法、常量等移到了元空間。其實在1.7的某個版本就已經把字串常量移到了堆記憶體中。

    大部分類元資料都在本地記憶體中分配。用於描述類元資料的“klasses”已經被移除。預設情況下,類元資料只受可用的本地記憶體限制。可以通過-XX:MaxDirectMemorySize=50m設定直接記憶體。

    因為是本地記憶體中儲存,所以如果程式存在記憶體洩露,不停的擴充套件Metaspace的空間,會導致機器的記憶體不足,所以還是要有必要的除錯和監控。

  • Metaspace可以通過-XX:MetaspaceSize=10m-XX:MaxMetaspaceSize=50m 設定初始空間大小和最大空間


1.3. 虛擬機器器棧 JVM Stack

JVM Stack

  • Stack 是一個先進後出的資料結構。JVM中的棧是描述Java方法執行的記憶體區域,它是執行緒私有的。每個方法從開始呼叫到結束呼叫就是棧幀從入棧到出棧的結果。

  • 活動執行緒中,只有棧頂的棧幀才是有效的,稱為當前棧幀。正在執行的方法稱為當前方法,棧幀是方法執行的基本結構。在執行引擎執行時,所有指令都只能針對當前棧幀操作。

  • 棧幀(Stack Frame)用於儲存區域性變量表、操作棧、動態連結、方法返回地址等資訊。

    區域性變量表:存放方法引數,編譯期可知的基本資料型別、物件引用型別(reference)和returnAddress型別(指向一條位元組碼指令地址)。區域性變量表所需的記憶體空間是在編譯期確定,方法在區域性變量表中分配多少空間是完全確定的。在執行期間不會改變區域性變量表的大小。區域性變數沒有準備階段,必須顯示初始化。

    操作棧是一個初始狀態為空的桶式結構棧。方法執行過程中,會有各種指令往棧寫入和提取資訊。JVM的執行引擎就是基於操作棧的執行引擎。

    動態連線: 在Class檔案中的常量持中存有大量的符號引用。位元組碼中的方法呼叫指令就以常量池中指向方法的符號引用作為引數。這些符號引用一部分在類的載入階段或第一次使用的時候就轉化為了直接引用,稱為靜態連結。而相反的,另一部分在執行期間轉化為直接引用,就稱為動態連結

    方法返回地址:方法執行時有兩種退出情況:一是正常退出,正常執行到方法的返回位元組碼指令;二是異常退出。兩種退出都會返回當前被呼叫的位置。方法退出相當於彈出當前棧幀,退出的方式有三種:1.返回值壓入上層呼叫棧幀。2.異常資訊拋給能夠處理的棧幀。3.PC計數器指向方法呼叫後的下一條指令。

  • StackOverflowError:當棧深度超過虛擬機器器分配給執行緒的棧大小時就會出現此error。

    最常見的就是遞迴深度超出了限定,然後丟擲這個錯誤

  • OutOfMemoryError:虛擬機器器擴充套件時無法申請到足夠的記憶體空間,多執行緒下的記憶體溢位,與棧空間是否足夠大並不存在任何聯絡。

    為每個執行緒的棧分配的記憶體越大(引數-Xss),那麼可以建立的執行緒數量就越少,建立執行緒時就越容易把剩下的記憶體耗盡,越容易記憶體溢位。

  • 可以通過-Xss2m設定棧記憶體大小,設定每個執行緒的棧記憶體,預設1M,一般來說是不需要改的。-XX:ThreadStackSize執行緒堆疊大小

    如果把-Xss或者-XX:ThreadStackSize設為0,就是使用“系統預設值”。而在Linux x64上HotSpot VM給Java棧定義的“系統預設”大小也是1MB。

    JDK1.6以前,誰設定在後面,誰就生效;JDK1.6以後,-Xss設定在後面,則以-Xss為準,-XXThreadStackSize設定在後面,則主執行緒-Xss為準,其它執行緒以-XX:ThreadStackSize為準。


1.4. 本地方法棧 Native Method Stacks

  • 本地方法棧為Native方法服務

  • 本地方法通過JNI(Java Native Interface)來訪問虛擬機器器執行時的資料區,甚至是呼叫暫存器,具有和JVM相同的能力和許可權。

  • 本地方法棧也會丟擲:OutOfMemoryError和StackOverflowError

  • JNI

    JNI深度使用作業系統的特性功能。複用非Java程式碼。如果大量使用其他語言來實現JNI,會失去跨平臺特性。

    如果對執行效率要求高,偏底層的跨程式的操作等,可以考慮設計為JNI呼叫方式。


1.5. 程式計數器 Program Counter Register

  • 每個執行緒建立後都會產生自己的程式計數器和棧幀,程式計數器用來存放執行指令的偏移量和行號指示器等,執行緒執行或恢復都依賴程式計數器。
  • 程式計數器是執行緒獨佔,在各個執行緒直接互不影響,在此區域也不會有記憶體溢位異常。
  • 執行緒如果在執行一個Java方法則記錄虛擬機器器位元組碼指令的地址,如果程式碼執行到了Native方法計數器就為undefined。

1.6. 直接記憶體 Direct Memory

  • 直接記憶體,即本機使用的堆外的系統記憶體。該部分記憶體可被JVM使用,不會被JVM堆記憶體限制,但是動態拓展時也會出現OutOfMemory,可用-XX:MaxDirectMemorySize=50m來限制使用記憶體空間的最大值最大值

  • DirectByteBuffer可以直接操作DirectMemory,它通過JNI呼叫native方法直接分配堆外記憶體,通過DirectByteBuffer物件對這塊記憶體物件進行操作

    這個呼叫,實際上是從系統的使用者態切換到了核心態使用系統呼叫來完成這個操作。

    為什麼要切換到核心態?使用者態沒有許可權去操作核心態的資源,它只能通過系統呼叫外完成使用者態到核心態的切換,然後在完成相關操作後再有核心態切換回使用者態。

    DirectByteBuffer該類本身還是位於Java記憶體模型的堆中。堆內記憶體是JVM可以直接管控、操縱。

    由於DirectByteBuffer的許可權修飾符是空的也就是預設的,所以在我們程式設計中是無法直接new,只允許同包建立,我們可以通過ByteBuffer中的靜態方法allocateDirect(int)方法來建立物件。

    public static ByteBuffer allocateDirect(int capacity) {
            return new DirectByteBuffer(capacity);
    }
    複製程式碼

    而 DirectByteBuffer 類中呼叫了native的unsafe.allocateMemory(size)來分配空間,實際上是使用了c語言的malloc方法。

    // Primary constructor
    //
    DirectByteBuffer(int cap) {                   // package-private
    
        super(-1,0,cap,cap);
        boolean pa = VM.isDirectMemoryPageAligned();
        int ps = Bits.pageSize();
        long size = Math.max(1L,(long)cap + (pa ? ps : 0));
        Bits.reserveMemory(size,cap);
    
        long base = 0;
        try { // 這裡是重點!!!掉黑板
            base = unsafe.allocateMemory(size);
        } catch (OutOfMemoryError x) {
            Bits.unreserveMemory(size,cap);
            throw x;
        }
        unsafe.setMemory(base,size,(byte) 0);
        if (pa && (base % ps != 0)) {
            // Round up to page boundary
            address = base + ps - (base & (ps - 1));
        } else {
            address = base;
        } // 這裡記錄分配空間的資訊。
        cleaner = Cleaner.create(this,new Deallocator(base,cap));
        att = null;
    }
    
        // 記錄分分配空間資訊的類
        private Deallocator(long address,long size,int capacity) {
            assert (address != 0);
            this.address = address;
            this.size = size;
            this.capacity = capacity;
        }
    複製程式碼

2. 物件建立與記憶體分配

2.1 物件建立

  • 物件使用new建立的簡單過程

建立物件過程

  • 指標碰撞:

    假設Java堆中記憶體是絕對規整的,所有用過的記憶體都被放在一邊,空閒的記憶體被放在另一邊,中間放著一個指標作 為分界點的指示器,那所分配記憶體就僅僅是把那個指標向空閒空間那邊挪動一段與物件大小相等的距離,這種分配方式稱為“指標碰撞”(Bump The Pointer) 【帶Compact過程的Serial、ParNew等採用指標碰撞。】

  • 空閒列表

    如果Java堆中的記憶體並不是規整的,已使用的和空閒的記憶體相互交錯,就無法進行指標碰撞了,JVM就必須維護一個列表,記錄可用記憶體區域,在分配的時候從列表中找到一塊足夠大的空間劃分給物件例項,並更新列表上的記錄,這種分配方式稱為“空閒列表”(Free List) 【CMS這種基於Mark-Sweep演演算法的使用空閒列表】

  • 不難想到,分配記憶體時如果多個執行緒同時建立物件,就會出現併發問題。JVM實際採用:

    一種是CAS(Compare And Swap)加上失敗重試機制來保證更新操作的原子性;

    另一種是本地執行緒緩衝(TLAB,Thread Local Allocation Buffer.),即把記憶體分配的動作按照執行緒劃分在不同的空間之中進行,每個執行緒都預先分配一小塊記憶體。執行緒在自己的TLAB中分配,只有TLAB用完才需要同步加鎖。虛擬機器器是否用TLAB,可以通過-XX:+/-UseTLAB引數設定。


2.2 物件記憶體

  • 物件頭(Header)包含兩部分:一是自身執行時資料;二是型別指標

    執行時資料: 32位和64位JVM分別對應32位和64位長度(未開啟指正壓縮),儲存包括:雜湊碼、GC分帶年齡、鎖狀態標誌、執行緒池持有鎖、偏向鎖ID、偏向時間戳等。(Mark Word)。

    型別指標: 即物件指向它的類元資料的指標,虛擬機器器通過這個指標確定是哪個物件的例項。查詢物件的元資料資訊不一定要經過物件本身。物件是Java陣列,則物件頭中則會有一塊記錄陣列長度的資料;普通Java類可以通過元資料資訊確定Java類大小,但陣列還需要需要物件頭中的長度資料才能確定。

  • 例項資料(Instance Data)

    就是物件儲存的真正的有效資訊,也就是程式程式碼中定義的所有欄位內容。

  • 對齊填充(Padding)

    因為HotSpot VM的自動記憶體管理系統要求物件起始地址必須是8位元組的整數倍,物件頭部分正好是8位元組的倍數,當物件例項資料部分沒有對齊時,就需要通過對齊填充來補全。


2.3 物件訪問

  • Java通過棧上的reference資料來操作堆上的具體物件,而reference是一個指向物件的引用,通過reference去定位和訪問物件,目前主流的使用兩種方式:一是使用控制程式碼,二是使用直接指標

    控制程式碼: JVM堆會專門劃分記憶體作為控制程式碼池,而reference中存的就是物件的控制程式碼地址;控制程式碼中包含了物件例項資料與型別資料各自的具體地址。

    控制程式碼

    直接指標: 如果是直接指標,Java堆中就會防止訪問型別資料相關的資訊。而reference中儲存的直接就是物件地址。

    直接指標


關於我

  • 座標杭州,普通本科在讀,電腦科學與技術專業,20年畢業,目前處於實習階段。
  • 主要做Java開發,會寫點Golang、Shell。對微服務、大資料比較感興趣,預備做這個方向。
  • 目前處於菜鳥階段,各位大佬輕噴,小弟正在瘋狂學習。
  • 歡迎大家和我交流鴨!!!