JVM—【01】認識JVM的內存佈局和運行時數據區

1. Java 內存區域

1.1. JVM 內存佈局 與 運行時數據區

JVM 內存佈局 與 運行時數據區


1.2. Heap 堆

  • 它的惟一目的就是存放對象實例;幾乎全部對象實例和數組,分配內存的區域java

  • 堆內存區域是線程共享區域,併發編程時須要考慮線程安全問題。算法

  • 能夠經過-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, size, 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。對微服務、大數據比較感興趣,預備作這個方向。
  • 目前處於菜鳥階段,各位大佬輕噴,小弟正在瘋狂學習。
  • 歡迎你們和我交流鴨!!!
相關文章
相關標籤/搜索