Java內存模型和GC機制

最近學習Python的GC機制時,想到了java的GC,忘得差不多了,(⊙﹏⊙)b?。∵@里便做一下回顧總結。推薦周志明譯本的《深入理解Java虛擬機》。

1. Java內存模型

1.1 程序計數器

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

這部分的內存區域是線程私有的。JVM中的多線程是通過線程輪流切換,每個線程在CPU分配的時間片執行的方式來實現的。任何一時刻,每個CPU內核都只會執行一個線程,線程切換的時候會保存上一個任務的狀態,以便下次切換會這個任務時再加載這個任務。程序計數器的作用就是在做上下文切換的時候,可以讓程序恢復到正確的位置。

1.2 Java虛擬機棧

Java虛擬機棧也是線程私有的,它的生命周期和線程相同。虛擬機描述的是Java方法執行的內存模型:每個方法在執行的時候都會創建一個棧幀,用于存儲局部變量表、操作數棧、返回值等信息。每一個方法從調用直至執行完成的過程,就雪瑩這一個棧幀在虛擬機棧中入棧到出棧的過程。

通常會粗粒度的把Java內存劃分為堆內存(Heap)和棧內存(Stack),這里的棧內存講的就是虛擬機棧(局部變量表部分)。

局部變量存放了編譯器可知的各種基本數據類型、引用類型。64位的long和double類型數據會占用2個局部變量空間,其余數據類型只占用1個。局部變量表所需要的內存空間在編譯期間完成分配,當進入一個方法時,這個方法在棧中分配多大的空間是確定的,在方法運行期間不會改變局部變量表的大小。

1.3 本地方法棧

本地方法棧和虛擬機棧的作用是非常類似的,在HotSpot虛擬機中把這兩部分合到了一起。本地方法棧和虛擬機棧的區別是:虛擬機棧為虛擬機執行Java方法(即字節碼)服務,而本地方法棧則為虛擬機使用到的native方法服務。

1.4 Java堆

Java堆是JVM所管理的內存中最大的一塊,它是被所有線程所共享的一塊內存區域。在虛擬機啟動時創建,此內存區域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內存。

Java堆是GC管理的主要區域。從內存回收的角度來看,由于現在基本上都采用分代回收算法,Java堆還可以分為新生代和老年代,后面小節會詳細介紹。

Java堆可以處于物理上不連續的內存空間,只要邏輯上是連續的即可。在實現中,既可以實現成固定大小的,也可以是可擴展的??梢酝ㄟ^-Xmx(最大值)和-Xms(最小值)配置。

1.5 方法區

方法區也是各個線程共享的內存區域,它用于存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯后的代碼等數據。雖然JVM規范把方法區描述為堆的一部分,它卻有一個別名(Non-Heap)非堆。

有人稱方法區為永久代,但其實永久代只是HotSpot虛擬機對方法去這個概念的實現。在JDK1.8,永久代已被移除,用元空間代替,元空間不再使用虛擬機內存,而直接使用本地的系統內存。

方法區中有一部分叫做常量池,用來存放編譯期生成的各種字面量和符號引用,這部分內容在類加載后放入方法區的常量池中。

1.6 直接內存

主要用在NIO中,它提供了一個DirectByteBuffer對象,可以直接直接訪問系統內存,可以避免在Java堆和Native堆中來回切換數據。

2. 對象創建過程

  1. 首先虛擬機會檢查常量池中類的信息,如果沒有,需要先加載類信息。檢查通過后,JVM將為新生對象分配內存,對象所需的內存大小在類加載完之后就可以確定,為對象分配空間的任務其實就是將一塊確定大小的內存從Java堆中劃分出來。

    分配內存有兩種方式:

    • 指針碰撞:假設堆中的內存是絕對規整的,所有使用過的內存都放在一遍,空閑的內存放在另一邊,中間放著一個指針作為分界點的指示器,那么為新對象分配內存時只需要將指針向空閑空間的那邊挪動一段與對象大小相等的距離即可
    • 空閑列表:假設堆中的內存不是規整的,已使用的內存和空閑的內存相互交錯,虛擬機就必須維護一個列表,記錄哪些內存時可用的,在分配的時候從列表中找到一塊足夠大的空間劃分給新的對象,并更新列表上的記錄
  2. 內存分配完成后,JVM將分配到的內存空間都初始化零值

undefined接下來JVM要對對象進行必要的設置,例如對象是哪個類的實例,如何才能找到類的元數據信息、對象的哈希碼、對象的GC分代年齡,這些信息將保存在對象的對象頭中

undefined下面將是執行init,根據編寫的代碼對對象進行初始化,對象創建完成

3. Java引用類型

Java中將引用分為了四種類型:強引用(Strong Reference),軟引用(Soft Reference),弱引用(Weak Reference),虛引用(Phantom Reference)。

  • 強引用:指的是類似Object obj=new Object()這樣顯示聲明的對象引用,是最普遍存在的引用,只要強引用還在,GC永遠不會回收掉被引用的對象,即使拋出OutOfMemmoryError,使程序終止。
  • 軟引用:用來描述一些還有用但非必需的對象。對于軟引用關聯的對象,在系統即將發生OOM錯誤之前,將會對這些對象進行回收,如果這次回收還沒有足夠的內存,才會拋出內存溢出異常??梢允褂肧oftReference類來實現軟引用。可以使用軟引用來構建緩存。
  • 弱引用:用來描述非必須對象,優先級比軟引用要低,在垃圾收集器工作時,無論當前內存是否足夠,都會回收掉只被弱引用關聯的對象
  • 虛引用:最弱的一種引用關系,一個對象是否有虛引用的存在,不會對其生存時間構成影響,也無法通過虛引用來獲取一個對象實例。為一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。
引用類型 被垃圾回收時間 用途 生存時間
強引用 從來不會 對象的一般狀態 JVM停止運行時終止
軟引用 在內存不足時 對象緩存 內存不足時終止
弱引用 在垃圾回收時 對象緩存 gc運行后終止
虛引用 Unknown Unknown Unknown

4. 垃圾檢測

垃圾回收(Garbage Collection)是JVM垃圾回收器提供的一種在空閑時間,不定時回收無任何引用對象占用的內存空間的一種機制。那么如何判定一個對象已經沒有任何引用了呢?

4.1 引用計數法

每個對象都有一個引用計數器,當一個對象被創建初始化后,該數字就為1。每當別的地方引用它時,計數器就會加1。當引用失效(如超出作用域,引用指向新的對象等),計數器就會減1。如果對象的引用計數為0,則就會被GC回收。

引用計數的優點是執行簡單、判定效率高。缺點是無法解決對象之間的循環引用問題。

# python簡單演示循環引用
class C():
    def __init__(self):
        print('內存地址是:%s' % str(hex(id(self))))

def test():
        c1 = C()
        c2 = C()
        c1.t = c2 # &c2的引用計數加1,變為2
        c2.t = c1 # &c1的引用計數加1,變為2
        del c1 # c1指向的對象引用計數減1,變為1
        del c2 # c2指向的對象引用數減1,變為1
test()

# 即使是將當前對象的引用刪除,由于原來c1和c2對象中還引用著彼此,所以引用計數都不為0,無法被GC回收

4.2 可達性分析算法

Java通常采用可達性分析(Reachability Analysis)來判定對象是否存活的。它是從離散數學中的圖論引入的。

基本思路是:先找到一組對象作為GC Roots(根節點),然后從根節點開始遍歷,遍歷結束后,如果發現某個對象與GC Roots沒有任何引用鏈相連(即該對象不可達),就證明該對象就是不可用的垃圾對象,GC會在接下來清除它們。

即使是循環引用的對象,如果與根節點沒有引用鏈,依然會被GC回收。

以下對象可以作為GC Roots:

  • 虛擬機棧(棧幀中的本地變量表)中的引用的對象
  • 方法區中的類靜態屬性以及常量引用的對象
  • 本地方法棧中Native方法引用的對象
  • 存活的線程

在使用可達性分析遍歷對象圖的時候,有幾個關鍵點需要注意:

  • GC停頓:在整個分析期間不能出現對象引用關系還在不斷變化的情況,所以在GC進行的時候必須要停頓所有的線程(Stop The World),停頓的位置稱為安全點(Safepoint),一般在循環的末尾、方法返回前、拋出異常的位置等。如果發生GC的時候,線程還沒有執行到一個安全點,線程繼續執行,到達下一個安全點的的時候暫停,然后等待GC;
  • finialize():在可達性分析中不可達的對象,真正宣判它的死亡,需要兩次標記過程:
    • 如果對象在進行可達性分析過后沒有與GC Roots相連,那么它會被第一次標記并且進行第一次篩選,篩選的條件是此對象有沒有必要執行finalize()方法。當對象沒有覆蓋finalize方法或者已經被JVM調用過,該對象會被視作“沒有必要執行”。
    • 如果對象被判定為有必要finalize()方法,那么這個對象將會被放置在一個F-Queue隊列中,并在稍后由一條由虛擬機自動建立的、低優先級的Finalizer線程去執行finalize()方法。由于finalize()只會被系統調用一次,這是對象完成“自我救贖”的最后一次機會。稍后GC將對F-Queue中的對象進行第二次小規模的標記,如果要在finalize()方法中成功拯救自己,只要在finalize()方法中讓該對象重新引用鏈上的任何一個對象建立關聯即可。而如果對象這時還沒有關聯到任何鏈上的引用,那它就會被回收掉。
    • 建議盡量不要去使用finalize()方法。

5. 垃圾回收

5.1 標記-清除(Mark-Sweep)

標記-清除算法是最基礎的收集算法。它分為兩個階段:

  • 標記:標記階段的任務就是標記出所有需要被回收的對象
  • 清除:回收被標記對象所占用的內存空間

優點:不需要移動對象,僅需要對不存活的對象進行處理,在對象存活率較高的場景下極為高效

缺點:

  • 效率問題:標記和清除的效率都不高,需要維護一張空閑列表
  • 空間問題:標記清除后會產生大量不連續的內存碎片,當分配大對象時,因為找不到足夠的連續內存空間而不得不提前觸發另一次GC

5.2 標記-整理(Mark-Compact)

與標記-清除法類似,但標記過后不是對可回收對象進行清理, 而是將所有存活的對象都向一段,然后直接清理掉邊界以外的內存。

優點:經過整理過后,新對象的分配只需要指針碰撞即可完成,而且不會再有碎片問題

缺點:需要將所有的對象都拷貝到一個新的地址,并且更新引用地址,GC停頓較長

5.3 復制(Copying)

該算法的提出是為了解決句柄開銷和內存碎片問題。它將可用內存分為大小相等的兩塊區域,每次只使用其中一塊。當一塊中的內存用完了,就將還存活的對象復制到另外一塊區域上面,然后將使用過的內存空間一次性清理掉。

優點:

  • 標記和復制階可以同時執行
  • 每次是對整塊半區進行回收,在對象存活率較低的場景下效率較高
  • 分配對象時不用考慮碎片問題

缺點:實際可用內存縮小為原來的一半

5.4 分代回收(Generational Collection)

JVM中采用的是分代回收,它根據對象的存活周期將內存區域分為新生代和老年代。

新生代中:對象生命周期短,每次GC時都有大批對象死去,只有少量存活,比較適用于復制方法。

老年代:對象存活時間極長,比較實用于標記-清理或標記-整理方法。

Python中也采用分代回收方法,將對象分為0、1、2代,可參照文章了解。

6. Java中的分代回收

JVM中的堆內存按照GC的角度可分為新生代和和老年代,新生代又可以分為三個部分:Eden和兩個Survivor區(Survivor0、Survivor1)。

新生代GC:Minor GC,非常頻繁,回收速度比較快

老年代GC:Major GC,一般會伴隨著Minor GC,對整個堆內存做一次GC,所以也稱Full GC,頻次較低,速度較慢

6.1 新生代

幾乎所有新創建的對象都是放在了年輕代。新生代在GC時,采用的是復制算法,由于新生代中的對象生命周期大都很短,所以并不需要按照1:1的比例來劃分內存空間,而是將內存劃分為較大的Eden區,和兩塊較小的Survivor區,三者的比例一般是8:1:1,可以通過-XX:SurvivorRatio設置。

大部分對象是在Eden中生成。GC時大致的過程如下:

  • 當創建新的對象時,如果Eden空間不足時,會觸發一次Minor GC。回收時,先將Eden區的存活對象復制到S0區
  • 當再次觸發Minor GC時,會將Eden和S0區存活的對象復制到S1區,清空Eden和S0區
  • 每次Minor GC時,都會對Eden和其中一個Survivor區域操作,將存活的對象放入到另外一個Survivor區中,如此反復。
  • 如果另外一塊Survior區沒有足夠空間存放上一次Mionr GC下存活的對象,這些對象將存放到老年代(這種稱之為分配擔保)
  • 每當對象在Survivor區經歷一次GC存活下來,它的年齡將加1,如果年齡達到N(一般是15)歲,就會移動到老年代中

6.2 老年代

老年代中存在的一般都是生命周期比較長的對象,它的空間也比新生代大很多(一般是2:1),一般采用的標記-整理方法。需要注意的有以下幾點:

  • 如上一小節所說,在新生代長期存活的對象將會被放入到老年代中

  • 大對象(很長的字符串、長數組等)直接進入老年代,大對象可能導致內存還有不少空間時就提前觸發Minor GC以獲取足夠的連續空間,也可以避免在Eden和Survivor區之間發生大量的內存復制。大對象的閾值可以通過參數設置

  • Survivor空間中,如果相同年齡的對象大小總和,大于Survivor空間的一半,年齡大于等于該年齡的對象可以直接進入老年代

  • 當老年代中的空間不足不足以存放即將升入老年代的對象時,會觸發一次Full GC。

  • 發生Minor GC之前,由于可能存在大量對象存活的情況(假如100%存活),虛擬機會檢查老年代中剩余空間是否大于新生代所有對象總空間,如果這個條件成立,Minor GC可以確保是安全的。如果不成立,虛擬機會查看HandlePromotionFailure設置值是否允許擔保失敗。如果允許,那么會繼續檢查老年代中最大的可用連續空間,是否大于歷次晉升到老年代對象的平均大小。如果大于,將嘗試Minor GC。如果小于、或者設置中不允許擔保失敗,或者在Minor GC時擔保失敗,則會發生一次Full GC。

    總結

    以上粗淺的介紹了JAVA中的GC機制。由于每次GC都會造成GC停頓,所以在開發過程中,盡可能減少GC的開銷。比如盡可能不要顯式調用System.gc()、字符串拼接時盡量使用StringBuffer、能使用基本類型的地方不要使用包裝類等。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,702評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,143評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 175,553評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,620評論 1 307
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,416評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,940評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,024評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,170評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,709評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,597評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,784評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,291評論 5 357
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,029評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,407評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,663評論 1 280
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,403評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,746評論 2 370

推薦閱讀更多精彩內容