文章目錄
一、什麼是GC
GC(Garbage Collection)垃圾收集,回收垃圾,釋放內存(那Java中的垃圾是什麼呢?垃圾一般是沒有被任何對象引用的對象),Java 提供的 GC 功能可以自動監測對象是否過期(超過生命週期)從而達到自動清除過期對象回收內存的目的。
二、爲什麼要了解GC
在實際項目中,可能會遇到內存泄漏的問題,我們需要對項目業務進行合理的內存分配,需要對其進行監控和調節,在這之前,我們需要了解GC。
三、對象被判定爲垃圾的標準
前邊已經說過了,沒有被任何對象引用的對象被判定爲垃圾
四、判斷對象是否爲垃圾的算法
- 引用計數算法
- 可達性分析算法
4.1、引用計數算法
- 通過判斷對象的引用數量來決定對象是否可以被回收
- 每個對象實例都有一個引用計數器,被引用則+1,完成引用則-1
- 任何引用計數爲0 的對象實例可以被當做垃圾收集
- 優點:執行效率高,程序執行受影響小
- 缺點:無法檢測出循環引用的情況,導致內存泄漏
比如下邊這個例子:
package com.mtli.jvm.gc;
/**
* @Description:
* @Author: Mt.Li
* @Create: 2020-04-28 13:38
*/
public class MyObject {
public MyObject childNode;
}
package com.mtli.jvm.gc;
/**
* @Description:
* @Author: Mt.Li
* @Create: 2020-04-28 13:39
*/
public class ReferenceCountProblem {
public static void main(String[] args) {
MyObject object1 = new MyObject();
MyObject object2 = new MyObject();
// 循環引用
object1.childNode = object2;
object2.childNode = object1;
}
}
這種情況下,引用是無法爲0的,無法檢測,這也是一個致命短板
4.2、可達性分析算法
- 主流的Java垃圾收集器採用的是可達性分析算法。它通過判斷對象的引用鏈是否可達來決定對象是否可以被回收。
- 程序把所有的引用關係看做一張有向圖,將名稱爲gc root 的對象作爲起始點,從這些節點開始向下搜索,其所走過的路徑被稱爲引用鏈(reference chain),路徑上所有遇到的對象都標記爲存活,當gc root到某個對象沒有任何引用鏈時,就是說gc root到對象是不可達的,那麼該對象也就被標記爲垃圾了。
紅色部分是GC Root可達的對象,標記存活,綠色的是不可達的,即垃圾對象。
那麼問題來了什麼對象可以作爲GC Root的對象? - 虛擬機棧中引用的對象(棧幀中的本地變量表)
- 方法區中的常量引用的對象,即常量保存的是該對象的地址
- 方法區中的類靜態屬性引用的對象(類似於常量情況)
- 本地方法棧中INI(Native方法)的引用對象
- 活躍線程的引用對象也可以作爲GC Root
五、垃圾回收算法
說完如何判定垃圾的算法後,當然是要進行垃圾回收了,這又有哪些算法呢?
- 標記-清除算法(Mark and Sweep)
- 複製算法(Copying)
- 標記-整理算法(Compacting)
5.1、標記-清除算法
- 標記:從根集合進行掃描,對存活的對象進行標記
- 清除:對堆內存從頭到尾進行線性遍歷,回收不可達對象內存
缺點: - 容易造成大量碎片
- 上圖中,我們回收掉不可達對象之後,剩下的存活對象處於不連續的內存空間中,造成碎片化,當我們想要插入一個新的對象,並且需要的連續內存較大(比如需要3個單位)圖中最多隻有兩個單位,那麼就要進行新一輪的垃圾回收,這樣也會導致新的碎片出現,也會浪費時間,降低效率。
5.2、複製算法
-
分爲對象面和空閒面
-
對象在對象面上創建
-
當被定義的對象面用完的時候,將還存活的對象複製到空閒面,然後將該對象面所有對象內存清除
-
順序分配內存,簡單高效
-
解決碎片化問題
-
適用於對象存活率低的場景,如年輕代
在這裏插入圖片描述
優點很明顯,解決了碎片化的問題,複製算法不使用鏈表,分塊直接用連續的內存空間,放入新對象只需要找到滿足該對象內存大小的內存塊放入即可,省去了大量時間。
缺點: -
這種算法,需要留出一半的空間給空閒面,造成堆使用率低下
-
不適合高存活率的場景,那樣的話每次複製要複製很多,性能上不允許,故不適合老年代
5.3、標記-整理算法
- 標記:從根集合進行掃描,對存活的對象進行標記
- 清除:移動所有存活的對象,且按照內存地址次序一次排列,然後將末端內存地址以後的內存全部回收
- 在標記-清除算法上增加了對象的移動,增加了成本,卻解決了內存碎片的問題
- 適用於存活率極高的場景,如老年代
5.4、主流回收算法——分代收集算法(Generation Collector)
- 按照對象生命週期的不同劃分區域以採用不同的垃圾回收算法
- 目的:提高JVM的回收效率
- 年輕代採用複製算法
- 老年代採用標記-清除算法、**標記-整理算法
JDK1.8中的分代
JDK1.6中的分代
六、年輕代的垃圾收集
年輕代佔堆空間的1/3,是爲了儘可能快速地收集掉那些生命週期短的對象,它有如下分區
- Eden區(新創建的對象一般放在這裏),佔新生代的8/10
- 兩個Survivor區,一個爲From區一個是To區(不是固定的,根據GC執行而定的),是爲了配合複製算法
下面我們用圖來解釋:
我們假設Eden能存4個對象,Survivor分別能存3個對象
1.現在Eden(臨時急救中心)中有4個對象,其中三個掛了,需要回收屍體,空出位置,另外一個需要轉移到醫院,現在有S0和S1兩個醫院,規定是必須有個醫院作爲待命的to隨時準備接收病人,另一個則作爲接診區from如下圖,S0轉移進一個則S0此時是from,S1是to,且病人編號+1
2.現在Eden又來了四個急救病人,有倆沒挺住,另外兩個存活的要送醫院,爲了方便接收和明示病人批次病人編號都+1,我們要將S0醫院的轉移到to中,剛進來的兩個也要轉到to中,兩家醫院的標識對換,病人編號都+1(以此類推)。複製完後S0是to,S1是from
3.沒想到壞事並行啊,這下又有四個人進到Eden臨時急救中心,可惜的是隻有一個活了下來,禍不單行,醫院S1的有一個沒救過來,掛了。剛好都給他們轉到S0去,S1留着待命。此時S0變成from,S1變成to。
最後情況(Eden可以是有對象的):
(回收Eden和Survivor區的過期對象的過程被稱爲Minor GC)
當編號達到某個值時(由 -XX:MaxTenuringThreshold定義,可以是15或者自定義),需要轉移到老年代去。當然情況不是唯一,有的對象較大,Eden裝不下或者S區裝不下,經過上面的程序,容納不下,就會進入到老年代。
對象如何晉升到老年代
- 經歷一定Minor次數依然存活的對象
- Survivor區或者Eden區裝不下的對象
- 新生成的大對象( -XX: +PretenuerSizeThreshold)
常用的調優參數
- -XX:SurvivorRatio : Eden和Survivor的比值,默認8:1
- -XX:NewRatio : 老年代和年輕代內存大小的比例,比如2:1
- -XX:MaxTenuring Threshold : 對象從年輕代晉升到老年代經過GC次數的最大閾值
七、老年代的垃圾收集
老年代(2/3堆空間):
- 存放生命週期較長的對象
- 標記-清理算法
- 標記-整理算法
- 垃圾回收:Full GC和Major GC。major gc 通常與Full GC等價,只是由於HotSpot VM發展了這麼多年,外界對各種名詞的解讀已經完全混亂了,所以說到Major的時候,要詢問清楚是full gc還是old gc
Full GC(全局範圍的GC)
Full GC比Minor GC慢很多,它是全局範圍的GC,清理年輕代、老年代、元空間
觸發條件:
- 老年代空間不足
- 永久代空間不足(jdk1.7之前)
- CMS GC時出現promotion failed ,concurrent mode failure
- Minor GC晉升到老年代的平均大小大於老年代的剩餘空間
- 調用System.gc(),對老年代和新生代進行回收,具體還是由虛擬機執行
可以這麼理解,當觸發老年代GC時,通常也會觸發年輕代GC回收,也就是對整個堆進行垃圾回收,即full GC。
Stop-the-World
- JVM由於要執行GC而停止應用程序的執行
- 任何一種GC算法中都會發生
- 多數GC優化用減少Stop-the-world發生的時間來提高程序性能
SafePoint
-
分析過程中對象引用關係不會發生變化的點
-
產生Safepoint的地方:方法調用;循環跳轉;異常跳轉等等
-
一旦GC發生,所有的線程跑到安全點再停頓下來,如果某個線程不在安全點,就等其到達安全點,再GC
-
安全點數量要適中,太少會讓GC等待很久,太多會增加程序運行的負荷
八、常見的垃圾收集器
JVM運行模式
- Server,啓動較慢,長期穩定運行後,程序運行速度要比Client快。因爲採用重量級虛擬機,其中對程序有更多優化
- Client,啓動較快
可以在cmd中查詢java -version
默認是以Server啓動
垃圾收集器之間的聯繫
(借圖)
8.1、年輕代常見的垃圾收集器
Serial收集器(-XX:+UseSerialGC,複製算法)
-
1.3版本之前年輕代的唯一選擇
-
單線程收集,進行垃圾收集時,必須暫停所有工作線程
-
簡單高效,Client模式下默認的年輕代收集器
ParNew收集器(-XX:+UseParNewGC,複製算法)
- 多線程收集,其餘的行爲、特點和Serial收集器一樣
- 單核CPU情況下執行效率不如Serial,在多核下執行纔有優勢
- 默認開啓線程數和CPU核數相同,可以加限制
Parallel Scavenge收集器(-XX:+UseParallelGC,複製算法)
- 吞吐量=運行用戶代碼時間/(運行用戶代碼+垃圾收集時間)
- 比起其他的收集器關注用戶線程停頓時間,它更關注系統的吞吐量
- 在多核下執行纔有優勢,Server模式下默認的年輕代收集器
- 調優參數(-XX:+UseAdaptiveSizePolicy),如果開啓 AdaptiveSizePolicy,則每次 GC 後會重新計算 Eden、From 和 To 區的大小,計算依據是 GC 過程中統計的 GC 時間、吞吐量、內存佔用量。
8.2、老年代常見收集器
Serial Old收集器( -XX:USeSerialOldGC,標記-整理算法)
- 單線程收集,進行垃圾收集時,必須暫停所有工作線程
- 簡單高效,Client模式下默認的老年代收集器
CMS收集器(-XX:+UseConcMarkSweepGC,標記-清除算法)(老年代收集器的半壁江山)
- 初始標記:stop-the-world(std),虛擬機會停頓正在執行的任務,從垃圾回收的根對象開始,只掃描到能夠和根對象關聯的對象,並做標記,並不會耗時太多,很快就能完成。
- 併發標記:併發追溯標記,程序不會停頓,該階段緊隨初始標記,會繼續向下追溯標記,不會停止應用線程,兩者併發執行
- 併發預處理:查找執行併發標記階段從年輕代晉升到老年代的對象,減少下一個階段——重新標記的工作量(重新標記會暫停虛擬機)
- 重新標記:暫停虛擬機,掃描CMS堆中的剩餘對象。從跟對象開始一直向下追溯所有對象,比較耗時
- 併發清理:清理垃圾對象,程序不會停頓
- 併發重置:重置CMS收集器的數據結構
缺點:
- 如果在併發標記後產生的垃圾,只能等待本次清理完畢
- 最大的問題是本收集器採用的是標記-清除算法,會產生比較多的不連續的碎片空間,如果碎片空間過多,但是又需要一塊較大的連續空間,這時候只能進行一次GC了
G1收集器(-XX:+UseG1GC,複製+標記-整理算法)
- 並行和併發
- 分代收集
- 空間整合(用標記-整理算法)
- 可預測的停頓
- 將整個Java內存劃分爲多個大小相等的Region
- 年輕代和老年代不再物理隔離。堆內存被劃分爲多個大小相等的 heap 區,每個heap區都是邏輯上連續的一段內存(virtual memory). 其中一部分區域被當成老一代收集器相同的角色(eden, survivor, old), 但每個角色的區域個數都不是固定的。這在內存使用上提供了更多的靈活性。
- 相比CMS,G1消除了內存碎片的問題
詳解及其執行過程可參考G1垃圾收集器入門
九、關於Java中的強引用、軟引用、弱引用、虛引用
強引用(Strong Reference)
- 最普遍的引用:Object obj = new Object()
- GC哪怕是拋出OOM終止程序也不會回收具有強引用的對象
- 通過將對象設置爲null來弱化引用,使其被回收(等待其超過生命週期)
軟引用(Soft Reference)
-
對象處在有用但非必須的狀態
-
只有當內存空間不足時,GC會回收該引用的對象的內存
-
可以用來實現高速緩存
-
使用方法:
-
String str = new String("abc"); // 強引用 SoftReference<String> softRef = new SoftReference<String>(str); // 軟引用
-
弱引用(Weak Reference)
-
非必須的對象,比軟引用更弱一些
-
GC時會被回收(無論內存資源是否緊缺)
-
被回收的概率也不大,因爲GC線程優先級比較低
-
適用於引用偶爾被使用且不影響垃圾收集的對象
-
String str = new String("abc"); // 強引用 WeakReference<String> weakRef = new WeakReference<String>(str); // 弱引用
虛引用(PhantomReference)
-
不會決定對象的生命週期
-
任何時候都可能被垃圾收集器回收
-
跟蹤對象被垃圾收集器回收的活動,起哨兵作用,GC在回收對象時,會先判斷該對象是否有虛引用,如果有,則加入到引用隊列中,程序可以判斷,引用隊列是否已經加入虛引用來了解被引用的對象是否被回收
-
必須和引用隊列ReferenceQueue聯合使用
-
String str = new String("abc"); ReferenceQueue queue = new ReferenceQueue(); PhantomReference ref = new PhantomReference(str, queue);
綜上,Java中的引用強弱級別爲:
強引用>軟引用>弱引用>虛引用
引用類型 | 被垃圾回收時間 | 用途 | 生存時間 |
---|---|---|---|
強引用 | 從來不會 | 對象的一般狀態 | JVM停止運行時終止 |
軟引用 | 在內存不足時 | 對象緩存 | 內存不足時終止 |
弱引用 | 在垃圾回收時 | 對象緩存 | gc運行後終止 |
虛引用 | Unknown | 標記、哨兵 | Unknown |
以上均爲個人理解,如有不足,請及時在評論區提出,謝謝。