天天看點

Java-垃圾回收概述

1. 什麼是垃圾

在提到什麼是垃圾之前,我們先看下面一張圖

Java-垃圾回收概述

從上圖我們可以很明确的知道,Java 和 C++語言的差別,就在于垃圾收集技術和記憶體動态配置設定上,C語言沒有垃圾收集技術,需要我們手動的收集。垃圾收集,不是Java語言的伴生産物。早在1960年,第一門開始使用記憶體動态配置設定和垃圾收集技術的Lisp語言誕生。 關于垃圾收集有三個經典問題:

  • 哪些記憶體需要回收?
  • 什麼時候回收?
  • 如何回收?

垃圾收集機制是Java的招牌能力,極大地提高了開發效率。如今,垃圾收集幾乎成為現代語言的标配,即使經過如此長時間的發展,Java的垃圾收集機制仍然在不斷的演進中,不同大小的裝置、不同特征的應用場景,對垃圾收集提出了新的挑戰。

1.1 什麼是垃圾?

垃圾是指在運作程式中沒有任何指針指向的對象,這個對象就是需要被回收的垃圾。如果不及時對記憶體中的垃圾進行清理,那麼,這些垃圾對象所占的記憶體空間會一直保留到應用程式的結束,被保留的空間無法被其它對象使用,甚至可能導緻記憶體溢出。

2. 為什麼需要GC

對于進階語言來說,一個基本認知是如果不進行垃圾回收,記憶體遲早都會被消耗完,因為不斷地配置設定記憶體空間而不進行回收,就好像不停地生産生活垃圾而從來不打掃一樣。

除了釋放沒用的對象,垃圾回收也可以清除記憶體裡的記錄碎片。碎片整理将所占用的堆記憶體移到堆的一端,以便JVM将整理出的記憶體配置設定給新的對象。

随着應用程式所應付的業務越來越龐大、複雜,使用者越來越多,沒有GC就不能保證應用程式的正常進行。而經常造成STW的GC又跟不上實際的需求,是以才會不斷地嘗試對GC進行優化。

3. 早期垃圾回收

在早期的C/C++時代,垃圾回收基本上是手工進行的。開發人員可以使用new關鍵字進行記憶體申請,并使用delete關鍵字進行記憶體釋放。這種方式可以靈活控制記憶體釋放的時間,但是會給開發人員帶來頻繁申請和釋放記憶體的管理負擔。倘若有一處記憶體區間由于程式員編碼的問題忘記被回收,那麼就會産生記憶體洩漏,垃圾對象永遠無法被清除,随着系統運作時間的不斷增長,垃圾對象所耗記憶體可能持續上升,直到出現記憶體溢出并造成應用程式崩潰。

4. Java垃圾回收機制

4.1 優點

自動記憶體管理,無需開發人員手動參與記憶體的配置設定與回收,這樣降低記憶體洩漏和記憶體溢出的風險。如果沒有自動垃圾回收機制,java也會和c語音一樣,各種懸垂指針,野指針,洩露問題讓你頭疼不已。自動記憶體管理機制,将程式員從繁重的記憶體管理中釋放出來,可以更專心地專注于業務開發

4.2 缺點

對于Java開發人員而言,自動記憶體管理就像是一個黑匣子,如果過度依賴于“自動”,那麼這将會是一場災難,最嚴重的就會弱化Java開發人員在程式出現記憶體溢出時定位問題和解決問題的能力。

此時,了解JVM的自動記憶體配置設定和記憶體回收原理就顯得非常重要,隻有在真正了解JVM是如何管理記憶體後,我們才能夠在遇見outofMemoryError時,快速地根據錯誤異常日志定位問題和解決問題。

當需要排查各種記憶體溢出、記憶體洩漏問題時,當垃圾收內建為系統達到更高并發量的瓶頸時,我們就必須對這些“自動化”的技術實施必要的監控和調節。

4.3 GC主要關注的區域

GC主要關注于 方法區 和堆中的垃圾收集

Java-垃圾回收概述

垃圾收集器可以對年輕代回收,也可以對老年代回收,甚至是全棧和方法區的回收

  • 其中,Java堆是垃圾收集器的工作重點

從次數上講:

  • 頻繁收集Young區
  • 較少收集Old區
  • 基本不收集Perm區(元空間)

5. System.gc()的了解

在預設情況下,通過system.gc()者Runtime.getRuntime().gc() 的調用,會顯式觸發FullGC,同時對老年代和新生代進行回收,嘗試釋放被丢棄對象占用的記憶體。

然而system.gc() )調用附帶一個免責聲明,無法保證對垃圾收集器的調用。(不能確定立即生效)

JVM實作者可以通過system.gc() 調用來決定JVM的GC行為。而一般情況下,垃圾回收應該是自動進行的,無須手動觸發,否則就太過于麻煩了。在一些特殊情況下,如我們正在編寫一個性能基準,我們可以在運作之間調用System.gc()。

6. 記憶體溢出

記憶體溢出相對于記憶體洩漏來說,盡管更容易被了解,但是同樣的,記憶體溢出也是引發程式崩潰的罪魁禍首之一。

由于GC一直在發展,所有一般情況下,除非應用程式占用的記憶體增長速度非常快,造成垃圾回收已經跟不上記憶體消耗的速度,否則不太容易出現OOM的情況。

大多數情況下,GC會進行各種年齡段的垃圾回收,實在不行了就放大招,來一次獨占式的FullGC操作,這時候會回收大量的記憶體,供應用程式繼續使用。

首先說沒有空閑記憶體的情況:說明Java虛拟機的堆記憶體不夠。原因有二:

  • Java虛拟機的堆記憶體設定不夠。

比如:可能存在記憶體洩漏問題;也很有可能就是堆的大小不合理,比如我們要處理比較可觀的資料量,但是沒有顯式指定JVM堆大小或者指定數值偏小。我們可以通過參數-Xms 、-Xmx來調整。

  • 代碼中建立了大量大對象,并且長時間不能被垃圾收集器收集(存在被引用)

在抛出OutofMemoryError之前,通常垃圾收集器會被觸發,盡其所能去清理出空間。當然,也不是在任何情況下垃圾收集器都會被觸發的。比如,我們去配置設定一個超大對象,類似一個超大數組超過堆的最大值,JVM可以判斷出垃圾收集并不能解決這個問題,是以直接抛出OutofMemoryError。

7. 記憶體洩漏

也稱作“存儲滲漏”。嚴格來說,隻有對象不會再被程式用到了,但是GC又不能回收他們的情況,才叫記憶體洩漏。

但實際情況很多時候一些不太好的實踐(或疏忽)會導緻對象的生命周期變得很長甚至導緻00M,也可以叫做寬泛意義上的“記憶體洩漏”。

盡管記憶體洩漏并不會立刻引起程式崩潰,但是一旦發生記憶體洩漏,程式中的可用記憶體就會被逐漸蠶食,直至耗盡所有記憶體,最終出現outofMemory異常,導緻程式崩潰。

7.1 舉例

  • 單例模式

單例的生命周期和應用程式是一樣長的,是以單例程式中,如果持有對外部對象的引用的話,那麼這個外部對象是不能被回收的,則會導緻記憶體洩漏的産生。

  • 一些提供close的資源未關閉導緻記憶體洩漏

資料庫連接配接(dataSourse.getConnection() ),網絡連接配接(socket)和io連接配接必須手動close,否則是不能被回收的。

8. 對象已死?

堆裡幾乎存放着java中所有的執行個體對象,在對堆進行回收前,第一件事情就是要确定這些對象有哪些還 "存活" 着 ?哪些已經 "死去" (不可能再被任何途徑使用的對象)。

8.1 引用計數算法

給對象中增加一個引用計數器,每當一個地方引用它時,計數器值就加1;當引用失效,計數器值就建1;電腦為0的對象就是不可能再被使用的。

引用計數算法的實作簡單,判斷效率也很高,但是它很難解決對象之間的互相循環引用的問題。是以Java沒有選用引用計數算法來管理記憶體。

8.2 根搜尋算法

通過一系列的稱為“GC Roots”的對象作為起始點,從這些節點開始向下搜尋,搜尋的路徑稱為引用鍊,當一個對象到“GC Roots”沒有任何引用鍊相連的話,也就是GC Roots到這個對象不可達時,證明此對象已經不可用,可以被回收了。

可作為GC roots的對象的包括下面幾種:

  • 棧中的對象引用、
  • 方法區中常量的引用、
  • 方法區中靜态對象的引用、
  • 本地方法區中native對象的引用
Java-垃圾回收概述

9. Stop The World

stop-the-world,簡稱STW,指的是GC事件發生過程中,會産生應用程式的停頓。停頓産生時整個應用程式線程都會被暫停,沒有任何響應,有點像卡死的感覺,這個停頓稱為STW。

可達性分析算法中枚舉根節點(GC Roots)會導緻所有Java執行線程停頓。

被STW中斷的應用程式線程會在完成GC之後恢複,頻繁中斷會讓使用者感覺像是網速不快造成電影卡帶一樣,是以我們需要減少STW的發生。

STW事件和采用哪款GC垃圾回收器無關所有的GC都有這個事件。哪怕是G1也不能完全避免Stop-the-world情況發生,隻能說垃圾回收器越來越優秀,回收效率越來越高,盡可能地縮短了暫停時間。

STW是JVM在背景自動發起和自動完成的。在使用者不可見的情況下,把使用者正常的工作線程全部停掉。開發中不要用system.gc() 會導緻stop-the-world的發生。

10. 安全點與安全區域

通過上文我們知道 HotSpot 虛拟機采取的是可達性分析算法。即通過 GC Roots 枚舉判定待回收的對象。那麼,首先要找到哪些是 GC Roots。有兩種查找 GC Roots 的方法:

  • 一種是周遊方法區和棧區查找(保守式 GC)。
  • 一種是通過 OopMap 資料結構來記錄 GC Roots 的位置(準确式 GC)。

保守式 GC 的成本太高。準确式 GC 的優點就是能夠讓虛拟機快速定位到 GC Roots。對應 OopMap 的位置即可作為一個安全點(Safe Point)。在執行 GC 操作時,所有的工作線程必須停頓,這就是所謂的”Stop-The-World”。

為什麼呢?

因為可達性分析算法必須是在一個確定一緻性的記憶體快照中進行。如果在分析的過程中對象引用關系還在不斷變化,分析結果的準确性就不能保證。安全點意味着在這個點時,所有工作線程的狀态是确定的,JVM 就可以安全地執行 GC 。

10.1 安全點

程式執行時并非在所有地方都能停頓下來開始GC,隻有在特定的位置才能停頓下來開始GC,這些位置稱為“安全點(Safepoint)”。

Safe Point的選擇很重要,如果太少可能導緻GC等待的時間太長,如果太頻繁可能導緻運作時的性能問題。大部分指令的執行時間都非常短暫,通常會根據“是否具有讓程式長時間執行的特征”為标準。

比如:選擇一些執行時間較長的指令作為Safe Point,如方法調用、循環跳轉和異常跳轉等。

如何在gc發生時,檢查所有線程都跑到最近的安全點停頓下來呢?

  • 搶先式中斷:(目前沒有虛拟機采用)首先中斷所有線程。如果還有線程不在安全點,就恢複線程,讓線程跑到安全點。
  • 主動式中斷:設定一個中斷标志,各個線程運作到Safe Point的時候主動輪詢這個标志,如果中斷标志為真,則将自己進行中斷挂起。(有輪詢的機制)

注意:程式運作到安全點,不是一定要進行垃圾回收。而是在這些點上進行垃圾回收,較為安全。是以叫做安全點。

10.2 安全區域

Safepoint 機制保證了程式執行時,在不太長的時間内就會遇到可進入GC的Safepoint。但是,程式“不執行”的時候呢?例如線程處于sleep-狀态或Blocked 狀态,這時候線程無法響應JVM的中斷請求,“走”到安全點去中斷挂起,JVM也不太可能等待線程被喚醒。對于這種情況,就需要安全區域(Safe Region)來解決。

安全區域是指在一段代碼片段中,對象的引用關系不會發生變化,在這個區域中的任何位置開始Gc都是安全的。我們也可以把Safe Region看做是被擴充了的Safepoint。

執行流程:

  • 當線程運作到Safe Region的代碼時,首先辨別已經進入了Safe Relgion,如果這段時間内發生GC,JVM會忽略辨別為Safe Region狀态的線程
  • 當線程即将離開Safe Region時,會檢查JVM是否已經完成GC,如果完成了,則繼續運作,否則線程必須等待直到收到可以安全離開Safe Region的信号為止。

11. 強引用、軟引用、虛引用、弱引用

11.1 強引用

當記憶體不足,jvm開始垃圾回收,對于強引用的對象,就算是出現了OOM也不會對該對象進行回收。這也是Java中最常見的普通對象的引用,隻要還有強引用指向這個對象,就不會被垃圾回收。

當這個對象沒有了其他的引用關系,隻要是超過了引用的作用域,或者顯示的将強引用指派為null,一般就可以進行垃圾回收了。

11.2 軟引用

軟引用是相對強引用弱化了一些的引用,對于軟引用的對象來說:

  • 當記憶體充足時,它不會被回收。
  • 當記憶體不足時。會被回收。

通常用在對記憶體敏感的程式中,就像高速緩存。

11.3 弱引用

發現即回收

弱引用也是用來描述那些非必需對象,被弱引用關聯的對象隻能生存到下一次垃圾收集發生為止。在系統GC時,隻要發現弱引用,不管系統堆空間使用是否充足,都會回收掉隻被弱引用關聯的對象。但是,由于垃圾回收器的線程通常優先級很低,是以,并不一定能很快地發現持有弱引用的對象。在這種情況下,弱引用對象可以存在較長的時間。

弱引用和軟引用一樣,在構造弱引用時,也可以指定一個引用隊列,當弱引用對象被回收時,就會加入指定的引用隊列,通過這個隊列可以跟蹤對象的回收情況。

軟引用、弱引用都非常适合來儲存那些可有可無的緩存資料。如果這麼做,當系統記憶體不足時,這些緩存資料會被回收,不會導緻記憶體溢出。而當記憶體資源充足時,這些緩存資料又可以存在相當長的時間,進而起到加速系統的作用。

在JDK1.2版之後提供了WeakReference類來實作弱引用

// 聲明強引用
Object obj = new Object();
// 建立一個弱引用
WeakReference<Object> sf = new WeakReference<>(obj);
obj = null; //銷毀強引用,這是必須的,不然會存在強引用和弱引用           

弱引用對象與軟引用對象的最大不同就在于,當GC在進行回收時,需要通過算法檢查是否回收軟引用對象,而對于弱引用對象,GC總是進行回收。弱引用對象更容易、更快被GC回收。

開發中使用過WeakHashMap的場景?

WeakHashMap用來存儲圖檔資訊,可以在記憶體不足的時候,及時回收,避免了OOM

11.4 虛引用

也稱為“幽靈引用”或者“幻影引用”,是所有引用類型中最弱的一個一個對象是否有虛引用的存在,完全不會決定對象的生命周期。如果一個對象僅持有虛引用,那麼它和沒有引用幾乎是一樣的,随時都可能被垃圾回收器回收。它不能單獨使用,也無法通過虛引用來擷取被引用的對象。當試圖通過虛引用的get()方法取得對象時,總是null。為一個對象設定虛引用關聯的唯一目的在于跟蹤垃圾回收過程。比如:能在這個對象被收集器回收時收到一個系統通知。

虛引用必須和引用隊列一起使用。虛引用在建立時必須提供一個引用隊列作為參數。當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象後,将這個虛引用加入引用隊列,以通知應用程式對象的回收情況。由于虛引用可以跟蹤對象的回收時間,是以,也可以将一些資源釋放操作放置在虛引用中執行和記錄。

在JDK1.2版之後提供了PhantomReference類來實作虛引用。

// 聲明強引用
Object obj = new Object();
// 聲明引用隊列
ReferenceQueue phantomQueue = new ReferenceQueue();
// 聲明虛引用(還需要傳入引用隊列)
PhantomReference<Object> sf = new PhantomReference<>(obj, phantomQueue);
obj = null;           

繼續閱讀