十六、垃圾回收相關概念

1、System.gc()的理解

 

  • 在默認情況下,通過System.gc ()或者Runtime.getRuntime () .gc()的調用,會顯式觸發Full GC,同時對老年代和新生代進行回收,嘗試釋放被丟棄對象佔用的內存。
  • 然而System.gc ()調用附帶一個免責聲明,無法保證對垃圾收集器的調用。
  • JVM實現者可以通過System.gc ()調用來決定JVM的GC行爲。而一般情況下,垃圾回收應該是自動進行的,無須手動觸發,否則就太過於麻煩了。在一些特殊情況下,如我們正在編寫一個性能基準,我們可以在運行之間調用System.gc ()。
public class SystemGCTest {
    public static void main(String[] args) {
        new SystemGCTest();
        System.gc();//提醒jvm的垃圾回收器執行gc,但是不確定是否馬上執行gc
        //與Runtime.getRuntime().gc();的作用一樣。

        System.runFinalization();//強制調用失去引用的對象的finalize()方法
    }

    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("SystemGCTest 重寫了finalize()");
    }
}

 

 

2、內存溢出與內存泄漏

 

內存溢出(OOM)

 

  • 內存溢出相對於內存泄漏來說,儘管更容易被理解,但是同樣的,內存溢出也是引發程序崩潰的罪魁禍首之一。
  • 由於GC一直在發展,所有一般情況下,除非應用程序佔用的內存增長速度非常快,造成垃圾回收已經跟不上內存消耗的速度,否則不太容易出現OOM的情況。
  • 大多數情況下, GC會進行各種年齡段的垃圾回收,實在不行了就放大招,來一次獨佔式的Full GC操作,這時候會回收大量的內存,供應用程序繼續使用。
  • javadoc中對OutOfMemoryError的解釋是,沒有空閒內存,並且垃圾收集器也無法提供更多內存。

 

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

(1) Java虛擬機的堆內存設置不夠。

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

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

對於老版本的Oracle JDK,因爲永久代的大小是有限的,並且JVM對永久代垃圾回收(如,常量池回收、卸載不再需要的類型)非常不積極,所以當我們不斷添加新類型的時候,永久代出現OutOfMemoryError也非常多見,尤其是在運行時存在大量動態類型生成的場合;類似intern字符串緩存佔用太多空間,也會導致OOM問題。對應的異常信息,會標記出來和永久代相關: "java.lang.OutOfMemoryError: PermGen space"

隨着元數據區的引入,方法區內存已經不再那麼窘迫,所以相應的OOM有所改觀,出現OOM,異常信息則變成了: "java.lang.OutOfMemoryError: Metaspace",直接內存不足,也會導致OOM

 

  • 這裏面隱含着一層意思是,在拋出OutOfMemoryError之前,通常垃圾收集器會被觸發,盡其所能去清理出空間。

> 例如:在引用機制分析中,涉及到JVM會去嘗試回收軟引用指向的對象等。

> 在java.nio.BIts.reserveMemory()方法中,我們能清楚的看到, System.gc()會被調用,以清理空間。

  • 當然,也不是在任何情況下垃圾收集器都會被觸發的

> 比如,我們去分配一個超大對象,類似一個超大數組超過堆的最大值, JVM可以判斷出垃圾收集並不能解決這個問題,所以直接拋出OutOfMemoryError.

 

 

 

內存泄漏(Memory Leak)

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

但實際情況很多時候一些不太好的實踐(或疏忽)會導致對象的生命週期變得很長甚至導致OOM,也可以叫做寬泛意義上的“內存泄漏”

儘管內存泄漏並不會立刻引起程序崩潰,但是一旦發生內存泄漏,程序中的可用內存就會被逐步蠶食,直至耗盡所有內存,最終出現OutOfMemory異常導致程序崩潰。

注意,這裏的存儲空間並不是指物理內存,而是指虛擬內存大小,這個虛擬內存大小取決於磁盤交換區設定的大小。

 

舉例:

1、單例模式

單例的生命週期和應用程序是一樣長的,所以單例程序中,如果持有對外部對象的引用的話,那麼這個外部對象是不能被回收的,則會導致內存泄漏的產生。

2、一些提供close的資源未關閉導致內存泄漏

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

 

 

 

 

3、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的發生。

 

 

 

 

 

4、垃圾回收的並行與併發

 

併發(Concurrent)

 

  • 在操作系統中,是指一個時間段中有幾個程序都處於已啓動運行到運行完畢之間,且這幾個程序都是在同一個處理器上運行。
  • 併發不是真正意義上的“同時進行” ,只是CPU把一個時間段劃分成幾個時間片段(時間區間),然後在這幾個時間區間之間來回切換, 由於CPU處理的速度非常快,只要時間間隔處理得當,即可讓用戶感覺是多個應用程序同時在進行。

 

並行(Parallel)

  • 當系統有一個以上CPU時,當一個CPU執行一個進程時,另一個CPU可以執行另一個進程兩個進程互不搶佔CPU資源,可以同時進行,我們稱之爲並行(Parallel)。
  • 其實決定並行的因素不是CPU的數量,而是CPU的核心數量,比如一個CPU多個核也可以並行。
  • 適合科學計算,後臺處理等弱交互場景

 

 

併發vs並行

二者對比:

併發,指的是多個事情,在同一時間段內同時發生了。

並行,指的是多個事情,在同一時間點上同時發生了。

併發的多個任務之間是互相搶佔資源的。並行的多個任務之間是不互相搶佔資源的。

只有在多CPU或者一個CPU多核的情況中,纔會發生並行。否則,看似同時發生的事情,其實都是併發執行的。

 

 

垃圾回收的併發與並行

併發和並行,在談論垃圾收集器的上下文語境中,它們可以解釋如下

  • 並行(Parallel) :指多條垃圾收集線程並行工作,但此時用戶線程仍處於等待狀態。

> 如 ParNew 、 Parallel Scavenge 、 Parallel Old;

  • 串行(Serial)

> 相較於並行的概念,單線程執行。

> 如果內存不夠,則程序暫停,啓動JVM垃圾回收器進行垃圾回收。回收完,再啓動程序的線程。

 

 

併發和並行,在談論垃圾收集器的上下文語境中,它們可以解釋如下:

  • 併發(Concurrent) :指用戶線程與垃圾收集線程同時執行(但不一定是並行的,可能會交替執行) ,垃圾回收線程在執行時不會停頓用戶程序的運行。

> 用戶程序在繼續運行,而垃圾收集程序線程運行於另一個CPU上;

> 如: CMS 、G1

 

 

 

 

 

5、安全點與安全區域

 

安全點

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

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的信號爲止;

 

 

 

6、再談引用:強引用

 

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

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

在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()"這種引用關係。無論任何情況下,只要強引用關係還存在,垃圾收集器就永遠不會回收掉被引用的對象。
  • 軟引用(SoftReference) :在系統將要發生內存溢出之前,將會把這些對象列入回收範圍之中進行第二次回收。如果這次回收後還沒有足夠的內存,纔會拋出內存溢出異常。
  • 弱引用(WeakReference) :被弱引用關聯的對象只能生存到下一次垃圾收集之前。當垃圾收集器工作時,無論內存空間是否足夠,都會回收掉被弱引用關聯的對象。
  • 虛引用(PhantomReference) :一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來獲得一個對象的實例。爲一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。

 

 

強引用( Strong Reference )-不回收

在Java程序中,最常見的引用類型是強引用(普通系統99%以上都是強引用) ,也就是我們最常見的普通對象引用,也是默認的引用類型。

 

當在Java語言中使用new操作符創建一個新的對象,並將其賦值給一個變量的時候,這個變量就成爲指向該對象的一個強引用。

強引用的對象是可觸及的,垃圾收集器就永遠不會回收掉被引用的對象。

對於一個普通的對象,如果沒有其他的引用關係,只要超過了引用的作用域或者顯式地將相應(強)引用賦值爲null,就是可以當做垃圾被收集了,當然具體回收時機還是要看垃圾收集策略。

相對的, 軟引用、弱引用和虛引用的對象是軟可觸及、弱可觸及和虛可觸及的,在一定條件下,都是可以被回收的。所以,強引用是造成Java內存泄漏的主要原因之一。

 

 

強引用例子:

 

局部變量 str 指向StringBuffer 實例所在堆空間,通過str可以操作該實例,那麼str就是StringBuffer實例的強引用

對應內存結構:

 

此時,如果再運行一個賦值語句:

 

對應內存結構:

 

本例中的兩個引用,都是強引用,強引用具備以下特點:

  • 強引用可以直接訪問目標對象。
  • 強引用所指向的對象在任何時候都不會被系統回收,虛擬機寧願拋出OOM異常,也不會回收強引用所指向對象。
  • 強引用可能導致內存泄漏。

 

 

 

7、再談引用:軟引用

 

軟引用( Soft Reference)-內存不足即回收

軟引用是用來描述一些還有用,但非必需的對象。只被軟引用關聯着的對象,在系統將要發生內存溢出異常前,會把這些對象列進回收範圍之中進行第二次回收,如果這次回收還沒有足夠的內存,纔會拋出內存溢出異常。

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

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

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

 

在JDK 1.2版之後提供了java.lang.ref. SoftReference類來實現軟引用

 

 

 

 

8、再談引用:弱引用

 

弱引用( Weak Reference )-發現即回收

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

但是,由於垃圾回收器的線程通常優先級很低,因此,並不一定能很快地發現持有弱引用的對象。在這種情況下,弱引用對象可以存在較長的時間。

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

軟引用、弱引用都非常適合來保存那些可有可無的緩存數據。如果這麼做,當系統內存不足時,這些緩存數據會被回收,不會導致內存溢出。而當內存資源充足時,這些緩存數據又可以存在相當長的時間,從而起到加速系統的作用。

 

在JDK 1.2版之後提供了java.lang.ref.WeakReference類來實現弱引用。

 

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

 

面試題:你開發中使用過WeakHashMap嗎?

 

 

 

 

 

9、再談引用:虛引用

 

虛引用( Phantom Reference)-對象回收跟蹤

 

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

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

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

爲一個對象設置虛引用關聯的唯一目的在於跟蹤垃圾回收過程。比如:能在這個對象被收集器回收時收到一個系統通知。

 

 

  • 虛引用必須和引用隊列一起使用。虛引用在創建時必須提供一個引用隊列作爲參數。當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象後,將這個虛引用加入引用隊列,以通知應用程序對象的回收情況。
  • 由於虛引用可以跟蹤對象的回收時間,因此,也可以將一些資源釋放操作放置在虛引用中執行和記錄。
  • 在JDK 1.2版之後提供了PhantomReference類來實現虛引用。

 

 

 

10、再談引用:終結器引用

 

  • 它用以實現對象的finalize ()方法,也可以稱爲終結器引用。
  • 無需手動編碼,其內部配合引用隊列使用。
  • 在GC時,終結器引用入隊。由Finalizer線程通過終結器引用找到被引用對象並調用它的finalize ()方法,第二次GC時才能回收被引用對象。

 

 

 

 

 

 

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章