天天看點

JVM詳解(十一)——垃圾回收算法(補充)

  通過System.gc()或Runtime.getRuntime().gc()的調用,會顯式觸發Full GC,對新生代和老年代進行回收,釋放垃圾對象占用的記憶體。

  然而System.gc()調用附帶一個免責聲明,它無法保證對垃圾收集器的調用。即:隻是告訴JVM希望調用一次,但不保證一定被調用,啥時候調用。

  JVM實作者可以通過System.gc()調用來決定JVM的GC行為,然而一般情況下,垃圾回收應該是自動進行的,無須手動觸發,否則就太過于麻煩了。

  代碼示例:

  在執行gc之前,對象的finalize()方法會執行一次。下面這句話就應該會被列印。多運作幾次,可能會列印出來,說明了System.gc()方法不一定一定執行GC操作。

  代碼示例:手動gc了解不可達對象的回收行為

  記憶體溢出:Javadoc中對OOM的解釋是,沒有空閑記憶體,并且垃圾收集器也無法提供更多記憶體。沒有空閑記憶體,原因有:

  (1)Java虛拟機的堆記憶體設定不夠。

  (2)代碼中建立了大量大對象,且長時間不能被垃圾收集器收集。

  通常在抛出OOM之間,垃圾收集器會被觸發,盡可能的清理空間。比如:在引用機制分析中,JVM會去嘗試回收軟引用指向的對象等;在java.nio.BIts.reserveMemory()方法中,System.gc()會被調用,以清理空間。

  當然,也不是在任何情況下垃圾收集器都會被觸發。比如:配置設定一個超大對象,已經超過堆的最大值,JVM可以判斷出垃圾收集并不能解決這個問題,就會直接抛出OOM。

  記憶體洩露(Memory Leak):嚴格來說,隻有對象不再被程式用到了,但是GC又不能回收的情況,才叫記憶體洩漏。但實際上,一些不太好的實踐(或疏忽)會導緻對象的生命周期變得很長,導緻OOM,也可以叫寬泛意義上的"記憶體洩漏"。

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

JVM詳解(十一)——垃圾回收算法(補充)

  記憶體洩漏案例:

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

  (2)一些提供close的資源未關閉導緻記憶體洩漏:資料庫連接配接,網絡連接配接,io流等,必須手動close,否則是不能被回收的。

  STW:指GC發生過程中,會産生應用程式的停頓。停頓時,整個應用程式線程都會被暫停,沒有任何響應,有點像卡死的感覺。

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

  (1)分析工作必須在一個能確定一緻性的快照中進行。

  (2)一緻性指整個分析期間,執行系統看起來像被當機在某個時間點上。如果分析過程中,對象引用關系還在不斷變化,則分析結果的準确性就無法保證。

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

  STW和采用哪款GC無關,所有的GC都會有。隻能說垃圾回收期越來越優秀,回收效率越來越高,盡可能的縮短STW時間。

  STW是JVM在背景自動發起和自動完成的,在使用者不可見的情況下,把使用者線程全部停掉。

  代碼示例:STW

  并發:不是真正意義上的"同時進行",隻是CPU在多個應用程式之間來回切換,由于CPU處理速度非常快,讓使用者感覺是在同時進行。

JVM詳解(十一)——垃圾回收算法(補充)

  并行:系統有多個CPU時,一個CPU執行一個程序,另一個CPU執行另一個程序,互不搶占CPU資源,同時進行。

  其實,決定并行的因素不是CPU的數量,而是CPU的核心數量,比如一個CPU多個核也可以并行。

JVM詳解(十一)——垃圾回收算法(補充)

  并發:多個事情,在同一時間段内同時發生。

  并行:多個事情,在同一時間點上同時發生。

  并發的多個任務之間是互相搶占資源的,并行的多個任務之間是不互相搶占資源的。隻有在多CPU或一個CPU多核的情況下,才會發生并行。否則,看似同時發生的事情,其實都是并發執行的。

  垃圾回收的并行與并發

  并行(Parallel):指多條垃圾收集線程并行工作,但此時使用者線程仍處于等待狀态。比如:ParNew、Parallel Scavenge、Parallel Old。

  串行(Serial):相較于并行,單線程執行。如果記憶體不足,則程式暫停,啟動JVM垃圾回收器進行垃圾回收,回收完,再啟動程式的線程。

JVM詳解(十一)——垃圾回收算法(補充)

  并發(Concurrent):指使用者線程與垃圾收集線程同時執行(但不一定是并行的,可能會交替執行),垃圾回收線程在執行時不會停頓使用者程式的運作。比如:CMS,G1。

JVM詳解(十一)——垃圾回收算法(補充)

  安全點:程式執行時并非在所有地方都能停頓下來GC,隻有在特定位置才能停頓下來開始GC,這些位置稱為"安全點"。類似于高速路的服務區,并不是所有的地方都能停車,隻有到達服務區後,才能停車。

  安全點的選擇很重要,如果太少可能導緻GC等待時間太長,如果太頻繁可能導緻運作時的性能問題。大部分指令的執行時間都非常短暫,通常會根據"是否具有讓程式長時間執行的特征"為标準。比如選擇一些執行時間較長的指令作為安全點,如方法調用,循環跳轉,異常處理等。

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

  (1)搶先式中斷(目前沒有虛拟機采用了):首先中斷所有線程,如果還有線程不在安全點,就恢複線程,讓線程跑到安全點。

  (2)主動式中斷:設定一個中斷标志,各個線程運作到安全點的時候主動輪詢這個标志。如果中斷标志為真,則将自己進行中斷挂起。

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

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

  實際執行時,當線程運作到安全區域的代碼時,首先辨別已經進入了安全區域,如果這段時間内發生GC,JVM會忽略辨別為安全區域狀态的線程。

  當線程即将離開安全區域時,會檢查JVM是否已經完成GC,如果完成了,則繼續運作。否則線程必須等待直到收到可以安全離開安全區域的信号為止。

  強引用(StrongReference):是指在程式代碼中普遍存在的引用指派,即:Object obj = new Object(),這種引用關系。無論任何情況下,隻要引用關系還在,垃圾收集器就永遠不會回收掉被引用的對象。

  軟引用(SoftReference):系統在發生記憶體溢出之前,将會把這些對象列入回收範圍進行二次回收。如果這次回收後還沒有足夠的記憶體,才會抛出記憶體溢出。

  弱引用(WeakReference):被弱引用關聯的對象隻能生存到下一次垃圾收集之前。當垃圾收集器工作時,無論記憶體空間是否足夠,都會回收被弱引用關聯的對象。

  虛引用(PhantomReference):一個對象是否有虛引用的存在,完全不會對其生存時間造成影響,也無法通過虛引用來獲得一個對象的執行個體。為一個對象設定虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。

  99%都是用的強引用;軟、弱,用于緩存的場景下;虛引用,主要用于對象回收的跟蹤。

JVM詳解(十一)——垃圾回收算法(補充)

  強引用的對象是可觸及的,垃圾收集器永遠不會回收被引用的對象。相對的,軟引用、弱引用和虛引用的對象是軟可觸及、弱可觸及和虛可觸及。在一定條件下,都是可以被回收的。是以,強引用是造成Java記憶體洩漏的主要原因之一。

  代碼示例:強引用

  軟引用是用來描述一些還有用,但非必需的對象。隻被軟引用關聯的對象,在系統發生記憶體溢出前,會把這些對象列進回收範圍中進行第二次回收(第一次,指不可達的對象)。如果這次回收還沒有足夠的記憶體,才會抛出記憶體溢出。

  軟引用通常用來實作記憶體敏感的緩存。比如:高速緩存,如果還有空閑記憶體,就暫時保留緩存,當記憶體不足時就清理掉。這樣就保證了使用緩存的同時,不會耗盡記憶體。軟引用,不會引起OOM。

  記憶體足夠:不會回收軟引用的可達對象。

  記憶體不夠:會回收軟引用的可達對象。

  代碼示例:軟引用

  弱引用也是用來描述非必需對象,隻被弱引用關聯的對象隻能生存到下一次垃圾收集發生為止。在系統GC時,不管堆空間是否足夠,都會回收隻被弱引用關聯的對象。

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

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

  應用:三級緩存,記憶體—>本地—>網絡。用WeakHashMap去存儲圖檔資訊,就可以在記憶體不足的時候,及時回收資料。

  代碼示例:弱引用

  一個對象是否有虛引用的存在,完全不會決定對象的生命周期。若隻有虛引用,那麼它和沒有引用幾乎是一樣的,随時都可能被垃圾回收器回收。

  它不能單獨使用,也無法通過虛引用來擷取被引用的對象。

  為一個對象設定虛引用關聯的唯一目的在于跟蹤垃圾回收過程。能在這個對象被收集器回收時收到一個系統通知。

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

  由于虛引用可以跟蹤對象的回收時間,是以,也可以将一些資源釋放操作放置在虛引用中執行和記錄。

  代碼示例:虛引用

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

作者:Craftsman-L

繼續閱讀