GC是什么?為什么要GC?
GC( Garbage Collection ),垃圾回收,是Java與C++的主要區別之一。作為Java開發者,一般不需要專門編寫內存回收和垃圾清理代碼。這是因為在Java虛擬機中,存在自動內存管理和垃圾清理機制。對JVM中的內存進行標記,并確定哪些內存需要回收,根據一定的回收策略,自動的回收內存,保證JVM中的內存空間,防止出現內存泄露和溢出問題。
GC是任意時候都能進行的嗎
GC垃圾收集只能在安全點才能進行。在Java虛擬機(JVM)中,安全點(Safe Point)是程序執行的某些特定位置。JVM只能在安全點安全地暫停執行,從而進行垃圾回收(GC)等操作。安全點的設定確保了當線程暫停時,程序的狀態是可知和一致的。
如何判斷一個對象是否存活?
引用計數法
給對象中添加一個引用計數器,每當有一個地方引用它,計數器就加 1;當引用失效,計數器就減 1;任何時候計數器為 0 的對象就是不可能再被使用的。
優點:可即刻回收垃圾,當對象計數為0時,會立刻回收;
弊端:循環引用時,兩個對象的計數都為1,導致兩個對象都無法被釋放。JVM不用這種算法
可達性分析算法
通過 GC Root 對象為起點,從這些節點向下搜索,搜索所走過的路徑叫引用鏈,當一個對象到 GC Root沒有任何的引用鏈相連時,說明這個對象是不可用的。
-
JVM中的垃圾回收器通過可達性分析來探索所有存活的對象
-
掃描堆中的對象,看能否沿著GC Root對象為起點的引用鏈找到該對象,如果找不到,則表示可以回收
GC Root的對象有哪些?
-
虛擬機棧(棧幀中的本地變量表)中引用的對象,例如各個線程被調用的方法棧用到的參數、局部變量或者臨時變量等。
-
方法區中類靜態屬性引用的對象或者說Java類中的引用類型的靜態變量。
-
方法區中常量引用的對象或者運行時常量池中的引用類型變量。
-
本地方法棧中JNI(即一般說的Native方法)引用的對象
-
JVM內部的內存數據結構的一些引用、同步的監控對象(被修飾同步鎖)。
finalize()方法的作用?
finalize() 類似 C++ 的析構函數,用來做關閉外部資源等工作。但是 try-finally 等方式可以做的更好,并且該方法運行代價高昂,不確定性大,無法保證各個對象的調用順序,因此最好不要使用。(Java 9中已棄用)
當一個對象可被回收時,如果需要執行該對象的 finalize() 方法,那么就有可能通過在該方法中讓對象重新被引用,從而實現自救。自救只能進行一次,如果回收的對象之前調用了 finalize() 方法自救,后面回收時不會調用 finalize() 方法。
Java種有哪些引用類型?有什么特點?
-
強引用:gc時不會回收
-
軟引用:只有在內存不夠用時,gc才會回收
-
弱引用:只要gc就會回收;弱引用對象非常適合于實現 Map 的緩存(weakHashMap),當對象只通過弱引用可達時,可以快速釋放內存。
-
虛引用:是否回收都找不到引用的對象,僅用于管理直接內存
什么時候需要安全點?安全點的觸發條件?
安全點的作用:
- 垃圾收集: 在進行垃圾收集時,JVM需要暫停所有應用程序線程(GC暫停),以確保不會有線程在操作內存。同時,狀態的快照是確定的,以便于GC工作。
- 堆棧遍歷: 在執行如線程轉儲(Thread Dump)等操作時,JVM需要安全地遍歷線程棧,這時也需要安全點。
- 性能損耗最小化: 通過在最可能長時間運行的指令設置安全點(例如循環的末端、方法的調用與返回),JVM可以減少程序暫停的頻率,從而降低性能損耗。
安全點的觸發條件:
- 方法調用:每次方法調用都是一個潛在的安全點。
- 循環回跳:長時間循環中間會插入安全點檢查。
- 異常處理:處理異常時,也會檢查是否到達安全點。
young Gc、old Gc、full Gc 和 mixed Gc 的區別是什么?
Young GC(Minor GC或 YGC),即年輕代垃圾回收:
- 作用范圍:僅針對新生代(Eden和S0/S1)。
- 觸發條件:當新生代內存(尤其是 Eden 區)被填滿時觸發。
- 執行方式:只回收新生代中的對象,老年代不受影響。
- 特點:回收頻率較高,回收時間較短,因為新生代中的對象大多數是短命對象,容易被回收。
Old Gc(Major GC或OGC),老年代垃圾回收:
- 作用范圍:只針對老年代。
- 觸發條件:當老年代空間不足時觸發,通常是當從新生代晉升到老年代的對象過多,或者老年代的存活對象數量達到一定閾值時。
- 執行方式:只回收老年代的對象,新生代不受影響。
- 特點:執行時間比 Young GC 長,因為老年代中的對象存活時間更長,且數量較多
Full GC,全堆垃圾回收:
- 作用范圍:對整個堆內存(包括新生代和老年代)進行回收。
- 觸發條件:當老年代空間不足且無法通過老年代垃圾回收釋放足夠空間,或其他情況導致系統內存壓力較大時觸發(如 system.gc()調用)
- 執行方式:回收所有代(新生代、老年代)中的垃圾,并且可能會伴隨著元空間的回收。
- 特點:回收時間最長,會觸發整個JM 的停頓(Stop-The-World),對性能有較大影響,通常不希望頻繁發生
Mixed Gc(僅適用于 G1 GC 的混合垃圾回收):
- 作用范圍:同時回收新生代和部分老年代區域
- 觸發條件:當 G1垃圾回收器發現老年代區域的垃圾過多時觸發。
- 執行方式:混合回收新生代和部分老年代區域,主要目的是減少老年代中的垃圾積壓。
- 特點:結合了 YGC 的快速回收和 OGC的深度回收,盡量減少停頓時間,適用于大內存應用
對象在堆中的生命周期?
-
在 JVM 內存模型的堆中,堆被劃分為新生代和老年代
- 新生代又被進一步劃分為 Eden區Survivor區From SurvivorTo Survivor
-
當創建一個對象時,對象會被優先分配到新生代的 Eden 區
- 此時 JVM 會給對象定義一個對象年輕計數器 -XX:MaxTenuringThreshold
-
當 Eden 空間不足時,JVM 將執行新生代的垃圾回收(Minor GC)
-
JVM 會把存活的對象轉移到 Survivor 中,并且對象年齡 +1
-
對象在 Survivor 中同樣也會經歷 Minor GC,每經歷一次 Minor GC,對象年齡都會+1
-
-
如果分配的對象超過了 -XX:PetenureSizeThreshold 直接被分配到老年代
內存的分配策略?
-
對象優先在 Eden 分配: 大多數情況下,對象在新生代 Eden 上分配,當 Eden 空間不夠時,觸發 Minor GC
-
大對象直接進入老年代: 當遇到一個較大的對象時,就算新生代的伊甸園為空,也無法容納該對象時,會將該對象直接晉升為老年代,最典型的大對象有長字符串和大數組。可以設置JVM參數 -XX:PretenureSizeThreshold ,大于此值的對象直接在老年代分配。
-
長期存活的對象進入老年代: 通過參數 -XX:MaxTenuringThreshold 可以設置對象進入老年代的年齡閾值。對象在 Survivor 區每經過一次 Minor GC ,年齡就增加 1 歲,當它的年齡增加到一定程度,就會被晉升到老年代中。
-
動態對象年齡判定: 并非對象的年齡必須達到 MaxTenuringThreshold 才能晉升老年代,如果在 Survivor 中相同年齡所有對象大小的總和大于 Survivor 空間的一半,則年齡大于或等于該年齡的對象可以直接進入老年代,無需達到 MaxTenuringThreshold 年齡閾值。
-
空間分配擔保: 在發生 Minor GC 之前,虛擬機先檢查老年代最大可用的連續空間是否大于新生代所有對象總空間,如果條件成立的話,那么 Minor GC 是安全的。如果不成立的話虛擬機會查看HandlePromotionFailure 的值是否允許擔保失敗。如果允許,那么就會繼續檢查老年代最大可用的連續空間是否大于歷次晉升到老年代對象的平均大小,如果大于,將嘗試著進行一次 Minor GC,盡管這次Minor GC是有風險的;(也就是說,會把原先新生代的對象挪到老年代中) ;如果小于,或者 HandlePromotionFailure 的值為不允許擔保失敗,那么就要進行一次 Full GC 。
空間分配擔保時的 “冒險”是冒了什么風險?
新生代使用復制收集算法,但為了內存利用率,只使用其中一個Survivor空間來作為輪換備份,因此當出現大量對象在Minor GC后仍然存活的情況(最極端的情況就是內存回收后新生代中所有對象都存活),就需要老年代進行分配擔保,把Survivor無法容納的對象直接進入老年代。但前提是老年代本身還有容納這些對象的剩余空間,一共有多少對象會活下來在實際完成內存回收之前是無法明確知道的,所以只好取之前每一次回收晉升到老年代對象容量的平均大小值作為經驗值,與老年代的剩余空間進行比較,決定是否進行Full GC來讓老年代騰出更多空間。
取平均值進行比較其實仍然是一種動態概率的手段,也就是說,如果某次Minor GC存活后的對象突增,遠遠高于平均值的話,依然會導致擔保失敗(Handle Promotion Failure)。如果出現了HandlePromotionFailure失敗,那就只好在失敗后重新發起一次Full GC。雖然擔保失敗時繞的圈子是最大的,但大部分情況下都還是會將HandlePromotionFailure開關打開,避免Full GC過于頻繁。
Young GC的觸發條件
在Java 中,Young GC(Minor GC)是針對新生代(Young Generation)對象的垃圾回收。主要有三種情況會會觸發 Young GC:
- Eden 區空間不足:新生代被劃分為三個區域,Eden區、S0(Survivor0)區和S1(Survivor 1)區,大部分新創建的對象會先分配到 Eden 區。當 Eden 區的對象填滿,無法再為新的對象分配空間時,Young GC 會被觸發,回收新生代中不再使用的對象。
- Eden 區+Survivor 區都裝滿:如果 Eden 區和 Survivor 區的空間都不足以存放新分配的對象時,Young GC 也會被觸發,清理空間并將幸存的對象轉移到 Survivor 區或老年代
- 部分垃圾回收器在 full gc 之前:有一些收集器的回收實現是在 full gc 前會讓先執行以下 young gc。比如 Parallel Scavenge,不過有參數可以調整讓其不進行 young gc。
Full GC 的觸發條件?
對于 Minor GC,其觸發條件非常簡單,當 Eden 空間滿時,就將觸發一次 Minor GC。而 Full GC 則相對復雜,有以下條件:?
-
用 System.gc(): 只是建議虛擬機執行 Full GC,但是虛擬機不一定真正去執行。不建議使用這種方式,而是讓虛擬機管理內存。
-
老年代空間不足: 老年代空間不足的常見場景為前文所講的大對象直接進入老年代、長期存活的對象進入老年代等。為了避免以上原因引起的 Full GC,應當盡量不要創建過大的對象以及數組、注意編碼規范避免內存泄露。除此之外,可以通過 -Xmn 參數調大新生代的大小,讓對象盡量在新生代被回收掉,不進入老年代。還可以通過 -XX:MaxTenuringThreshold 調大對象進入老年代的年齡,讓對象在新生代多存活一段時間。
-
空間分配擔保失敗: 當程序創建一個大對象時,Eden區域放不下大對象,使用復制算法的 Minor GC 需要老年代的內存空間作擔保,如果擔保失敗會執行一次 Full GC。
-
JDK 1.7 及以前的永久代空間不足: 在 JDK 1.7 及以前,HotSpot 虛擬機中的方法區是用永久代實現的,永久代中存放的為一些 Class 的信息、常量、靜態變量等數據。當系統中要加載的類、反射的類和調用的方法較多時,永久代可能會被占滿,在未配置為采用 CMS GC 的情況下也會執行 Full GC。如果經過 Full GC 仍然回收不了,那么虛擬機會拋出 java.lang.OutOfMemoryError 。(JDK 8以后元空間不足)
-
Concurrent Mode Failure:執行 CMS GC 的過程中同時有對象要放入老年代,而此時老年代空間不足(可能是 GC 過程中浮動垃圾過多導致暫時性的空間不足),便會報 Concurrent Mode Failure 錯誤,并觸發 Full GC。