1. 程式人生 > 實用技巧 >JVM垃圾回收概念

JVM垃圾回收概念

垃圾回收概念

什麼是垃圾

  • 垃圾是指在執行程式中沒有任何指標指向的物件,這個物件就是需要被回收的垃圾。
  • 如果不及時對記憶體中的垃圾進行清理那麼,這些垃圾物件所佔的記憶體空間會一直保留到應用程式結束,被保留的空間無法被其他物件使用。甚至可能導致記憶體溢位。

為什麼需要GC

  • 對於高階語言來說,一個基本認知是如果不進行垃圾回收,記憶體遲早都會被消耗完,因為不斷地分配記憶體空間而不進行回收,就好像不停地生產生活垃圾而從來不打掃一樣。
  • 除了釋放沒用的物件,垃圾回收也可以清除記憶體裡的記錄碎片。碎片整理將所佔用的堆記憶體移到堆的一端,以便JVM 將整理出的記憶體分配給新的物件。
  • 隨著應用程式所應付的業務越來越龐大、複雜,使用者越來越多,沒有GC就不能保證應用程式的正常進行
    。而經常造成STW的GC又跟不上實際的需求,所以才會不斷地嘗試對GC進行優化。

Java的記憶體回收

  • 自動記憶體管理,無需開發人員手動參與記憶體的分配與回收,這樣降低記憶體洩漏和記憶體溢位的風險
    ➢沒有垃圾回收器,java也會和cpp一樣,各種懸垂指標,野指標,洩露問題讓你頭疼不已。
  • 自動記憶體管理機制,將程式設計師從繁重的記憶體管理中釋放出來,可以更專心地專注於業務開發
  • 擔憂
    • 對於Java開發人員而言,自動記憶體管理就像是一個黑匣子,如果過度依賴於“自動”,那麼這將會是一場災難,最嚴重的就會弱化Java開發人員在程式出現記憶體溢位時定位問題和解決問題的能力。
    • 此時,了 解JVM的自動記憶體分配和記憶體回收原理就顯得非常重要,只有在真正瞭解JVM是如何管理記憶體後,我們才能夠在遇見OutOfMemoryError時,快速地根據錯誤異常日誌定位問題和解決問題。
    • 當需要排查各種記憶體溢位、記憶體洩漏問題時,當垃圾收整合為系統達到更高併發量的瓶頸時,我們就必須對這些“自動化”的技術實施必要的監控和調節。
  • 垃圾回收器可以對年輕代回收,也可以對老年代回收,甚至是全堆和方法區的回收。
    其中,Java堆是垃圾收集器的工作重點。
  • 從次數上講:
    ➢頻繁收集Young區
    ➢較少收集old區
    ➢基本不動Perm區(元空間)

System.gc()的理解

  • 在預設情況下,通過system.gc()或者Runtime.getRuntime().gc()的呼叫,會顯式觸發Full GC,同時對老年代和新生代進行回收,嘗試釋放被丟棄物件佔用的記憶體。
  • 然而System.gc()呼叫附帶一個免責宣告,無法保證對垃圾收集器的呼叫。
  • JVM實現者可以通過System.gc()呼叫來決定JVM的GC行為。而一般情況下,垃圾回收應該是自動進行的,無須手動觸發,否則就太過於麻煩了。在一些特殊情況下,如我們正在編寫一個性能基準,我們可以在執行之間呼叫System.gc()。

記憶體溢位與記憶體洩漏

記憶體溢位(OOM)

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

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

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

  • javadoc中對OutOfMemoryError的解釋是,沒有空閒記憶體,並且垃圾收集器也無法提供更多記憶體。

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

    1. Java虛擬機器的堆記憶體設定不夠。

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

    2. 程式碼中建立了大量大物件,並且長時間不能被垃圾收集器收集(存在被引用)

      對於老版本的Oracle JDK,因為永久代的大小是有限的,並且JVM對永久代垃圾回收(如,常量池回收、解除安裝不再需要的型別)非常不積極,所以當我們不斷新增新型別的時候,永久代出現OutOfMemoryError也非常多見,尤其是在執行時存在大量動態型別生成的場合;類似intern字串快取佔用太多空間,也會導致0OM問題。對應的異常資訊,會標記出來和永久代相關: "java.lang.OutofMemoryError: PermGen space"。

      隨著元資料區的引入,方法區記憶體已經不再那麼窘迫,所以相應的00M有所改觀,出現OOM,異常資訊則變成了:“java.lang.OutOfMemoryError: Metaspace". 直接記憶體不足,也會導致OOM。

  • 這裡面隱含著一層意思是,在丟擲0utOfMemoryError之前,通常垃圾收集器會被觸發,盡其所能去清理出空間。
    ➢例如:在引用機制分析中,涉及到JVM會去嘗試回收軟引用指向的物件等。
    ➢在java .nio.BIts.reserveMemory()方法中,我們能清楚的看到,System. gc()會被呼叫,以清理空間。

  • 當然,也不是在任何情況下垃圾收集器都會被觸發的
    ➢比如,我們去分配一一個超大物件,類似一一個超大陣列超過堆的最大值,JVM可以判斷出垃圾收集並不能解決這個問題,所以直接丟擲OutOfMemoryError.

記憶體洩漏(Memory Leak)

  • 也稱作“儲存滲漏”, 嚴格來說,只有物件不會再被程式用到了,但是GC又不能回收他們的情況,才叫記憶體洩漏。
  • 但實際情況很多時候一些不太好的實踐(或疏忽)會導致物件的生命週期變得很長甚至導致OOM,也可以叫做寬泛意義上的“記憶體洩漏
  • 儘管記憶體洩漏並不會立刻引起程式崩潰,但是一旦發生記憶體洩漏,程式中的可用記憶體就會被逐步蠶食,直至耗盡所有記憶體,最終出現0utOfMemory異常,導致程式崩潰。
  • 注意,這裡的儲存空間並不是指實體記憶體,而是指虛擬記憶體大小,這個虛擬記憶體大小取決於磁碟交換區設定的大小。
  • 舉例:
    1、單例模式
    單例的生命週期和應用程式是一樣長的,所以單例程式中,如果持有對外部物件的引用的話,那麼這個外部物件是不能被回收的,則會導致記憶體洩漏的產生。
    2、一些提供close的資源未關閉導致記憶體洩漏資料庫連線(dataSourse . getConnection()),網路連線(socket)和io連線必須手動close,否則是不能被回收的。

Stop The Word

  • stop-the-World,簡稱STW,指的是GC事件發生過程中,會產生應用程式的停頓。停頓產生時整個應用程式執行緒都會被暫停,沒有任何響應,有點像卡死的感覺,這個停頓稱為STW。
    ➢可達性分析演算法中列舉根節點(GC Roots)會導致所有Java執行執行緒停頓。
    • 分析工作必須在一個能確保一致性的快照中進行
    • 一致性指整個分析期間整個執行系統看起來像被凍結在某個時間點上
    • 如果出現分析過程中物件引用關係還在不斷變化,則分析結果的準確性無法保證
  • 被STW中斷的應用程式執行緒會在完成GC之後恢復,頻繁中斷會讓使用者感覺像是網速不快造成電影卡帶-樣,所以我們需要減少STW的發生。
  • STW事件和採用哪款GC無關,所有的GC都有這個事件。
  • STW是JVM在後臺自動發起和自動完成的。在使用者不可見的情況下,把使用者正常的工作執行緒全部停掉。
  • 開發中不要用System.gc() ;會導致Stop-the-world的發生。

垃圾回收的並行與併發

  • 並行(Parallel) :指多條垃圾收集執行緒併發工作,但此時使用者執行緒仍處於等待狀態。
    ➢如ParNew、 Parallel Scavenge、 Parallel 0ld;
  • 序列(Serial)
    ➢相較於並行的概念,單執行緒執行。
    ➢如果記憶體不夠,則程式暫停,啟動JVM垃圾回收器進行垃圾回收。回收完,再啟動程式的執行緒。
  • 併發(Concurrent) :指使用者執行緒與垃圾收集執行緒同時執行(但不一定是並行的,可能會交替執行),垃圾回收執行緒在執行時不會停頓使用者程式的執行。|
    ➢使用者程式在繼續執行,而垃圾收集程式執行緒運行於另一個CPU上;
    ➢如: CMS、G1

安全點與安全區域

安全點(Safe point)

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

Safe Point的選擇很重要,如果太少可能導致GC等待的時間太長,如果太頻繁可能導致執行時的效能問題。大部分指令的執行時間都非常短暫,通常會根據“是否具有讓程式長時間執行的特徵”為標準。比如:選擇一些執行時間較長的指令作為Safe Point, 如方法呼叫、迴圈跳轉和異常跳轉等。

  • 如何在GC發生時,檢查所有執行緒都跑到最近的安全點停頓下來呢?
    • 搶先式中斷: (目前沒有虛擬機器採用了)
      • 首先中斷所有執行緒。如果還有執行緒不在安全點,就恢復執行緒,讓執行緒跑到安全點。
    • 主動式中斷:
      • 設定一箇中斷標誌,各個執行緒執行到Safe Point的時候主動輪詢這個標誌,如果中斷標誌為真,則將自己進行中斷掛起。

安全區域(Safe Region)

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

安全區域是指在一.段程式碼片段中,物件的引用關係不會發生變化,在這個區域中的任何位置開始GC都是安全的。我們也可以把Safe Region看做是被擴充套件了的Safepoint.

  • 實際執行時
    1. 當執行緒執行到Safe Region的程式碼時,首先標識已經進入了Safe Region,如果這段時間內發生GC,JVM會 忽略標識為Safe Region狀態的執行緒;
    2. 當執行緒即將離開Safe Region時, 會檢查JVM是否已經完成GC,如果完成了,則繼續執行,否則執行緒必須等待直到收到可以安全離開Safe Region的訊號為止;

Java的引用

我們希望能描述這樣一類物件:當記憶體空間還足夠時,則能保留在記憶體中;如果記憶體空間在進行垃圾收集後還是很緊張,則可以拋棄這些物件。

[既偏門又非常高頻的面試題]強引用、軟引用、弱引用、虛引用有什麼區別?具體使用場景是什麼?

在JDK 1.2版之後,Java對引用的概念進行了擴充,將引用分為強引用(Strong Reference)、軟引用(Soft Reference)、弱引用(Weak Reference)和虛引用(Phantom Reference) 4種,這4種引用強度依次逐漸減弱。

除強引用外,其他3種引用均可以在java.lang.ref包中找到它們的身影。如下圖,顯示了這3種引用型別對應的類,開發人員可以在應用程式中直接使用它們。

  • Reference子類中只有終結器引用是包內可見的,其他3種引用型別均為public,可以在應用程式中直接使用

  • 強引用(StrongReference) :最傳統的“引用”的定義,是指在程式程式碼之中普遍存在的引用賦值,即類似“Object obj=new object( )”這種引用關係。無論任何情況下,只要強引用關係還存在,垃圾收集器就永遠不會回收掉被引用的物件。

    • 強引用可以直接訪問目標物件。
    • 強引用所指向的物件在任何時候都不會被系統回收,虛擬機器寧願丟擲OOM異常,也不會回收強引用所指向物件。
    • 強引用可能導致記憶體洩漏。
  • 軟引用(SoftReference) :在系統將要發生記憶體溢位之前,將會把這些物件列入回收範圍之中進行第二次回收。如果這次回收後還沒有足夠的記憶體,才會丟擲記憶體溢位異常。

    • 軟引用通常用來實現記憶體敏感的快取。比如:快取記憶體就有用到軟引用。如果還有空閒記憶體,就可以暫時保留快取,當記憶體不足時清理掉,這樣就保證了使用快取的同時,不會耗盡記憶體。

    • 垃圾回收器在某個時刻決定回收軟可達的物件的時候,會清理軟引用,並可選地把引用存放到一個引用佇列( Reference Queue) 。

    • 類似弱引用,只不過Java虛擬機器會盡量讓軟引用的存活時間長一些,迫不得已才清理。

      //建立軟引用
      SoftReference<User> userSoftRef = new SoftReference<User>(new User(1, "songhk"));
      
  • 弱引用(WeakReference):被弱引用關聯的物件只能生存到下一次垃圾收集之前。當垃圾收集器工作時,無論記憶體空間是否足夠,都會回收掉被弱引用關聯的物件。

    • 弱引用也是用來描述那些非必需物件,只被弱引用關聯的物件只能生存到下一次垃圾收集發生為止。在系統GC時,只要發現弱引用,不管系統堆空間使用是否充足,都會回收掉只被弱引用關聯的物件。

    • 但是,由於垃圾回收器的執行緒通常優先順序很低,因此,並不一定能很快地發現,持有弱引用的物件。在這種情況下,弱引用物件可以存在較長的時間。

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

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

      //構造了弱引用
      WeakReference<User> userWeakRef = new WeakReference<User>(new User(1, "songhk"));
      
  • 虛引用(PhantomReference) : 一個物件是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來獲得一個對 象的例項。為一個物件設定虛引用關聯的唯一目的就是能在這個物件被收集器回收時收到一個系統通知。

    • 也稱為“幽靈引用”或者“幻影引用”,是所有引用型別中最弱的一個。

    • 一個物件是否有虛引用的存在,完全不會決定物件的生命週期。如果一個物件僅持有虛引用,那麼它和沒有引用幾乎是一樣的,隨時都可能被垃圾回收器回收。

    • 它不能單獨使用,也無法通過虛引用來獲取被引用的物件。當試圖通過虛引用的get()方法取得物件時,總是null。

    • 虛引用必須和引用佇列一起使用。虛引用在建立時必須提供一個引用佇列作為引數。當垃圾回收器準備回收一個對 象時,如果發現它還有虛引用,就會在回收物件後,將這個虛引用加入引用佇列,以通知應用程式物件的回收情況。

    • 由於虛引用可以跟蹤物件的回收時間,因此,也可以將一些資源釋放操作放置在虛引用中執行和記錄。

    • 在JDK 1. 2版之後提供了PhantomReference類來實現虛引用。

      static ReferenceQueue<PhantomReferenceTest> phantomQueue = null;//引用佇列
      ....
      phantomQueue = new ReferenceQueue<PhantomReferenceTest>();
      obj = new PhantomReferenceTest();
      //構造了 PhantomReferenceTest 物件的虛引用,並指定了引用佇列
      PhantomReference<PhantomReferenceTest> phantomRef = new PhantomReference<PhantomReferenceTest>(obj, phantomQueue);
      

上一篇:StringTable
下一篇: