一文理清JVM和GC(上)

蔡不菜丶發表於2020-04-06

本文主要介紹JVM和GC解析
本文較長,分為上下篇(可收藏,勿吃塵)
如有需要,可以參考
如有幫助,不忘 點贊

一、前期預熱

1)JVM記憶體體系

其中方法區被JVM中多個執行緒共享,比如類的靜態常量就被存放在方法區,供類物件之間共享,虛擬機器棧本地方法棧程式計數器是每個執行緒獨立擁有的,不會與其他執行緒共享。所以Java在通過new建立一個類物件例項的時候,一方面會在虛擬機器棧中建立一個對該物件的引用,另一方面會在堆上建立類物件的例項,然後將物件引用指向該物件的例項。物件引用存放在每一個方法對應的棧幀中。

圖片來源於網上
圖片來源於網上
  • 虛擬機器棧:虛擬機器棧中執行每個方法的時候,都會建立一個棧幀用於儲存區域性變數表,運算元棧,動態連結,方法出口等資訊。
  • 本地方法棧:與虛擬機器棧發揮的作用相似,相比於虛擬機器棧為Java方法服務,本地方法棧為虛擬機器使用的Native方法服務,執行每個本地方法的時候,都會建立一個棧幀用於儲存區域性變數表,運算元棧,動態連結,方法出口等資訊。
  • 方法區:它用於儲存已被虛擬機器載入的類資訊,常量,靜態變數,即時編譯器編譯後的程式碼等資料,方法區在JDK1.7版本及之前稱為永久代,從JDK1.8之後永久代被移除。
  • 堆:堆是Java物件的儲存區域,任何new欄位分配的Java物件例項和陣列,都被分配在了堆上,Java堆可使用 - Xms / - Xmx 進行記憶體控制,從JDK1.7版本之後,執行時常量池從方法區移到了堆上。
  • 程式計數器:指示Java虛擬機器下一條需要執行的位元組碼指令。

2)JAVA8之後的JVM

從圖中我們可以看出JAVA8的JVM 用元空間取代了永久代

---
---

---
---
---
---

3)GC作用域

---
---

4)常見垃圾回收演算法

  • 引用計數法:

JVM的實現一般不採用這種方式

---
---

缺點:
1. 每次對物件賦值時均要維護引用計數器,且計數器本身也有一定的消耗;
2. 較難處理迴圈引用;

  • 複製演算法:

Java 堆從GC的角度可以細分為:新生代(Eden區、From Survivor區 和 To Survivor區)和 老年代。
特點:
複製演算法不會產生記憶體碎片,但會佔用空間。用於新生代。

---
---

MinorGC的過程(複製 --> 清空 --> 互換)

  1. 複製: (Eden、SurvivorFrom 複製到 SurvivorTo,年齡加1)
    首先,當Eden區滿的時候會觸發第一次GC,把還活著的物件拷貝到SurvivorFrom區,當Eden區再次觸發GC的時候會掃描Eden區域和From區域,對這兩個區域進行垃圾回收,經過這次回收後還存活的物件,則直接複製到To區域(如果有物件的年齡已經到達了老年的標準,則複製到老年代區),同時把這些物件的年齡加1。
  2. 清空:(清空Eden、SurvivorFrom)
    清空Eden和SurvivorFrom中的物件,也即複製之後有交換,誰空誰是to。
  3. 互換:(SurvivorTo和SurvivorFrom 互換)
    最後,SurvivorTo和SurvivorFrom 互換,原SurvivorTo成為下一次GC是的SurvivorFrom區。
  • `標記清除法`

演算法分成標記和清除兩個階段,先標記出要回收的物件,然後統一回收這些。
特點:
不會佔用額外空間,但會掃描兩次,耗時,容易產生碎片,用於老年代

---
---
  • `標記壓縮法`

優點:
沒有記憶體碎片,可以利用bump
缺點:
需要移動物件的成本,用於老年代
原理:

  1. 標記:與標記清除一樣
    ---
    ---

    2.壓縮:再次掃描,並往一段滑動存活物件
    ---
    ---

二、正文接入

1)判斷物件是否可回收

  • `引用計數法`

Java中,引用和物件是有關聯的。如果要操作物件則必須用引用進行。
因此,很顯然的一個方法就是通過引用計數來判斷一個物件是否可以回收。簡單來說就是給物件新增一個引用計數器。每當有一個地方引用它,計數器的值加1,每當有一個引用失效時,計數器的值減1。
任何時刻計數器值為0的物件就是不可能再被使用的,那麼這個物件就是可回收物件。
缺點: 很難解決物件之間相互迴圈引用的問題

---
---
  • `列舉根節點做可達性分析(根搜尋路徑)`

所謂“GC roots” 或者說tracing GC 的 "根集合" 就是一組必須活躍的引用。
基本思路就是通過一系列名為“GC Roots” 的物件作為起始點,從這個被稱為GC Roots的物件開始向下搜尋,如GC Roots沒有任何引用鏈相連是,則說明此物件不可用。也即給定一個集合的引用作為根出發,通過引用關係

圖片來源於網上
圖片來源於網上

2)哪些可以做GCRoots物件

  • 虛擬機器棧(棧幀中的區域性變數區,也叫做區域性變數表)
  • 方法區中的類靜態屬性引用的物件
  • 方法區中常量引用的物件
  • 本地方法棧中N(Native方法)引用的物件

3)JVM的引數型別

  • 標配引數
    • java -version
    • java -help
      ---
      ---
  • X引數
    • java -Xint -version :解釋執行
    • java -Xcomp -version :第一次使用就編譯成原生程式碼
    • java -Xmixed :混合模式
      ---
      ---
  • XX引數
    • Boolean型別
      > -XX:+ 或者 - 某個屬性值
      > +:表示開啟
      > -:表示關閉
      > `例子:`
      > -XX: +PrintGCDetails: 開啟列印GC收集細節
      > -XX: -PrintGCDetails: 關閉列印GC收集細節
      > -XX: +UseSerialGC: 開啟序列垃圾收集器
      > -XX: -UseSerialGC: 關閉序列垃圾收集器
    • KV設定型別
      > -XX: 屬性key = 屬性value
      > `例子:`
      > -XX: MetaspaceSize = 128m:設定元空間大小為128m
      > -XX:MaxTenuringThreshold = 15: 控制新生代需要經歷多少次GC晉升到老年代中的最大閾值
    • jinfo-檢視當前執行程式的配置
      > 公式: jinfo -flag 配置項 程式編號
      > `例子:`
      > 1. 檢視初始堆大小:
      ---
      ---

      >2. 檢視其他引數
      ---
      ---

      > 3. 檢視使用哪種垃圾回收器
      ---
      ---
  • 兩個經典引數
    • -Xms 等價於 -XX: InitialHeapSize
    • -Xmx 等價於 -XX: MaxHeapSize

4)檢視JVM預設值

  • -XX:+PrintFlagsInitial 檢視預設初始值
  • java -XX: +PrintFlagsInitial -version
  • java -XX: +PrintFlagsInitial
    ---
    ---
  • -XX:+PrintFlagsFinal 檢視修改更新
    • java -XX:+PrintFlagsFinal
    • java -XX:+PrintFlagsFinal -version
    • java -XX:+PrintCommandedLineFlags
      ---
      ---

5)常用的配置引數

經典案例設定:
-Xms128m -Xmx4096m -Xss1024k -XX:Metaspacesize=512m -XX:+PrintCommandLineFlags -XX:PrintGCDetails -XX:UseSerialGC

  • -Xms

初始化大小記憶體,預設為實體記憶體1/64
等價於 -XX:InitialHeapSize

  • -Xmx

最大分配記憶體,預設為實體記憶體1/4
等價於 -XX:MaxHeapSize

  • -Xss

設定單個執行緒的大小,一般預設為5112K~1024K
等價於 -XX:ThreadStackSize

  • -Xmn

設定年輕代大小

  • -XX:MetaspaceSize

設定元空間大小

---
---
  • -XX:+PrintGCdetails

輸出詳細的GC收集日誌資訊

---
---
---
---
  • -XX:SurvivorRatio

設定新生代中eden和S0/S1空間的比例
預設:
-XX:SurvivorRatio=8 --> Eden:S0:S1=8:1:1
修改:
-XX:SurvivorRatio=4 --> Eden:S0:S1=4:1:1
SurvivorRatio值就是設定eden區的比例佔多少,S0/S1相同

  • -XX:NewRatio

設定年輕代與老年代在堆結構的佔比
預設
-XX:NewRatio=2 新生代佔1,老年代佔2,年輕代佔整個堆的1/3
修改
-XX:NewRatio=4 新生代佔1,老年代佔4,年輕代佔整個堆的1/5
NewRatio值就是設定老年代的佔比,剩下的1給新生代

  • -XX:MaxTenuringThreshold

設定垃圾最大年齡
-XX:MaxTenuringThreshold=0:設定垃圾最大年齡。如果設定為0的話,則年輕代物件不經過Survivor區,直接進入老年代。對於老年代比較多的應用,可以提高效率。如果將此值設定為一個較大值,則年輕代物件會在Survivor區進行多次複製,這樣可以增加物件在年輕代的存活時間,增加年輕代被回收的概論。

6)強軟弱虛

圖片來源於網路
圖片來源於網路

圖片來源於網路
圖片來源於網路
  • 強引用
    • 當記憶體不足,JVM開始垃圾回收,對於強引用的物件,就算出現了OOM也不會對該物件進行回收,死都不收
    • 強引用是我們最常見的普通物件引用,只要還有強引用指向一個物件,就能表名物件還“活著”,垃圾收集器不會碰這種物件。在Java中最常見的就是強引用,把一個物件賦給一個引用變數,這個引用變數就是一個強引用。當一個物件被強引用變數引用時,它處於可達狀態,它是不可能被垃圾回收機制回收的。即使該物件以後永遠都不會被用到,JVM也不會回收。 因此強引用是造成Java記憶體洩漏的主要原因之一。
    • 對於一個普通的物件,如果沒有其他的引用關係,只要超過了引用的作用域或者顯式地將相應(強)引用賦值為null,一般就是認為可以被垃圾收集(具體看垃圾收集策略)
public static void main(String[] args) {
        Object o1 = new Object();   //預設為強引用
        Object o2 = o1;     //引用賦值
        o1 = null;          //置空 讓垃圾收集
        System.gc();
        System.out.println(o1);     // null
        System.out.println(o2);     // java.lang.Object@1540e19d
    }
複製程式碼
  • 軟引用
    • 軟引用就是一種相對強引用弱化了一些的引用。需要用java.lang.ref.SoftReference類來實現,可以讓物件豁免一些垃圾收集。
    • 系統記憶體充足 -> 不會回收
    • 系統記憶體不足 -> 會回收
    • 軟引用通常用在對記憶體敏感的程式中,比如快取記憶體就有用到軟引用,記憶體夠用的時候就保留,不夠用就回收
    public static void main(String[] args) {
        Object o1 = new Object();
        SoftReference softReference = new SoftReference(o1);
        o1 = null;
        System.gc();
        System.out.println(o1);
        System.out.println(softReference.get());
    }
複製程式碼
  • 弱引用
    • 弱引用需要用java.lang.ref.WeakReference類來實現,它比軟引用的生存期更短
    • 對於弱引用的物件,只要垃圾回收機制一執行,不管JVM的記憶體空間是否足夠,都會回收該物件佔用的記憶體。
    public static void main(String[] args) {
        Object o1 = new Object();
        WeakReference weakReference = new WeakReference(o1);
        o1 = null;
        System.gc();
        System.out.println(o1);                     //null
        System.out.println(weakReference.get());    //null
    }
複製程式碼
  • 虛引用
    • 虛引用需要java.lang.ref.PhantomReference類來實現。
    • 形如虛設,它不會決定物件的生命週期。
    • 如果一個物件持有虛引用,那麼它就和沒有任何一樣,在任何時候都可能被垃圾回收器回收, 它不能單獨使用也不能通過它來訪問物件,虛引用必須和引用佇列(ReferenceQueue)聯合使用。
    • 虛引用的主要作用是跟蹤物件被垃圾回收的狀態,僅僅是提供了一種確保物件被finalize以後,做某些事情的機制。PhantomReference的get方法總是返回null,因此無法訪問對應的引用物件。其意義在於說明一個物件已經進入finalization階段,可以被gc回收,用來實現比finalization機制更靈活的回收操作。
public static void main(String[] args) {
        Object o1 = new Object();
        ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
        PhantomReference<Object> phantomReference = new PhantomReference<>(o1,referenceQueue);
        System.out.println(o1);                         //java.lang.Object@1540e19d
        System.out.println(phantomReference.get());     //null
        System.out.println(referenceQueue.poll());      //null
    }
複製程式碼
  • 擴充套件
    • 軟弱引用適用場景 > 假如有一個引用需要讀取大量的本地圖片 >存在問題:
      1. 如果每次讀取圖片都從硬碟讀取則會嚴重影響效能。
      2. 如果一次性全部載入到記憶體中有可能造成記憶體溢位。
        >解決思路:
        > 用一個HashMap來儲存圖片的路徑和相應圖片物件關聯的軟引用之間的對映關係,在記憶體不足時,JVM會自動回收這些快取圖片物件所佔用的空間,從而有效地避免了OOM的問題。
        Map imgMap = new HashMap();
    • WeakHashMap
public static void main(String[] args) {
        WeakHashMap<Integer,String> weakHashMap = new WeakHashMap<>();
        Integer key = new Integer(1);
        weakHashMap.put(key,"測試1");
        System.out.println(weakHashMap);    //{1=測試1}
        key=null;
        System.out.println(weakHashMap);    //{1=測試1}
        System.gc();
        System.out.println(weakHashMap+"\t"+weakHashMap.size());    //{} 0
    }
複製程式碼

相關文章