阿里面試官:小夥子,你給我說一下JVM對象建立與內存分配機制吧

內存分配機制

逐步分析java

類加載檢查:

虛擬機遇到一條new指令(new關鍵字、對象的克隆、對象的序列化等)時,會先去檢查這個指令的參數在常量池中定位到一個類的符號引用,而且這個符號引用表明的類是否應被加載過,若是沒有那麼就去加載該類面試

分配內存

類加載完畢後會給對象分配內存空間。對象的所需的內存大小在類加載完畢後就即可徹底確認,爲對象分配內存大小的空間等同於把一塊肯定大小的內存從java堆中劃分出來。算法

如何劃份內存?數組

指針碰撞(默認使用指針碰撞):若是java堆內存是絕對規整的,那麼會把全部用過的內存放在一邊,空閒的內存放在另一邊,中間用一個指針來做爲分界點的指示器,那所分配的內存僅僅把那個指針空閒空間的挪動一段與對象大小相同的距離。 空閒列表:若是java堆內存不是絕對規整的,已使用的空間和未使用的空間互相交錯,那麼虛擬機維護一份列表,記錄哪些內存塊是可用的,在劃份內存空間的時候從列表中找到一塊足夠大的內存空間分配給對象實例,並更新列表上的記錄。 分配內存遇到高併發的問題?如今有多個線程同時併發須要進行內存分配bash

CAS  :虛擬機採用失敗重試的機制方式保證操做的原子性對分配內存空間的動做進行同步處理,第一個線程搶佔到了分配空間,第二個線程沒有搶佔到就重試搶佔後面一塊內存空間 本地線程分配緩衝:把內存分配的動做按照線程分配在不一樣的空間之中完成,也就是每一個線程在java堆中預先分配出一塊小的內存。經過-XX:+/-UseTLAB參數來設定虛擬機是否使用(JVM默認開啓-XX:+UseTLAB) ,-XX:TLABSize指定TLAB大小,默認是Eden區的百分之1,放不下就走CAS併發

初始化

內存分配完畢後,給變量賦默認值,若是使用了TLAB,那麼這個過程也能夠提早至TLAB分配時進行jvm

4.設置對象頭高併發

初始化零值以後,虛擬機要對對象進行必要的設置,例如這個對象是那個類的實例,如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡等信息。這些信息都存放在對象的對象頭Object Header中。佈局

在HosSpot虛擬機中,對象在內存中存儲的佈局能夠分爲3塊區域:對象頭,實例數據,對齊填充。性能

1.HotSpot虛擬機的對象頭包括三部分信息:Mark Word、Klass Pointer類型指針、數組長度

Mark Word標記字段(32位 4字節 ,64位佔8字節)用於存儲對象自身的運行時數據,如哈希碼、GC分代年齡、鎖狀態標誌、線程持有的鎖、偏向線程ID、偏向時間戳等。 對象頭的另一部分是類型指針(Klass Point 開啓壓縮佔4字節,關閉壓縮佔8字節),並非Class ,咱們使用的對象的getClass方法的那個Class對象是在堆內存而這個是類的元數據信息 。即對象指向它類的元數據的指針,元數據信息是放在方法區之中,虛擬機經過這個指針來肯定這個對象是那個類的實例,類的元數據信息是放在C++的對象來承載的  

實例數據

對象的實例數據就是該對象的引用大小

對齊填充

保證對象是8個字節的整數倍。64位的機器每一行都是64位,若是如今8個字節直接取一行,那若是不是對齊,還要評估這個對象的大小,還要從這個對象大小的起始位置開始偏移,這樣很是的麻煩,8個字節對齊是最優的尋址方式.

什麼是java對象的 指針壓縮?

jdk1.6 update14開始,在64bit操做系統中,JVM支持指針壓縮 jvm配置參數:啓用指針壓縮:­XX:+UseCompressedOops(默認開啓),禁止指針壓縮:­XX:­UseCompressedOops 爲何要進行指針壓縮?

1.在64位平臺的HotSpot中使用32位指針,內存使用會出多1.5倍左右,同時GC也會承受較大壓力

2.在jvm中,32位地址最大支持4G內存(2的32次方),能夠經過對象指針存入堆內存時壓縮的編碼而後在取出到CPU寄存器後解碼進行優化(對象指針在堆內存中是32位,在寄存器是35位,2的35次是32G),使得JVM使用32位地址就能夠支持更大的內存配置

若是壓縮了用4個字節沒有壓縮用8個字節,節約內存空間。多一個Object header,實際沒開指針壓縮是經過兩塊一塊兒來存儲Klass Point,成員對象String類型也用8個字節來存儲 ,成員對象Object也須要8個字節 。那麼咱們每一個對象都有對象頭,指針壓縮能夠減小咱們每一個對象的大小,一樣的內存大小能夠放更多的對象纔會觸發GC

執行方法

成員變量的賦值以及構造方法的調用

對象內存分配

對象內存分配流程圖

對象棧上分配

JVM內存分配能夠知道JAVA中的對象都是堆上進行分配,當對象沒有被引用的時候,須要一開GC進行回收內存,若是對象數量較多的時候,會給GC帶來較大的壓力,也間接影響了應用的性能。爲了減小臨時對象在堆內分配的數量,JVM經過逃逸分析來肯定該對象不會被外部訪問。若是不會逃逸能夠將該對象在棧上分配內存,這樣對象所佔用的內存空間就能夠隨着棧幀出棧而銷燬,就減輕了垃圾回收的壓力。

對象逃逸分析:就是分析對象動態做用域,當一個對象在方法中被定義後,它可能被外部方法所引用,例如做爲調用參數傳遞到其餘地方中

public class AllotOnStack {
    /**
     * Description : 棧上分配,標量替換
     * 代碼調用了1億次alloc(),若是是分配到堆上,大概須要1GB以上堆空間,若是堆空間小於該值,必然會觸發GC。
     * 使用以下參數不會發生GC
     * -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
     * 使用以下參數都會發生大量GC
     * -Xmx15m -Xms15m -XX:-DoEscapeAnalysis -XX:+PrintGC -XX:+EliminateAllocations
     * -Xmx15m -Xms15m -XX:+DoEscapeAnalysis -XX:+PrintGC -XX:-EliminateAllocations
     **/
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        System.out.println(start);
        for (int i = 0; i < 100000000; i++) {
            allot();
        }
        long end = System.currentTimeMillis();
        System.out.println(end);
        System.out.println(end - start);
    }
    private static void allot() {
        AllotOnBO allotOnBO = new AllotOnBO();
        allotOnBO.setA("123");
    }
}
 
class AllotOnBO {
    public void setA(String a) {
        this.a = a;
    }
    private String a;
}
複製代碼

很顯然test1方法中的user對象被返回了,這個對象的做用域範圍不肯定,test2方法中的user對象咱們能夠肯定當方法結 束這個對象就能夠認爲是無效對象了,對於這樣的對象咱們其實能夠將其分配在棧內存裏,讓其在方法結束時跟隨棧內 存一塊兒被回收掉。

JVM對於這種狀況能夠經過開啓逃逸分析參數(-XX:+DoEscapeAnalysis)來優化對象內存分配位置,使其經過標量替換優 先分配在棧上(棧幀上分配),JDK7以後默認開啓逃逸分析,若是要關閉使用參數(-XX:-DoEscapeAnalysis)

標量替換:經過逃逸分析肯定該對象不會被外部訪問,而且對象能夠被進一步分解時,JVM不會建立該對象,而是將該 對象成員變量分解若干個被這個方法使用的成員變量所代替,這些代替的成員變量在棧幀或寄存器上分配空間,這樣就 不會由於沒有一大塊連續空間致使對象內存不夠分配。開啓標量替換參數(-XX:+EliminateAllocations),JDK7以後默認 開啓。

標量與聚合量:標量即不可被進一步分解的量,而JAVA的基本數據類型就是標量(好比:int,long等基本數據類型以及reference類型等),標量的對立就是能夠被進一步分解的量,也就是聚合量。而JAVA中的對象就是能夠被進一步分解的聚合量。

結論:棧上分配的依賴於逃逸分析和標量替換,若是不開變量替換意義不大

Minor GC 和Full GC 有什麼區別?

MinorGC / Young GC:指的是新生代的垃圾收集動做,Minor GC 很是頻繁,回收速度通常也比較快。 MajorGC / Full GC:通常指老年代,年輕代,方法區的垃圾回收,Major GC 的速度一遍比 Minor GC的慢10倍以上 Eden與Survivor區默認8:1

大量的對象被分配在Eden區,Eden區滿了以後出發minor GC ,可能百分之99的對象都被當成垃圾回收掉,存活(標記)對象會被移動到Survivor,下一次當Eden區又滿了以後會觸發Minor GC把Eden區和Survivor區的存活對象移動到另外一塊Survivor區.每移動一次年齡加1,一直達到年齡15的時候會把移動到老年代。新生代的對象都是朝生夕死的,因此爲了減小Minor GC的頻率 儘可能讓Eden區儘可能大 ,Survivor區夠用便可。JVM默認比例已經很合適了

JVM默認有這個參數-XX:+UseAdaptiveSizePolicy(默認開啓),會致使這個8:1:1比例自動變化,若是不想這個比例有變化能夠設置參數-XX:-UseAdaptiveSizePolicy

public class MinorGc {
    // -XX:+PrintGCDetails
    public static void main(String[] args) {
        byte[] allocation1, allocation2;
        allocation1 = new byte[60000 * 1024];
        allocation2 = new byte[20000 * 1024];
    }
}
 
[GC (Allocation Failure) [PSYoungGen: 65245K->776K(76288K)] 65245K->60784K(251392K), 0.0333426 secs] [Times: user=0.05 sys=0.03, real=0.05 secs] 
Heap
 PSYoungGen      total 76288K, used 21431K [0x000000076b400000, 0x0000000774900000, 0x00000007c0000000)
  eden space 65536K, 31% used [0x000000076b400000,0x000000076c82bef8,0x000000076f400000)
  from space 10752K, 7% used [0x000000076f400000,0x000000076f4c2020,0x000000076fe80000)
  to   space 10752K, 0% used [0x0000000773e80000,0x0000000773e80000,0x0000000774900000)
 ParOldGen       total 175104K, used 60008K [0x00000006c1c00000, 0x00000006cc700000, 0x000000076b400000)
  object space 175104K, 34% used [0x00000006c1c00000,0x00000006c569a010,0x00000006cc700000)
 Metaspace       used 3487K, capacity 4498K, committed 4864K, reserved 1056768K
  class space    used 387K, capacity 390K, committed 512K, reserved 1048576K
複製代碼

分配了allocation1對象 這個時候Eden區幾乎已經滿了,下一步指令又要分配20M對象 ,Eden區已經不夠給allocation2 分配內存空間了虛擬機觸發Minor GC ,GC期間虛擬機發現結果From區只有10M放不下 ,因此只好把新生代對象提早存放到老年代,老年代的空間足夠存放allocation1,剩下的對象JVM自身的一些類 好比:Object,加載器被移動到了From區。Minor GC完以後Eden區給 allocation2 對象分配內存

大對象直接進入老年代

大對象就是須要大量連續空間內存空間的對象好比:字符串,數組。JVM參數-XX:PretenureSizeThreshold能夠設置大對象的大小,若是對象超過設置大小會直接進入老年代,不會進入年輕代,這個參數只在Serial和ParNew兩個收集器下有效

好比設置JVM參數-XX:+PrintGCDetails -XX:PretenureSizeThreshold=1000000(字節) -XX:+UseSerialGC ,在執行剛剛的代碼這個時候發現大對象直接進入老年代

爲何這樣設計?

爲了不爲大對象分配內存時的複製操做而下降效率

Heap
 def new generation   total 78656K, used 6995K [0x00000006c1c00000, 0x00000006c7150000, 0x0000000716800000)
  eden space 69952K,  10% used [0x00000006c1c00000, 0x00000006c22d4ed0, 0x00000006c6050000)
  from space 8704K,   0% used [0x00000006c6050000, 0x00000006c6050000, 0x00000006c68d0000)
  to   space 8704K,   0% used [0x00000006c68d0000, 0x00000006c68d0000, 0x00000006c7150000)
 tenured generation   total 174784K, used 80000K [0x0000000716800000, 0x00000007212b0000, 0x00000007c0000000)
   the space 174784K,  45% used [0x0000000716800000, 0x000000071b620020, 0x000000071b620200, 0x00000007212b0000)
 Metaspace       used 3485K, capacity 4498K, committed 4864K, reserved 1056768K
  class space    used 387K, capacity 390K, committed 512K, reserved 1048576K
複製代碼

長期存活的對象將進入老年代

JVM採用了分代年齡收集的思想,那麼回收這個對象的時候就須要考慮放在Survivor仍是老年代,考慮的依據虛擬機會爲每一個對象分配一個年齡計算器,若是對象在Eden區通過一次Monir GC後存活下的對象,移動到Survivor後年齡+1,以後的Minor GC每存活一次年齡再次+1 一直加到15(CMS默認是6,不一樣的垃圾收集器略不一樣),就會被移動到老年代。能夠經過參數-XX:MaxTenuringThreshold來設置

對象動態年齡判斷

若是在Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就能夠直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。那麼年齡大於或等於這批的對象將直接挪到老年代,例如:年齡1+年齡2+年齡3+年齡4+年齡N的對象 其中年齡1,2,3的對象總和超過了Survivor區域的百分之50(-XX:TargetSurvivorRatio能夠指定),那麼就會將3和3以上的對象都進入老年代。

老年代分配擔保機制(觸發Full GC)

年輕代每次Minor GC以前JVM都會計算下老年代剩餘可用空間,若是這個可用空間小於年輕代裏現有的全部對象大小之和(包括垃圾對象)就會看一個「-XX:-HandlePromotionFailure」(jdk1.8默認就設置了)的參數是否設置了

若是有這個參數,就會看看老年代的可用內存大小,是否大於以前每一次Minor GC後進入老年代的對象的平均大小。

若是上一步結果是小於或者以前說的參數沒有設置,那麼就會觸發一次Full GC ,對老年代和年輕代一塊兒回收一次垃圾,若是回收完仍是沒有足夠空間存放新的對象就會發生OOM

固然,若是Minor GC以後剩餘存活的須要挪到老年代的對象仍是大於老年代可用空間,那麼也會觸發Full GC ,Full GC完以後若是仍是沒有空間放Minor GC以後的存活對象,則會發生OOM

老年代分配擔保機制擔保的就是存在Full GC的狀況下 減小一次Minor GC ,若是沒有擔保那麼就是Minor GC->Full GC

對象內存回收

引用計數器

給對象中添加一個引用計數器,每當一個地方引用它,計數器就加;當引用失效,計數器就減1;任什麼時候候計數器爲0的對象就是不可能再被使用的。

這個方法實現簡單,效率高,可是目前主流的虛擬機中並無選擇這個算法來管理內存,其最重要緣由它很難解決對象之間互相循環引用的問題。所謂對象之間的互相引用問題,以下面代碼所示:除了對象ObjA和ObjB互相引用着對方以外,這兩個對象之間再無任何引用。可是由於他們互相引用對方,致使他們的引用計數器都不爲0,因而引用計數算法沒法通知GC回收他們

public class ReferenceCountingGc {
    Object instance = null;
 
    public static void main(String[] args) {
        ReferenceCountingGc objA = new ReferenceCountingGc();
        ReferenceCountingGc objB = new ReferenceCountingGc();
        objA.instance = objB;
        objB.instance = objA;
        objA = null;
        objB = null;
    }
}
複製代碼

可達性分析算法

將GCRoots對象做爲起點,從這些節點開始向下搜索引用的對象,找到的對象標記爲非垃圾對象,其他未標記的對象都是垃圾對象

GC Roots根節點: 線程棧的本地變量,靜態變量,本地方法棧的變量等等

finalize()方法最終判斷對象是否存活

即便在可達性分析算法中不可達的對象,也並不是是「非死不可」的,這個時候它們暫時處於「緩刑」階段,要真正宣告一個對象死亡,至少要經歷再次標記過程

標記的前提是對象在進行可達性分析法後發現沒有與GC Roots相鏈接的引用鏈

1.第一次標記並進行一次篩選

篩選的條件就是該對象是否覆蓋了finalize()方法,沒有覆蓋直接回收

2.第二次標記

若是這個對象覆蓋了finalize()方法,只要從新與引用鏈上任何一個對象關聯便可,好比把本身賦值給某個類的變量或對象的成員變量,那麼第二次標記的時候它將移除「即將回收」的集合。

注意:一個對象的finalize()方法只會被執行一次,也就是說經過調用finalize方法自我救命的機會就一次

如何判斷一個類是無用的類?

方法區主要回收的是無用的類,如何判斷一個類是無用的類

類須要知足下面三個條件才能算是無用的類

該類的全部實例都被回收,Java堆中沒有存在該類是任何實例 加載該類的ClassLoader被回收(只有自定義的類加載器才能被回收) 該類的對應的Java.lang.Class對象沒有任何地方被引用,沒法在任何地方經過反射訪問該類的方法。

最後

但願我總結的這些東西對大家會有幫助,大家看完以後有什麼的不懂的歡迎在下方留言討論,也能夠私信問我,我通常看完以後都會回的,也能夠關注個人公衆號:前程有光,立刻金九銀十跳槽面試季,整理了1000多道將近500多頁pdf文檔的Java面試題資料,文章都會在裏面更新,整理的資料也會放在裏面。

相關文章
相關標籤/搜索