天天看點

面試官:你了解對象的配置設定嗎?對象的配置設定政策,棧上配置設定與TLAB

JVM是如何自動進行記憶體管理的呢?本文詳細對象的配置設定政策,棧上配置設定與TLAB,相信相信大家看完已經掌握JVM是如何管理,本文适合點贊+收藏。有什麼錯誤希望大家直接指出~

前面我們學習了​​JVM記憶體區域​​​、​​JVM中的對象及引用​​,這節首先大家想一個問題:平時寫代碼需要去編寫對象被配置設定在記憶體的什麼位置了嗎?是的,就像是不需要考慮垃圾回收具體什麼時間點回收,JVM已經自動進行記憶體管理了,JVM這麼做也是有原因的。

Java自動記憶體管理概述

Java所支援的自動記憶體管理針對的是對象記憶體的自動配置設定和回收,原因如下:

1、在Java的記憶體區域中,本地方法棧、虛拟機棧、程式計數器這三塊記憶體區域的配置設定和回收具有确定性,他們在編譯階段就能确定需要配置設定的空間大小。此外,這些記憶體區域屬于線程私有,随線程生而生,随線程滅而滅。綜上,虛拟機不需要在這部分記憶體區域花費太多精力用于垃圾回收。

2、方法區存儲的是類資訊、靜态變量、常量、即時編譯器編譯過的代碼,這部分資料的回收條件較為苛刻,垃圾回收的“成績”并不是那麼令人滿意,是以不是垃圾收集器需要重點關注的區域。

3、Java堆存儲所有線程的對象,這些對象記憶體空間的配置設定是在程式運作期間才進行的,是以具有不确定性。此外,對象的生命周期長短不一,為了提高垃圾收集的效率,需要針對不同生命周期的對象設定不同的垃圾收集算法,這也就增加了記憶體管理的複雜度。

對象配置設定政策

對象優先在 Eden 區配置設定

大多數情況下,對象在新生代 Eden 區中配置設定。當 Eden 區沒有足夠空間配置設定時,虛拟機将發起一次 Minor GC。

大對象直接進入老年代

大對象就是指需要大量連續記憶體空間的 Java 對象,最典型的大對象便是那種很長的字元串,或者元素數量很龐大的數組。

大對象對虛拟機的記憶體配置設定來說就是一個不折不扣的壞消息,比遇到一個大對象更加壞的消息就是遇到“朝生夕滅”的“短命大對象”,我們寫程式的時候應注意避免。

在 Java 虛拟機中要避免大對象的原因是,在配置設定空間時,它容易導緻記憶體明明還有不少空間時就提前觸發垃圾收集,以擷取足夠的連續空間才能安置好它們。而當複制對象時,大對象就意味着高額的記憶體複制開銷。

HotSpot 虛拟機提供了-XX:PretenureSizeThreshold 參數,指定大于該設定值的對象直接在老年代配置設定,這樣做的目的就是避免在 Eden 區及兩個 Survivor區之間來回複制,産生大量的記憶體複制操作。

這樣做的目的:1.避免大量記憶體複制,2.避免提前進行垃圾回收,明明記憶體有空間進行配置設定。

PretenureSizeThreshold 參數隻對 Serial 和 ParNew 兩款收集器有效。-XX:PretenureSizeThreshold=4m

長期存活對象進入老年區

HotSpot 虛拟機中多數收集器都采用了分代收集來管理堆記憶體,那記憶體回收時就必須能決策哪些存活對象應當放在新生代,哪些存活對象放在老年代中。為做到這點,虛拟機給每個對象定義了一個對象年齡(Age)計數器,存儲在對象頭中。如果對象在 Eden 出生并經過第一次 Minor GC 後仍然存活,并且能被 Survivor 容納的話,将被移動到 Survivor 空間中,并将對象年齡設為 1,對象在 Survivor區中每熬過一次 Minor GC,年齡就增加 1,當它的年齡增加到一定程度(并發的垃圾回收器預設為 15),CMS 是 6 時,就會被晉升到老年代中。-XX:MaxTenuringThreshold 調整。

空間配置設定擔保

在發生 MinorGC 之前,虛拟機會先檢查老年代最大可用的連續空間是否大于新生代所有對象總空間,如果這個條件成立,那麼 MinorGC 可以確定是安全 的。如果不成立,則虛拟機會檢視 HandlePromotionFailure 設定值是否允許擔保失敗。

如果允許,那麼會繼續檢查老年代最大可用的連續空間是否大于曆次晉升到老年代對象的平均大小,如果大于,将嘗試着進行一次 MinorGC,盡管這次 MinorGC 是有風險的(因為判斷的是平均大小,有可能這次的晉升對象比平均值大很多),如果擔保失敗則會進行一次 FullGC;如果小于,或者 HandlePromotionFailure 設定不允許冒險,那這時也要改為進行一次 FullGC。

面試官:你了解對象的配置設定嗎?對象的配置設定政策,棧上配置設定與TLAB

BUT

在《深入了解Java虛拟機》書中,有這麼一句話:“對于大多數應用來說,Java堆是Java虛拟機所管理的記憶體中最大的一塊Java堆是被所有線程共享的一塊記憶體區域,在虛拟機啟動時建立。此記憶體區域的唯一目的就是存放對象執行個體,幾乎所有的對象執行個體都在堆上配置設定”。這裡沒有說所有的對象都在堆上進行配置設定,而是使用了“幾乎所有”一詞進行描述,那麼今天就來簡單聊一聊,除了堆以外的對象配置設定。

對Java對象配置設定的過程進行了分析,分析後可知為了解決線程安全問題并且提高效率,有另外兩個地方也是可以存放對象的這兩個地方分别是棧和TLAB。

棧上配置設定

再問一個問題:如果确定一個對象的作用域不會逃逸出方法之外,那可不可以将這個對象配置設定在棧上?這樣的話,對象所占用的記憶體空間就可以随着棧幀的出棧而銷毀。而且,在一般應用中,不會逃逸的局部對象所占的比例很大,是以如果能使用棧上配置設定,那大量的對象就會随着方法的結束而自動銷毀了,無須通過垃圾收集器回收,可以還可以減小垃圾收集器的負載。

分析完以後給出棧上配置設定官方定義:JVM允許将線程私有的對象打散配置設定在棧上,而不是配置設定在堆上。配置設定在棧上的好處是可以在函數調用結束後自行銷毀,而不需要垃圾回收器的介入,進而提高系統性能。棧上配置設定隻是JVM虛拟機提供的一種優化技術,對象主要還是配置設定在堆上的

逃逸分析

棧上配置設定也是有前提的,并不是所有的對象都可以棧上配置設定,首先需要進行逃逸分析的,是以逃逸分析是棧上配置設定的技術基礎那什麼是逃逸分析呢?逃逸分析是指判斷對象的作用域是否有可能逃逸出函數體,關于具體的逃逸分析算法和技術此篇不讨論Java SE 6u23版本之後,HotSpot中預設就開啟了逃逸分析,可以通過選項-XX:+PrintEscapeAnalysis檢視逃逸分析的篩選結果。

面試官:你了解對象的配置設定嗎?對象的配置設定政策,棧上配置設定與TLAB

如果是逃逸分析出來的對象可以在棧上配置設定的話,那麼該對象的生命周期就跟随線程了,就不需要垃圾回收,如果是頻繁的調用此方法則可以得到很大的性能提高。

逃逸分析的幾種情況:

public class EscapeAnalysisTest {
    static V global_v;
    public void a_method() {
        V v = b_method();
        c_method();
    }
    public V b_method() {
        V v = new V();
        return v;
    }
    public void c_method() {
        global_v = new V();
    }
}      

采用了逃逸分析後,滿足逃逸的對象在棧上配置設定

/**
 * 逃逸分析-棧上配置設定
 * -XX:-DoEscapeAnalysis
 *
 * @author macfmc
 * @date 2020/8/1-19:57
 */
public class EscapeAnalysisTest {
    private static class Stu {
        String a;
        int b;
        public Stu(String a, int b) {
            this.a = a;
            this.b = b;
        }
    }
    public static void alloc() {
        Stu stu = new Stu("小明", 22);
    }
    public static void main(String[] args) {
        long b = System.currentTimeMillis();
        for (int i = 0; i < 100000000; i++) {
            alloc();
        }
        long e = System.currentTimeMillis();
        System.out.println(e - b);
    }
}      

運作結果:沒有開啟逃逸分析,對象都在堆上配置設定,會頻繁觸發垃圾回收(垃圾回收會影響系統性能),導緻代碼運作慢。

// 1、參數為:-server -Xmx10m -Xms10m -XX:+DoEscapeAnalysis -XX:+PrintGC
[GC (Allocation Failure)  2048K->728K(9728K), 0.0017996 secs]
[GC (Allocation Failure)  2776K->696K(9728K), 0.0013323 secs]
10

// 2、參數為:-server -Xmx10m -Xms10m -XX:-DoEscapeAnalysis -XX:+PrintGC
[GC (Allocation Failure)  2760K->712K(9728K), 0.0004889 secs]
...瘋狂GC
[GC (Allocation Failure)  2760K->712K(9728K), 0.0004889 secs]
[GC (Allocation Failure)  2760K->712K(9728K), 0.0003785 secs]
[GC (Allocation Failure)  2760K->712K(9728K), 0.0008545 secs]
1955      

總結:1、棧上配置設定可以提升代碼性能,降低在多線程情況下的鎖使用,但是會受限于其空間的大小。

2、進行逃逸分析之後,産生的後果是所有的對象都将由棧上配置設定,而非從JVM記憶體模型中的堆來配置設定。

3、棧上配置設定可以提升代碼性能,降低在多線程情況下的鎖使用,但是會受限于其空間的大小。

4、分析找到未逃逸的變量,将變量類的執行個體化記憶體直接在棧裡配置設定(無需進入堆),配置設定完成後,繼續在調用棧内執行,最後線程結束,棧空間被回收,局部變量對象也被回收。

5、能在方法内建立對象,就不要再方法外建立對象。

TLAB(線程本地配置設定緩沖)

TLAB的全稱是Thread Local Allocation Buffer,即線程本地配置設定緩存區。

為什麼需要TLAB?

建立對象時,需要在堆上為新生的對象申請指定大小的記憶體,如果同時有大量線程申請記憶體的話,可以通過鎖機制確定不會申請到同一塊記憶體,在JVM運作中,記憶體配置設定是一個極其頻繁的動作,使用鎖這種方式勢必會降低性能。

是以就出現了TLAB,JVM通過使用TLAB來避免多線程沖突,每個線程使用自己的TLAB,這樣就保證了不使用同步,也不會出現線程安全問題,提高了對象配置設定的效率。(為新對象配置設定記憶體空間時,讓每個 Java 應用線程能在使用自己專屬的配置設定指針來配置設定空間,減少同步開銷。)

TLAB是什麼?

TLAB本身占用eden區空間,在開啟TLAB的情況下,虛拟機會為每個Java線程配置設定一塊TLAB空間。參數-XX:+UseTLAB開啟TLAB,預設是開啟的。

TLAB空間的記憶體非常小,預設情況下僅占有整個Eden空間的1%,當然可以通過選項-XX:TLABWasteTargetPercent設定TLAB空間所占用Eden空間的百分比大小。

TLAB隻是讓每個線程有私有的配置設定指針,但底下存對象的記憶體空間還是給所有線程通路的,隻是其它線程無法在這個區域配置設定而已。當一個TLAB用滿,就新申請一個TLAB,而在老TLAB裡的對象還留在原地什麼都不用管——它們無法感覺自己是否是曾經從TLAB配置設定出來的,而隻關心自己是在eden裡配置設定的。

TLAB空間由于比較小,是以很容易裝滿。比如,一個100K的空間,已經使用了80KB,當需要再配置設定一個30KB的對象時,肯定就無能為力了。這時虛拟機會有兩種選擇,第一,廢棄目前TLAB,這樣就會浪費20KB空間;第二,将這30KB的對象直接配置設定在堆上,保留目前的TLAB,這樣可以希望将來有小于20KB的對象配置設定請求可以直接使用這塊空間。實際上虛拟機内部會維護一個叫作refill_waste的值,通俗一點來說就是可允許浪費空間的值,當TLAB剩餘的空間小于新申請對象的大小,且這個剩餘的空間大于refill_waste(可允許浪費空間的值)時,會選擇在堆中配置設定(保留目前的TLAB);若剩餘的空間小于refill_waste(可允許浪費空間的值)時,則會廢棄目前TLAB,建立TLAB來配置設定對象。這個門檻值可以使用TLABRefillWasteFraction來調整,它表示TLAB中允許産生這種浪費的比例。預設值為64,即表示使用約為1/64的TLAB空間作為refill_waste。預設情況下,TLAB和refill_waste都會在運作時不斷調整的,使系統的運作狀态達到最優。

再舉兩個通俗易懂的例子幫助了解:大家可以花兩分鐘時間跟着下邊的例子算一下,算完後,對refill_waste會有更到位的了解

假設TLAB大小為100KB,refill_waste(可允許浪費空間的值)為5KB

  1、假如目前TLAB已經配置設定96KB,還剩下4KB,但是現在new了一個對象需要6KB的空間,顯然TLAB的記憶體不夠了,這時可以簡單的重新申請一個TLAB,原先的TLAB交給Eden管理,這時隻浪費4KB的空間,在refill_waste 之内。

  2、假如目前TLAB已經配置設定90KB,還剩下10KB,現在new了一個對象需要11KB,顯然TLAB的記憶體不夠了,這時就不能簡單的抛棄目前TLAB,因為此時抛棄的話,就會浪費10KB的空間,10KB是大于咱們設定的refill_waste(可允許浪費空間的值)5KB的,是以此時會保留目前的TLAB不動,會把這11KB會被安排到Eden區進行申請。

總結

名稱 針對點 處于對象配置設定流程的位置
棧上配置設定 避免gc無謂負擔 1
TLAB 加速堆上對象的配置設定 2

對象配置設定流程圖

面試官:你了解對象的配置設定嗎?對象的配置設定政策,棧上配置設定與TLAB

繼續閱讀