本文主要介紹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的過程(複製 --> 清空 --> 互換)
- 複製: (Eden、SurvivorFrom 複製到 SurvivorTo,年齡加1)
首先,當Eden區滿的時候會觸發第一次GC,把還活著的物件拷貝到SurvivorFrom區,當Eden區再次觸發GC的時候會掃描Eden區域和From區域,對這兩個區域進行垃圾回收,經過這次回收後還存活的物件,則直接複製到To區域(如果有物件的年齡已經到達了老年的標準,則複製到老年代區),同時把這些物件的年齡加1。 - 清空:(清空Eden、SurvivorFrom)
清空Eden和SurvivorFrom中的物件,也即複製之後有交換,誰空誰是to。 - 互換:(SurvivorTo和SurvivorFrom 互換)
最後,SurvivorTo和SurvivorFrom 互換,原SurvivorTo成為下一次GC是的SurvivorFrom區。
- `標記清除法`
演算法分成標記和清除兩個階段,先標記出要回收的物件,然後統一回收這些。
特點:
不會佔用額外空間,但會掃描兩次,耗時,容易產生碎片,用於老年代
- `標記壓縮法`
優點:
沒有記憶體碎片,可以利用bump
缺點:
需要移動物件的成本,用於老年代
原理:
- 標記:與標記清除一樣
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. 檢視使用哪種垃圾回收器
- Boolean型別
兩個經典引數
- -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
}
複製程式碼
擴充套件
- 軟弱引用適用場景
> 假如有一個引用需要讀取大量的本地圖片
>存在問題:
- 如果每次讀取圖片都從硬碟讀取則會嚴重影響效能。
- 如果一次性全部載入到記憶體中有可能造成記憶體溢位。
>解決思路:
> 用一個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
}
複製程式碼