1. 程式人生 > 實用技巧 >JVM 專題十:執行時資料區(五)堆

JVM 專題十:執行時資料區(五)堆

1. 核心概述

1.1 堆概述

  1. 一個程序對應一個jvm例項,一個執行時資料區,又包含多個執行緒,這些執行緒共享了方法區和堆,每個執行緒包含了程式計數器、本地方法棧和虛擬機器棧。

  1. 一個jvm例項只存在一個堆記憶體,堆也是java記憶體管理的核心區域

| 配置jvm及檢視jvm程序

  1. Java堆區在JVM啟動的時候即被建立,其空間大小也就確定了。是JVM管理的最大一塊記憶體空間

| 堆記憶體的大小是可以調節的

  1. 《Java虛擬機器規範》規定,堆可以處於物理上不連續的記憶體空間中,但在邏輯上它應該被視為連續的

  1. 所有的執行緒共享Java堆,在這裡還可以劃分執行緒私有的緩衝區(TLAB:Thread Local Allocation Buffer)

| 堆空間一定是所有執行緒共享的麼?不是,TLAB執行緒在堆中獨有的

  1. 《Java虛擬機器規範》中對java堆的描述是:所有的物件例項以及陣列都應當在執行時分配在堆上。

| 從實際使用的角度看,“幾乎”所有的物件的例項都在這裡分配記憶體 ("幾乎"是因為可能在棧上分配)

  1. 陣列或物件永遠不會儲存在棧上,因為棧幀中儲存引用,這個引用指向物件或者陣列在堆中的位置

  1. 在方法結束後,堆中的物件不會馬上被移除,僅僅在垃圾收集的時候才會被移除

  1. 堆,是GC(Garbage Collection,垃圾收集器)執行垃圾回收的重點區域

1.2 記憶體細分

|

現代垃圾收集器大部分都基於分代收集理論設計,堆空間細分為

  1. JDK 7以前堆記憶體邏輯上分為: 新生區+養老區+永久區(相當於省+香港+臺灣)

    • Young Generation Space:新生區 Young/New

| 又被分為Eden區和Survior區

    • Tenure generation Space:養老區 Old/Tenure
    • Permanent Space: 永久區 Perm

  1. JDK 8以後: 新生區+養老區+元空間

    • Young Generation Space:新生區 Young/New

| 又被分為Eden區和Survior區

    • Tenure generation Space:養老區 Old/Tenure
    • Meta Space:元空間 Meta

| 約定: 新生區<—>新生代<—>年輕代

| 養老區<—>老年區<—>老年代

| 永久區<—>永久代

2. 設定堆記憶體大小與OOM

2.1 設定堆記憶體大小

  1. Java堆區用於儲存Java物件例項,堆的大小在jvm啟動時就已經設定好了,可以通過 "-Xmx"和 "-Xms"來進行設定

| "-Xms"用於表示堆區的起始記憶體,等價於:-XX:InitialHeapSize

| "-Xmx"用於表示堆區的最大記憶體,等價於:-XX:MaxHeapSize

  1. 通常會將-Xms和-Xmx兩個引數配置相同的值,其目的就是為了能夠在java垃圾回收機制清理完堆區後不需要重新分隔計算堆區的大小,從而提高效能

  1. 預設情況下,初始記憶體大小:物理電腦記憶體 /64;最大記憶體大小:物理電腦記憶體 /4

  1. 設定堆大小為600m,打印出的結果為575m,這是因為倖存者區S0和S1各佔據了25m,但是他們始終有一個是空的,存放物件的是伊甸園區和一個倖存者區

2.2 OOM

一旦堆區中的記憶體大小超過 -Xmx所指定的最大記憶體時,將會丟擲OOM異常

3. 年輕代與老年代

  1. 儲存在JVM中的java物件可以被劃分為兩類:

    • 一類是生命週期較短的瞬時物件,這類物件的建立和消亡都非常迅速
    • 另外一類物件時生命週期非常長,在某些情況下還能與JVM的生命週期保持一致

  1. Java堆區進一步細分可以分為年輕代(YoungGen)和老年代(OldGen)

| 其中年輕代可以分為Eden空間(物件最先建立的位置)、Survivor0空間和Survivor1空間(有時也叫from區,to區)

  1. 配置新生代與老年代在堆結構的佔比

    • 預設-XX:NewRatio=2,表示新生代佔1,老年代佔2,新生代佔整個堆的1/3
    • 可以修改-XX:NewRatio=4,表示新生代佔1,老年代佔4,新生代佔整個堆的1/5
    • 在hotSpot中,Eden空間和另外兩個Survivor空間預設所佔的比例是8:1:1(假設總空間600,新生代:老年代=1:2=200:400,Eden:Survivor0:Survivor1=8:1:1=160:20:20)

| 測試的時候是6:1:1,因為有自適應的分配策略,要想變成8:1:1就得使用-XX:SurvivorRatio 調整空間比例,如-XX:SurvivorRatio=8

  1. 幾乎所有的Java物件都是在Eden區被new出來的

| "幾乎":如果new了一個非常大得物件,Eden區放不下,就可能直接進入老年代

  1. 絕大部分的Java物件的銷燬都在新生代進行了

| IBM公司的專門研究表明,新生代80%的物件都是“朝生夕死”的

  1. 可以使用選項-Xmn設定新生代最大記憶體大小

| 這個引數一般使用預設值就可以了

4. 圖解物件分配過程

為新物件分配記憶體是件非常嚴謹和複雜的任務,JVM的設計者們不僅需要考慮記憶體如何分配、在哪裡分配的問題,並且由於記憶體分配演算法與記憶體回收演算法密切相關,所以還需要考慮GC執行完記憶體回收後是否會在記憶體空間中產生記憶體碎片。

  1. new的物件先放伊甸園區。此區有大小限制。

  1. 當伊甸園的空間填滿時,程式又需要建立物件,伊甸園區放不下了,就會觸發Minor GC/YGC,JVM的垃圾回收器將對伊甸園區進行垃圾回收,將伊甸園區中的不再被其他物件所引用的物件進行銷燬(下圖紅色)。再載入新的物件放到伊甸園區。

| YGC:伊甸園區滿了會觸發YGC,倖存者區滿了不會觸發YGC,但是伊甸園區觸發的YGC會把伊甸園區和倖存者區的垃圾一起回收

  1. 然後將伊甸園中的剩餘物件移動到倖存者0區(下圖綠色)。

  1. 如果再次觸發垃圾回收(伊甸園的空間填滿),此時上次倖存下來的放到倖存者0區的物件,如果沒有回收(意思就是說倖存者0區現在存了上次的物件,沒有位置了),這次倖存的物件就會放到目前沒有存放東西的倖存者1區。

  1. 如果第三次經歷垃圾回收,S0中存的物件還是沒有被回收,此時就會把S0的物件移動到S1區,相應的移到S1區後物件的Age會增長為2,因為S0已經被空出來了,S0就會成為to區(誰空誰是to),第三次垃圾回收倖存的物件就會被放進S0區。

  1. 啥時候能去養老區呢?可以設定次數。預設是15次(Age=15)。

| 可以設定引數:-XX:MaxTenuringThreshold=<N>進行設定。

  1. 在養老區,相對悠閒。當老年區記憶體不足時,再次觸發GC:Major GC,進行養老區的記憶體清理。

  1. 若養老區執行了Major GC之後發現依然無法進行物件的儲存,就會產生OOM異常。

| 總結

    • 針對倖存者s0,s1區:複製之後有交換,誰空誰是to(複製演算法)
    • 關於垃圾回收:頻繁在新生區收集,很少在養老區收集,幾乎不在永久區/元空間收集。

  1. 常用調優工具

    • JDK命令列
    • Eclipse:Memory Analyzer Tool
    • Jconsole
    • VisualVM
    • Jprofiler
    • Java Flight Recorder
    • GCViewer
    • GC Easy

5. Minor GC、Major GC、Full GC

  1. JVM在進行GC時,並非每次都針對三個記憶體區域(新生代、老年代、方法區)一起回收的,大部分時候回收都是指新生代。

  1. 針對hotSpot VM的實現,它裡面的GC按照回收區域又分為兩大種類型:一種是部分收集(Partial GC),一種是整堆收集(Full GC)。

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

      • 新生代收集(Minor GC / Young GC):只是新生代(Eden / S0,S1)的垃圾收集
      • 老年代收集(Major GC/Old GC):只是老年代的垃圾收集

| 目前,只有CMS GC會有單獨收集老年代的行為

| 注意,很多時候Major GC 會和 Full GC混淆使用,需要具體分辨是老年代回收還是整堆回收

      • 混合收集(Mixed GC):收集整個新生代以及部分老年代的垃圾收集

| 目前,只有G1 GC會有這種行為

    • 整堆收集(Full GC):收集整個Java堆和方法區的垃圾收集

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

    • 當年輕代空間不足時,就會觸發Minor GC,這裡的年輕代滿指的是Eden區滿,Survivor滿不會引發GC.(每次Minor GC會清理年輕代的記憶體,Survivor是被動GC,不會主動GC)
    • 因為Java物件大多都具備朝生夕滅的特性,所以Monor GC 非常頻繁,一般回收速度也比較快,這一定義既清晰又利於理解。
    • Minor GC 會引發STW(Stop the World),暫停其他使用者的執行緒,等垃圾回收結束,使用者執行緒才恢復執行。

  1. 老年代GC(Major GC/Full GC)觸發機制

    • 指發生在老年代的GC,物件從老年代消失時,Major GC 或者 Full GC 發生了
    • 出現了Major GC,經常會伴隨至少一次的Minor GC(不是絕對的,在Parallel Scavenge 收集器的收集策略裡就有直接進行Major GC的策略選擇過程)

| 也就是老年代空間不足時,會先嚐試觸發Minor GC。如果之後空間還不足,則觸發Major GC

    • Major GC速度一般會比Minor GC慢10倍以上,STW時間更長
    • 如果Major GC後,記憶體還不足,就報OOM了

  1. Full GC觸發機制

    • 觸發Full GC執行的情況有以下五種

      • 呼叫System.gc()時,系統建議執行Full GC,但是不必然執行
      • 老年代空間不足
      • 方法區空間不足
      • 通過Minor GC後進入老年代的平均大小大於老年代的可用記憶體
      • 由Eden區,Survivor S0(from)區向S1(to)區複製時,物件大小大於To Space可用記憶體,則把該物件轉存到老年代,且老年代的可用記憶體小於該物件大小

| 說明:Full GC 是開發或調優中儘量要避免的,這樣暫停時間會短一些

6. 堆空間分代思想

  1. 為什麼要把Java堆分代?不分代就不能正常工作了麼?

經研究,不同物件的生命週期不同。70%-99%的物件都是臨時物件。

| 新生代:有Eden、Survivor構成(s0,s1 又稱為from to),to總為空

| 老年代:存放新生代中經歷多次依然存活的物件

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

7. 記憶體分配策略

  1. 如果物件在Eden出生並經過第一次Minor GC後依然存活,並且能被Survivor容納的話,將被移動到Survivor空間中,把那個將物件年齡設為1。物件在Survivor區中每熬過一次MinorGC,年齡就增加一歲,當它的年齡增加到一定程度(預設15歲,其實每個JVM、每個GC都有所不同)時,就會被晉升到老年代中

| 物件晉升老年代的年齡閾值,可以通過選項 -XX:MaxTenuringThreshold來設定

  1. 針對不同年齡段的物件分配原則如下:

    • 優先分配到Eden
    • 大物件直接分配到老年代

| 儘量避免程式中出現過多的大物件(大物件需要連續記憶體空間,空間剩餘很多,但是由於需要連續空間,就必須GC,GC就會引發STW)

    • 長期存活的物件分配到老年代
    • 動態物件年齡判斷

| 如果Survivor區中相同年齡的所有物件大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的物件可以直接進入到老年代。無需等到MaxTenuringThreshold中要求的年齡

    • 空間分配擔保

| -XX: HandlePromotionFailure

8. 為物件分配記憶體:TLAB

8.1 為什麼有TLAB(Thread Local Allocation Buffer)?

  1. 堆區是執行緒共享區域,任何執行緒都可以訪問到堆區中的共享資料(程序通訊很方便)

  1. 由於物件例項的建立在JVM中非常頻繁,因此在併發環境下從堆區中劃分記憶體空間是執行緒不安全的

  1. 為避免多個執行緒操作同一地址,需要使用加鎖等機制,進而影響分配速度

8.2 什麼是TLAB?

  1. 從記憶體模型而不是垃圾收集的角度,對Eden區域繼續進行劃分,JVM為每個執行緒分配了一個私有快取區域,它包含在Eden空間內

  1. 多執行緒同時分配記憶體時,使用TLAB可以避免一系列的非執行緒安全問題,同時還能夠提升記憶體分配的吞吐量,因此我們可以將這種記憶體分配方式稱之為快速分配策略

  1. 所有OpenJDK衍生出來的JVM都提供了TLAB的設計

8.3 TLAB說明

  1. 儘管不是所有的物件例項都能夠在TLAB中成功分配記憶體(因為TLAB記憶體很小),但JVM確實是將TLAB作為記憶體分配的首選。

  1. 在程式中,開發人員可以通過選項"-XX:UseTLAB"設定是否開啟TLAB空間(預設開啟)

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

  1. 一旦物件在TLAB空間分配記憶體失敗時,JVM就會嘗試著通過使用加鎖機制確保資料操作的原子性,從而直接在Eden空間中分配記憶體。

9. 堆空間的引數設定

  1. -XX:PrintFlagsInitial: 檢視所有引數的預設初始值

  1. -XX:PrintFlagsFinal:檢視所有的引數的最終值(可能會存在修改,不再是初始值)

| 具體檢視某個引數的指令:

    • jps:檢視當前執行中的程序
    • jinfo -flag SurvivorRatio 程序id: 檢視新生代中Eden和S0/S1空間的比例

  1. -Xms: 初始堆空間記憶體(預設為實體記憶體的1/64)

  1. -Xmx: 最大堆空間記憶體(預設為實體記憶體的1/4)

  1. -Xmn: 設定新生代大小(初始值及最大值)

  1. -XX:NewRatio: 配置新生代與老年代在堆結構的佔比

  1. -XX:SurvivorRatio:設定新生代中Eden和S0/S1空間的比例,預設值是8

    • Eden園區如果過大,會使Minor GC失去意義,Minor GC回收Eden園區和倖存者區就沒有意義

    • 倖存者區如果過大,Eden區就會變小,頻繁引起STW

  1. -XX:MaxTenuringThreshold:設定新生代垃圾的最大年齡(預設15)

  1. -XX:+PrintGCDetails:輸出詳細的GC處理日誌

| 列印gc簡要資訊:

    • -XX:+PrintGC
    • -verbose:gc

  1. -XX:HandlePromotionFailure:是否設定空間分配擔保

| -XX:HandlePromotionFailure 引數說明:

    • 在發生Minor Gc之前,虛擬機器會檢查老年代最大可用的連續空間是否大於新生代所有物件的總空間。

      • 如果大於,則此次Minor GC是安全的。
      • 如果小於,則虛擬機器會檢視-XX:HandlePromotionFailure設定值是否允許擔保失敗。(JDK 7以後的規則HandlePromotionFailure可以認為就是true)
      • 如果HandlePromotionFailure=true,那麼會繼續檢查老年代最大可用連續空間是否大於歷次晉升到老年代的物件的平均大小。

        • 如果大於,則嘗試進行一次Minor GC,但這次Minor GC依然是有風險的;
        • 如果小於,則改為進行一次Full GC。

      • 如果HandlePromotionFailure=false,則改為進行一次Full GC。

在JDK6 Update24之後(JDK7),HandlePromotionFailure引數不會再影響到虛擬機器的空間分配擔保策略,觀察openJDK中的原始碼變化,雖然原始碼中還定義了HandlePromotionFailure引數,但是在程式碼中已經不會再使用它。JDK6 Update24之後的規則變為只要老年代的連續空間大於新生代物件總大小或者歷次晉升的平均大小就會進行Minor GC,否則將進行Full GC。

10. 堆是分配物件的唯一選擇嗎?

10.1 說明

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

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

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

10.2 逃逸分析

10.2.1 概述

  1. 如何將堆上的物件分配到棧,需要使用逃逸分析手段。

  1. 這是一種可以有效減少Java程式中同步負載和記憶體堆分配壓力的跨函式全域性資料流分析演算法。

  1. 通過逃逸分析,Java Hotspot編譯器能夠分析出一個新的物件的引用的使用範圍從而決定是否要將這個物件分配到堆上。

  1. 逃逸分析的基本行為就是分析物件動態作用域:

| 當一個物件在方法中被定義後,物件只在方法內部使用,則認為沒有發生逃逸。

| 當一個物件在方法中被定義後,它被外部方法所引用,則認為發生逃逸。例如作為呼叫引數傳遞到其他地方中。

10.2.2 判斷是否發生逃逸

10.2.3 引數設定

  1. 在JDK 6u23版本之後(JDK7),HotSpot中預設就已經開啟了逃逸分析

  1. 如果使用了較早的版本,開發人員可以通過

    • -XX:DoEscapeAnalysis 顯式開啟逃逸分析
    • -XX:+PrintEscapeAnalysis檢視逃逸分析的篩選結果

| 結論:開發中能使用區域性變數的,就不要在方法外定義

10.2.4 程式碼優化

| 使用逃逸分析,編譯器可以對程式碼做如下優化:

  1. 棧上分配:將堆分配轉化為棧分配。如果一個物件在子執行緒中被分配,要使指向該物件的指標永遠不會逃逸,物件可能是棧分配的候選,而不是堆分配

  1. 同步省略:如果一個物件被發現只能從一個執行緒被訪問到,那麼對於這個物件的操作可以不考慮同步

  1. 分離物件或標量替換:有的物件可能不需要作為一個連續的記憶體結構存在也可以北方問道,那麼物件的部分(或全部)可以不儲存在記憶體,而是儲存在CPU暫存器中。

10.2.5 棧上分配

  1. JIT編譯器在編譯期間根據逃逸分析的結果,發現如果一個物件並沒有逃逸出方法的話,就可能被優化成棧上分配。分配完成之後,繼續在呼叫棧內執行,最後執行緒結束,棧空間被回收,區域性變數物件也被回收。這樣就無須進行垃圾回收了。

  1. 常見的棧上分配場景:給成員變數賦值、方法返回值、例項引用傳遞
/**
 * 棧上分配測試
 * -Xmx1G -Xms1G -XX:-DoEscapeAnalysis -XX:+PrintGCDetails 未開啟棧上分配
 * -Xmx1G -Xms1G -XX:+DoEscapeAnalysis -XX:+PrintGCDetails 開啟棧上分配
 */
public class StackAllocation {
    public static void main(String[] args) {
        long start = System.currentTimeMillis();

        for (int i = 0; i < 10000000; i++) {
            alloc();
        }
        // 檢視執行時間
        long end = System.currentTimeMillis();
        System.out.println("花費的時間為: " + (end - start) + " ms");
        // 為了方便檢視堆記憶體中物件個數,執行緒sleep
        try {
            Thread.sleep(1000000);
        } catch (InterruptedException e1) {
            e1.printStackTrace();
        }
    }

    private static void alloc() {
        User user = new User();//未發生逃逸,可以考慮棧上分配
    }

    static class User {

    }
}

10.2.6 同步省略/鎖消除

  1. 執行緒同步的代價是相當高的,同步的後果是降低併發性和效能。

  1. 在動態編譯同步程式碼塊的時候,JIT編譯器可以藉助逃逸分析來判斷同步程式碼塊所使用的鎖物件是否只能夠被一個執行緒訪問而沒有被髮布到其他執行緒。如果沒有,那麼JIT編譯器在編譯這個同步程式碼塊的時候就會取消對這部分程式碼的同步。這樣就能大大提高併發性和效能。這個取消同步的過程就叫同步省略,也叫鎖消除。

10.2.7 標量替換

  1. 標量Scalar是指一個無法在分解成更小的資料的資料。Java中的原始資料型別就是標量

  1. 相對的,那些還可以分解的資料叫做聚合量(Aggregate),Java中物件就是聚合量,因為它可以分解成其他聚合量和標量

  1. 在JIT階段,如果經過逃逸分析,發現一個物件不會被外界訪問的話,那麼經過JIT優化,就會把這個物件拆解成若干個其中包含的若干個成員變數來替代。這個過程就是標量替換

/**
 * 標量替換測試
 *  -Xmx100m -Xms100m -XX:+DoEscapeAnalysis(逃逸分析) -XX:+PrintGC -XX:-EliminateAllocations(標量替換)
 */
public class ScalarReplace {
    public static class User {
        public int id;
        public String name;
    }

    public static void alloc() {
        User u = new User();//未發生逃逸
        u.id = 5;
        u.name = "www.atguigu.com";
    }

    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            alloc();
        }
        long end = System.currentTimeMillis();
        System.out.println("花費的時間為: " + (end - start) + " ms");
    }
}

10.2.8 逃逸分析小結:逃逸分析並不成熟

  1. 關於逃逸分析的論文在1999年就已經發表了,但直到JDK1.6才有實現,而且這項技術到如今也並不是十分成熟的。

  1. 其根本原因就是無法保證逃逸分析的效能消耗一定能高於他的消耗。雖然經過逃逸分析可以做標量替換、棧上分配、和鎖消除。但是逃逸分析自身也是需要進行一系列複雜的分析的,這其實也是一個相對耗時的過程。

  1. 一個極端的例子,就是經過逃逸分析之後,發現沒有一個物件是不逃逸的。那這個逃逸分析的過程就白白浪費掉了。

  1. 雖然這項技術並不十分成熟,但是它也是即時編譯器優化技術中一個十分重要的手段。

  1. 注意到有一些觀點,認為通過逃逸分析,JVM會在棧上分配那些不會逃逸的物件,這在理論上是可行的,但是取決於JVM設計者的選擇。據我所知,Oracle HotspotJVM中並未這麼做,這一點在逃逸分析相關的文件裡已經說明,所以可以明確所有的物件例項都是建立在堆上。

目前很多書籍還是基於JDK7以前的版本,JDK已經發生了很大變化,intern字串的快取和靜態變數曾經都被分配在永久代上,而永久代已經被元資料區取代。但是,intern字串快取和靜態變數並不是被轉移到元資料區,而是直接在堆上分配,所以這一點同樣符合前面一點的結論:物件例項都是分配在堆上

11. 本章小結

  1. 年輕代是物件的誕生、生長、消亡的區域,一個物件在這裡產生、應用、最後被垃圾回收器收集、結束生命。

  1. 老年代放置長生命週期物件,通常都是從Survivor區域篩選拷貝過來的Java物件。當然,也有特殊情況,我們知道普通的物件會被分配在TLAB上,如果物件較大,JVM會試圖直接分配在Eden其他位置上;如果物件太大,完全無法在新生代找到足夠長的連續空閒空間,JVM就會直接分配到老年代。

  1. 當GC只發生在年輕代中,回收年輕物件的行為被稱為MinorGC。當GC發生在老年代時則被稱為MajorGC或者FullGC。一般的,MinorGC的發生頻率要比MajorGC高很多,即老年代中垃圾回收發生的頻率大大低於年輕代。
10.2.6 同步省略/鎖消除

  1. 執行緒同步的代價是相當高的,同步的後果是降低併發性和效能。

  1. 在動態編譯同步程式碼塊的時候,JIT編譯器可以藉助逃逸分析來判斷同步程式碼塊所使用的鎖物件是否只能夠被一個執行緒訪問而沒有被髮布到其他執行緒。如果沒有,那麼JIT編譯器在編譯這個同步程式碼塊的時候就會取消對這部分程式碼的同步。這樣就能大大提高併發性和效能。這個取消同步的過程就叫同步省略,也叫鎖消除。

10.2.7 標量替換

  1. 標量Scalar是指一個無法在分解成更小的資料的資料。Java中的原始資料型別就是標量

  1. 相對的,那些還可以分解的資料叫做聚合量(Aggregate),Java中物件就是聚合量,因為它可以分解成其他聚合量和標量

  1. 在JIT階段,如果經過逃逸分析,發現一個物件不會被外界訪問的話,那麼經過JIT優化,就會把這個物件拆解成若干個其中包含的若干個成員變數來替代。這個過程就是標量替換

/**
 * 標量替換測試
 *  -Xmx100m -Xms100m -XX:+DoEscapeAnalysis(逃逸分析) -XX:+PrintGC -XX:-EliminateAllocations(標量替換)
 */
public class ScalarReplace {
    public static class User {
        public int id;
        public String name;
    }
    public static void alloc() {
        User u = new User();//未發生逃逸
        u.id = 5;
        u.name = "www.atguigu.com";
    }
    public static void main(String[] args) {
        long start = System.currentTimeMillis();
        for (int i = 0; i < 10000000; i++) {
            alloc();
        }
        long end = System.currentTimeMillis();
        System.out.println("花費的時間為: " + (end - start) + " ms");
    }
}