JVM---堆(垃圾回收機制)

圖解對象分配過程

爲新對象分配內存是一件非常嚴謹和複雜的任務,JVM的設計者們不僅需要考慮內存如何分配、在哪裏分配等問題,並且由於內存分配算法與內存回收算法密切相關,所以還需要考慮GC執行完內存回收後是否會在內存空間中停生內存碎片。
1.new的對象先放伊甸園區。此區有大小限制。
2.當伊甸園的空間填滿時,程序又需要創建對象,JVM的垃圾 回收器將對伊甸園區進行垃圾回收(Minor GC), 將伊甸園區中的不再被其他對象所引用的對象進行銷燬。再加載新的對象放到伊甸園區
3.然後將伊甸園中的剩餘對象移動到倖存者0區。
4.如果再次觸發垃圾回收,此時上次倖存下來的放到倖存者0區的,如果沒有回收,就會.放到倖存者1區。
5.如果再次經歷垃圾回收,此時會重新放回倖存者0區,接着再去倖存者1區。
6.啥時候能去養老區呢?可以設置次數。默認是15次。可以設置參數: -XX:MaxTenuringThreshold=進行設置。

在這裏插入圖片描述

在這裏插入圖片描述

Mi nor GC、Ma jor GC與Full GC

JVM在進行GC時,並非每次都對上面三個內存區域一起回收的,大部分時候回收的都是指新生代。
針對HotSpotVM的實現,它裏面的GC按照回收區域又分爲兩大種類型:-種是部分收集(Partial GC),一種是整堆收集(Fu1l GC)

  • 部分收集:不是完整收集整個Java堆的垃圾收集。其中又分爲:

    • 新生代收集(Minor GC / Young GC) :只是新生代的垃圾收集
    • 老年代收集(Major GC / 0ld GC) :只是老年代的垃圾收集。
      • 目前,只有CMS GC會有單獨收集老年代的行爲。
      • 注意,很多時候Major GC會和Full GC混淆使用,需要具體分辨是老年代回收還是整堆回收。
    • 混合收集(MixedGC):收集整個新生代以及部分老年代的垃圾收集。
      • 目前,只有G1 GC會有這種行爲
  • 整堆收集(Fu1l GC): 收集整個java堆和方法區的垃圾收集。

  • 年輕代GC(Minor GC)觸發機制:

    • 當年輕代空間不足時, 就會觸發Minor GC,這裏的年輕代滿指的是Eden代滿,Survivor滿不會引發GC。(每次 Minor GC會清理年輕代的內存。)
    • 因爲Java對象大多都具備朝生夕滅的特性,所以Minor GC非常頻繁,一般回收速度也比較快。這一-定義既清晰又易於理解。
    • Minor GC會引發STW, 暫停其它用戶的線程,等垃圾回收結束,用戶線程才恢復運行。
  • 老年代GC (Major GC/Fu11 GC)觸發機制:

    • 指發生在老年代的GC,對象從老年代消失時,我們說“Major GC"或“Fu11 GC”發生了。
    • 出現了Major GC,經常會伴隨至少一次的Minor GC (但非絕對的,在Para11e1Scavenge收集器的收集策略裏就有直接進行Major GC的 策略選擇過程)。
      • 也就是在老年代空間不足時,會先嚐試觸發Minor GC。如果之後空間還不足,則觸發Major GC
    • MajorGC的速度一般會比MinorGC慢10倍以上,STW的時間更長。
    • 如果Major GC後,內存還不足,就報00M了。
    • Major GC的速度- -般會比Minor GC慢10倍以上。
  • Full GC觸發機制:(後面細講)
    觸發Full GC執行的情況有如下五種:
    (1)調用System. gc()時,系統建議執行Full GC,但是不必然執行
    (2)老年代空間不足
    (3)方法區空間不足
    (4)通過Minor GC後進入老年代的平均大小大於老年代的可用內存
    (5)由Eden區、 survivor space0 ( From Space) 區向survivor space1 (To

  • Space)區複製時,對象大小大於To Space可 用內存,則把該對象轉存到老年代,且老年代的可用內存小於該對象大小

    說明:fullgc是開發或調優中儘量要避免的。這樣暫時時間會短一些。

堆空間分代思想

爲什麼需要把Java堆分代?不分代就不能正常] C作了嗎?

  • 經研究,不同對象的生命週期不同。70%-99%的對 象是臨時對象。
    • 新生代:有Eden、 兩塊大小相同的Survivor (又稱爲from/to,s0/s1) 構成,to總爲空。
    • 老年代:存放新生代中經歷多次GC仍然存活的對象。

其實不分代完全可以,吩代的唯一理由就是優化Gc性能。如果沒有分代,那所有的對象都在一塊,就如同把一個學校的人都關在一個教室。GC的時候要找到哪些對象沒用,這樣就會對堆的所有區域進行掃描。而很多對象都是朝生夕死的,如果分代的話,把新創建的對象放到某一地方,當GC的時候先把這塊存儲“朝生夕死”對象的區域進行回收,這樣就會騰出很大的空間出來。

內存分配策略

針對不同年齡段的對象分配原則如下所示:

  • 優先分配到Eden
  • 大對象直接分配到老年代
    • 儘量避免程序中出現過多的大對象.
  • 長期存活的對象分配到老年代
  • 動態對象年齡判斷
    • 如果Survivor區中相同年齡的所有對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象可以直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。
  • 空間分配擔保
    • -XX: HandlePromotionFailure

TLAB

爲什麼有TLAB ( Thread Local Allocation Buffer ) ?

  • 堆區是線程共享區域,任何線程都可以訪問到堆區中的共享數據
  • 由於對象實例的創建在JVM中非常頻繁,因此在併發環境下從堆區中劃分內存空間是線程不安全的|
  • 爲避免多個線程操作同-地址,需要使用加鎖等機制,進而影響分配速度。

什麼是TLAB ?

  • 從內存模型而不是垃圾收集的角度,對Eden區 域繼續進行劃分,JVM爲每個線程分配了一個私有緩存區域,它包含在Eden空間內。
  • 多線程同時分配內存時,使用TLAB可以避免一系列的非線程安全問題,同時還能夠提升內存分配的吞吐量,因此我們可以將這種內存分配方式稱之爲快速分配策略。
  • 據我所知所有OpenJDK衍生出來的JVM都提供了TLAB的設計。

TLAB的再說明:

  • 儘管不是所有的對象實例都能夠在TLAB中成功分配內存,但JVM確實是將TLAB作爲內存分配的首選。
  • 在程序中,開發人員可以通過選項“-XX:UseTLAB”設置是否開啓TLAB空間。
  • 默認情況下,TLAB空間的內存非常小,僅佔有整個Eden空間的1號τ當然我們可以通過選項“-XX: TLABWasteTargetPercent”設置TLAB空間所佔用Eden空間的百分比大小。
  • 一旦對象在TLAB空間分配內存失敗時,JVM就會嘗試着通過使用加鎖機制確保數據操
    作的原子性,從而直接在Eden空間中分配內存。

在這裏插入圖片描述

小結:堆空間的參數設置

測試堆空間常用的jvm參數:
-XX: +PrintFlagsInitial :查看所有的參數的默認初始值I
-XX: +PrintFlagsFinal : 查看所有的參數的最終值(可能會存在修改,不再是初始值)
-Xms:初始堆空間內存(默認爲物理內 存的1/64)
-Xmx:最大堆空間內存(默認爲物理內存的1/4)
-Xmn:設置新生代的大小。(初始值及最大值)
-XX:NewRatio:配置新生代與老年代在堆結構的佔比
-XX:SurvivorRatio:設置新生代中Eden和S0/S1空間的比例
-XX:MaxTenuringThreshold:設置新生代垃圾的最大年齡
-XX: +PrintGCDetails:輸出詳細的GC處理日誌
打印gc簡要信息:①-XX:+PrintGC ② -verbose:gc
-XX:HandlePromotionFailure:是否設置空間分配擔保

堆是分配對象存儲的唯一選擇嗎?

在《深入理解Java虛擬機》中關於Java堆內存有這樣一段描述:
隨着JIT編譯期的發展與逃逸分析技術逐漸成熟,棧上分配、標量替換優化技術將會導致一些微妙的變化,所有的對象都分配到堆上也漸漸變得不那麼“絕對”了。

在Java虛擬機中,對象是在Java堆中分配內存的,這是一個普遍的常識。但是,有一種特殊情況,那就是如果經過逃逸分析(Escape Analysis) 後發現,-一個對象並沒有逃逸出方法的話,那麼就可能被優化成棧上分配。這樣就無需在堆上分配內存,也無須進行垃圾回收了。這也是最常見的堆外存儲技術。.

此外,前面提到的基於OpenJDK深度定製的TaoBaoVM,其中創新的GCIH (GCinvisible heap)技術實現off-heap,將生命週期較長的Java對象從heap中移至heap外,並且GC不能管理GCIH內部的Java對象,以此達到降低GC的回收頻率和提升GC的回收效率的目的。

逃逸分析

  • 如何將堆上的對象分配到棧,需要使用逃逸分析手段。
  • 這是一種可以有效減少Java程序中同步負載和內存堆分配壓力的跨函數全局數據流分析算法。
  • 通過逃逸分析,JavaHotspot編譯器能夠分析出一個新的對象的引用的使用範圍從而決定是否要將這個對象分配到堆上。
  • 逃逸分析的基本行爲就是分析對象動態作用域:
    • 當一個對象在方法中被定義後,對象只在方法內部使用,則認爲沒有發生逃逸。
    • 當一個對象在方法中被定義後,它被外部方法所引用,則認爲發生逃逸。例如作爲調用參數傳遞到其他地方中。

逃逸分析:代碼優化

使用逃逸分析,編譯器可以對代碼做如下優化:
一、棧上分配。將堆分配轉化爲棧分配。如果一個對象在子程序中被分配,要使指向該對象的指針永遠不會逃逸,對象可能是棧分配的候選,而不是堆,分配。
二、同步省略。如果一個對象被發現只能從一個線程被訪問到,那麼對於這個對象的操作可以不考慮同步。
三、分離對象或標量替換。有的對象可能不需要作爲-一個連續的內存結構存在也可以被訪問到,那麼對象的部分(或全部)可以不存儲在內存,而是存儲在CPU寄存器中。

同步消除

  • 線程同步的代價是相當高的,同步的後果是降低並|發性和性能。
  • 在動態編譯同步塊的時候,JIT編譯器可以藉助逃逸分析來判斷同步塊所使用的鎖對象是否只能夠被一個線程訪問而沒有被髮布到其他線程。如果沒有,那麼JIT編譯器在編譯這個同步塊的時候就會取消對這部分代碼的同步。這樣就能大大提高併發性和性能。這個取消同步的過程就叫同步省略,也叫鎖消除。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章