什么是Java里的垃圾回收?如何觸發垃圾回收?
垃圾回收(Garbage Collection, GC)是自動管理內存的一種機制,它負責自動釋放不再被程序引用的對象所占用的內存,這種機制減少了內存泄漏和內存管理錯誤的可能性。垃圾回收可以通過多種方式觸發,具體如下:
- 內存不足時:當JVM檢測到堆內存不足,無法為新的對象分配內存時,會自動觸發垃圾回收。
- 手動請求:雖然垃圾回收是自動的,開發者可以通過調用?
System.gc()
?或?Runtime.getRuntime().gc()
?建議 JVM 進行垃圾回收。不過這只是一個建議,并不能保證立即執行。 - JVM參數:啟動 Java 應用時可以通過 JVM 參數來調整垃圾回收的行為,比如:
-Xmx
(最大堆大小)、-Xms
(初始堆大小)等。 - 對象數量或內存使用達到閾值:垃圾收集器內部實現了一些策略,以監控對象的創建和內存使用,達到某個閾值時觸發垃圾回收。
#判斷垃圾的方法有哪些?
在Java中,判斷對象是否為垃圾(即不再被使用,可以被垃圾回收器回收)主要依據兩種主流的垃圾回收算法來實現:引用計數法和可達性分析算法。
引用計數法(Reference Counting)
- 原理:為每個對象分配一個引用計數器,每當有一個地方引用它時,計數器加1;當引用失效時,計數器減1。當計數器為0時,表示對象不再被任何變量引用,可以被回收。
- 缺點:不能解決循環引用的問題,即兩個對象相互引用,但不再被其他任何對象引用,這時引用計數器不會為0,導致對象無法被回收。
可達性分析算法(Reachability Analysis)
Java虛擬機主要采用此算法來判斷對象是否為垃圾。
- 原理:從一組稱為GC Roots(垃圾收集根)的對象出發,向下追溯它們引用的對象,以及這些對象引用的其他對象,以此類推。如果一個對象到GC Roots沒有任何引用鏈相連(即從GC Roots到這個對象不可達),那么這個對象就被認為是不可達的,可以被回收。GC Roots對象包括:虛擬機棧(棧幀中的本地變量表)中引用的對象、方法區中類靜態屬性引用的對象、本地方法棧中JNI(Java Native Interface)引用的對象、活躍線程的引用等。
#垃圾回收算法是什么,是為了解決了什么問題?
JVM有垃圾回收機制的原因是為了解決內存管理的問題。在傳統的編程語言中,開發人員需要手動分配和釋放內存,這可能導致內存泄漏、內存溢出等問題。而Java作為一種高級語言,旨在提供更簡單、更安全的編程環境,因此引入了垃圾回收機制來自動管理內存。
垃圾回收機制的主要目標是自動檢測和回收不再使用的對象,從而釋放它們所占用的內存空間。這樣可以避免內存泄漏(一些對象被分配了內存卻無法被釋放,導致內存資源的浪費)。同時,垃圾回收機制還可以防止內存溢出(即程序需要的內存超過了可用內存的情況)。
通過垃圾回收機制,JVM可以在程序運行時自動識別和清理不再使用的對象,使得開發人員無需手動管理內存。這樣可以提高開發效率、減少錯誤,并且使程序更加可靠和穩定。
#垃圾回收算法有哪些?
- 標記-清除算法:標記-清除算法分為“標記”和“清除”兩個階段,首先通過可達性分析,標記出所有需要回收的對象,然后統一回收所有被標記的對象。標記-清除算法有兩個缺陷,一個是效率問題,標記和清除的過程效率都不高,另外一個就是,清除結束后會造成大量的碎片空間。有可能會造成在申請大塊內存的時候因為沒有足夠的連續空間導致再次 GC。
- 復制算法:為了解決碎片空間的問題,出現了“復制算法”。復制算法的原理是,將內存分成兩塊,每次申請內存時都使用其中的一塊,當內存不夠時,將這一塊內存中所有存活的復制到另一塊上。然后將然后再把已使用的內存整個清理掉。復制算法解決了空間碎片的問題。但是也帶來了新的問題。因為每次在申請內存時,都只能使用一半的內存空間。內存利用率嚴重不足。
- 標記-整理算法:復制算法在 GC 之后存活對象較少的情況下效率比較高,但如果存活對象比較多時,會執行較多的復制操作,效率就會下降。而老年代的對象在 GC 之后的存活率就比較高,所以就有人提出了“標記-整理算法”。標記-整理算法的“標記”過程與“標記-清除算法”的標記過程一致,但標記之后不會直接清理。而是將所有存活對象都移動到內存的一端。移動結束后直接清理掉剩余部分。
- 分代回收算法:分代收集是將內存劃分成了新生代和老年代。分配的依據是對象的生存周期,或者說經歷過的 GC 次數。對象創建時,一般在新生代申請內存,當經歷一次 GC 之后如果對還存活,那么對象的年齡 +1。當年齡超過一定值(默認是 15,可以通過參數 -XX:MaxTenuringThreshold 來設定)后,如果對象還存活,那么該對象會進入老年代。
#垃圾回收器有哪些?
- Serial收集器(復制算法): 新生代單線程收集器,標記和清理都是單線程,優點是簡單高效;
- ParNew收集器 (復制算法): 新生代收并行集器,實際上是Serial收集器的多線程版本,在多核CPU環境下有著比Serial更好的表現;
- Parallel Scavenge收集器 (復制算法): 新生代并行收集器,追求高吞吐量,高效利用 CPU。吞吐量 = 用戶線程時間/(用戶線程時間+GC線程時間),高吞吐量可以高效率的利用CPU時間,盡快完成程序的運算任務,適合后臺應用等對交互相應要求不高的場景;
- Serial Old收集器 (標記-整理算法): 老年代單線程收集器,Serial收集器的老年代版本;
- Parallel Old收集器 (標記-整理算法): 老年代并行收集器,吞吐量優先,Parallel Scavenge收集器的老年代版本;
- CMS(Concurrent Mark Sweep)收集器(標記-清除算法): 老年代并行收集器,以獲取最短回收停頓時間為目標的收集器,具有高并發、低停頓的特點,追求最短GC回收停頓時間。
- G1(Garbage First)收集器 (標記-整理算法): Java堆并行收集器,G1收集器是JDK1.7提供的一個新收集器,G1收集器基于“標記-整理”算法實現,也就是說不會產生內存碎片。此外,G1收集器不同于之前的收集器的一個重要特點是:G1回收的范圍是整個Java堆(包括新生代,老年代),而前六種收集器回收的范圍僅限于新生代或老年代
#標記清除算法的缺點是什么?
主要缺點有兩個:
- 一個是效率問題,標記和清除過程的效率都不高;
- 另外一個是空間問題,標記清除之后會產生大量不連續的內存碎片,空間碎片太多可能會導致,當程序在以后的運行過程中需要分配較大對象時無法找到足夠的連續內存而不得不提前觸發另一次垃圾收集動作。
#垃圾回收算法哪些階段會stop the world?
標記-復制算法應用在CMS新生代(ParNew是CMS默認的新生代垃圾回收器)和G1垃圾回收器中。標記-復制算法可以分為三個階段:
- 標記階段,即從GC Roots集合開始,標記活躍對象;
- 轉移階段,即把活躍對象復制到新的內存地址上;
- 重定位階段,因為轉移導致對象的地址發生了變化,在重定位階段,所有指向對象舊地址的指針都要調整到對象新的地址上。
下面以G1為例,通過G1中標記-復制算法過程(G1的Young GC和Mixed GC均采用該算法),分析G1停頓耗時的主要瓶頸。G1垃圾回收周期如下圖所示:
G1的混合回收過程可以分為標記階段、清理階段和復制階段。
標記階段停頓分析
- 初始標記階段:初始標記階段是指從GC Roots出發標記全部直接子節點的過程,該階段是STW的。由于GC Roots數量不多,通常該階段耗時非常短。
- 并發標記階段:并發標記階段是指從GC Roots開始對堆中對象進行可達性分析,找出存活對象。該階段是并發的,即應用線程和GC線程可以同時活動。并發標記耗時相對長很多,但因為不是STW,所以我們不太關心該階段耗時的長短。
- 再標記階段:重新標記那些在并發標記階段發生變化的對象。該階段是STW的。
清理階段停頓分析
- 清理階段清點出有存活對象的分區和沒有存活對象的分區,該階段不會清理垃圾對象,也不會執行存活對象的復制。該階段是STW的。
復制階段停頓分析
- 復制算法中的轉移階段需要分配新內存和復制對象的成員變量。轉移階段是STW的,其中內存分配通常耗時非常短,但對象成員變量的復制耗時有可能較長,這是因為復制耗時與存活對象數量與對象復雜度成正比。對象越復雜,復制耗時越長。
四個STW過程中,初始標記因為只標記GC Roots,耗時較短。再標記因為對象數少,耗時也較短。清理階段因為內存分區數量少,耗時也較短。轉移階段要處理所有存活的對象,耗時會較長。
因此,G1停頓時間的瓶頸主要是標記-復制中的轉移階段STW。
#minorGC、majorGC、fullGC的區別,什么場景觸發full GC
在Java中,垃圾回收機制是自動管理內存的重要組成部分。根據其作用范圍和觸發條件的不同,可以將GC分為三種類型:Minor GC(也稱為Young GC)、Major GC(有時也稱為Old GC)、以及Full GC。以下是這三種GC的區別和觸發場景:
Minor GC (Young GC)
- 作用范圍:只針對年輕代進行回收,包括Eden區和兩個Survivor區(S0和S1)。
- 觸發條件:當Eden區空間不足時,JVM會觸發一次Minor GC,將Eden區和一個Survivor區中的存活對象移動到另一個Survivor區或老年代(Old Generation)。
- 特點:通常發生得非常頻繁,因為年輕代中對象的生命周期較短,回收效率高,暫停時間相對較短。
Major GC
- 作用范圍:主要針對老年代進行回收,但不一定只回收老年代。
- 觸發條件:當老年代空間不足時,或者系統檢測到年輕代對象晉升到老年代的速度過快,可能會觸發Major GC。
- 特點:相比Minor GC,Major GC發生的頻率較低,但每次回收可能需要更長的時間,因為老年代中的對象存活率較高。
Full GC
-
作用范圍:對整個堆內存(包括年輕代、老年代以及永久代/元空間)進行回收。
-
觸發條件:
-
直接調用
System.gc()
或Runtime.getRuntime().gc()
方法時,雖然不能保證立即執行,但JVM會嘗試執行Full GC。 -
Minor GC(新生代垃圾回收)時,如果存活的對象無法全部放入老年代,或者老年代空間不足以容納存活的對象,則會觸發Full GC,對整個堆內存進行回收。
-
當永久代(Java 8之前的版本)或元空間(Java 8及以后的版本)空間不足時。
-
-
特點:Full GC是最昂貴的操作,因為它需要停止所有的工作線程(Stop The World),遍歷整個堆內存來查找和回收不再使用的對象,因此應盡量減少Full GC的觸發。
#垃圾回收器 CMS 和 G1的區別?
區別一:使用的范圍不一樣:
- CMS收集器是老年代的收集器,可以配合新生代的Serial和ParNew收集器一起使用
- G1收集器收集范圍是老年代和新生代。不需要結合其他收集器使用
區別二:STW的時間:
- CMS收集器以最小的停頓時間為目標的收集器。
- G1收集器可預測垃圾回收?(opens new window)的停頓時間(建立可預測的停頓時間模型)
區別三: 垃圾碎片
- CMS收集器是使用“標記-清除”算法進行的垃圾回收,容易產生內存碎片
- G1收集器使用的是“標記-整理”算法,進行了空間整合,沒有內存空間碎片。
區別四: 垃圾回收的過程不一樣
注意這兩個收集器第四階段得不同
區別五: CMS會產生浮動垃圾
- CMS產生浮動垃圾過多時會退化為serial old,效率低,因為在上圖的第四階段,CMS清除垃圾時是并發清除的,這個時候,垃圾回收線程和用戶線程同時工作會產生浮動垃圾,也就意味著CMS垃圾回收器必須預留一部分內存空間用于存放浮動垃圾
- 而G1沒有浮動垃圾,G1的篩選回收是多個垃圾回收線程并行gc的,沒有浮動垃圾的回收,在執行‘并發清理’步驟時,用戶線程也會同時產生一部分可回收對象,但是這部分可回收對象只能在下次執行清理是才會被回收。如果在清理過程中預留給用戶線程的內存不足就會出現‘Concurrent Mode Failure’,一旦出現此錯誤時便會切換到SerialOld收集方式。
#什么情況下使用CMS,什么情況使用G1?
CMS適用場景:
- 低延遲需求:適用于對停頓時間要求敏感的應用程序。
- 老生代收集:主要針對老年代的垃圾回收。
- 碎片化管理:容易出現內存碎片,可能需要定期進行Full GC來壓縮內存空間。
G1適用場景:
- 大堆內存:適用于需要管理大內存堆的場景,能夠有效處理數GB以上的堆內存。
- 對內存碎片敏感:G1通過緊湊整理來減少內存碎片,降低了碎片化對性能的影響。
- 比較平衡的性能:G1在提供較低停頓時間的同時,也保持了相對較高的吞吐量。
#G1回收器的特色是什么?
G1 的特點:
- G1最大的特點是引入分區的思路,弱化了分代的概念。
- 合理利用垃圾收集各個周期的資源,解決了其他收集器、甚至 CMS 的眾多缺陷
G1 相比較 CMS 的改進:
- 算法: G1 基于標記--整理算法, 不會產生空間碎片,在分配大對象時,不會因無法得到連續的空間,而提前觸發一次 FULL GC 。
- 停頓時間可控: G1可以通過設置預期停頓時間(Pause Time)來控制垃圾收集時間避免應用雪崩現象。
- 并行與并發:G1 能更充分的利用 CPU 多核環境下的硬件優勢,來縮短 stop the world 的停頓時間。
#GC只會對堆進行GC嗎?
JVM 的垃圾回收器不僅僅會對堆進行垃圾回收,它還會對方法區進行垃圾回收。
- 堆(Heap):?堆是用于存儲對象實例的內存區域。大部分的垃圾回收工作都發生在堆上,因為大多數對象都會被分配在堆上,而垃圾回收的重點通常也是回收堆中不再被引用的對象,以釋放內存空間。
- 方法區(Method Area):?方法區是用于存儲類信息、常量、靜態變量等數據的區域。雖然方法區中的垃圾回收與堆有所不同,但是同樣存在對不再需要的常量、無用的類信息等進行清理的過程。