深刻理解 JVM 之 垃圾回收機制

雖然內存的分配和回收技術已至關成熟,但若是須要排查內存溢出、內存泄露問題,或者要求高併發、高性能時,就須要對垃圾的回收進行監控和調節,以更好優化系統提升性能。html

對象存活斷定

Java 內存結構中,程序計數器、虛擬機棧、本地方法棧等隨着線程而生,隨線程而滅,不須要考慮內存回收問題。而 Java 堆和方法區則不一樣,它們的內存分配是動態的,只有在運行期間才能知道會建立哪些對象,垃圾回收關注的就是這兩部分。java

垃圾回收首先須要判斷哪些對象還存活着,主要有引用計數和可達性分析兩種算法。算法

引用計數算法

它的原理以下:給對象添加一個引用計數器,每當有一個地方引用它時,計時器值就加 1;當引用失效時,計數器值就減 1;若是計數器爲 0,對象就不可能再被使用。數組

引用計數算法雖然實現簡單、斷定效率較高。但它很難解決對象之間循環引用的問題。緩存

例如兩個對象相互引用,實際上兩個對象都不會再訪問,但由於相互引用着對方,致使它們的計數器值都不爲 0,因而引用技術算法沒法經過 GC 收集器回收它們。併發

可達性分析算法

它的原理以下:經過一系列稱爲 GC Roots 的對象做爲起始點,從這些節點開始向下搜索,搜索所走過的路徑稱爲引用鏈。當一個對象到 GC Roots 沒有任何引用鏈相連時,則證實對象是不可用的。高併發

Java 中,可做爲 GC Roots 的對象包括以下幾種:性能

  • 虛擬機棧(棧幀中的本地變量表)中引用的對象;
  • 方法區中類靜態屬性引用的對象;
  • 方法區中常量引用的對象;
  • 本地方法棧中 JNI( Native 方法) 引用的對象。

引用類型

能夠看到,對象回收斷定算法判斷對象是否存活都與引用有關。從 JDK1.2 開始,引用分爲四種類型,用來實現不一樣的功能,它們的引用強度也依次遞減。優化

強引用(Strong Reference)線程

平時使用的引用就是強引用。只要強引用還存在,該對象永遠不會被回收。

能夠經過將對象設置爲 null,使其被回收。

軟引用(Soft Reference)

用於描述一些還有用但並不是必需的對象。當系統內存空間不足時,會回收這些軟引用指向的對象。它經過 SoftReference 類來實現軟引用。

能夠用來實現高速緩存。

弱引用(Weak Reference)

用來描繪非必需對象。被弱引用指向的對象只能生存到下一次垃圾回收以前。只要垃圾收集器運行,弱引用指向的對象就會被回收。它經過 WeakReference 類來實現弱引用。

虛引用(Phantom Reference)

虛引用和沒有引用沒有任何區別。一個對象是否有虛引用,不會影響其生存時間,也沒法經過虛引用獲取對象實例。它經過 PhantomReference 來實現虛引用。必須和引用隊列 ReferenceQueue 聯合使用。

爲一個對象設置虛引用的惟一目的是該對象被垃圾收集器回收前會收到一條系統通知。

回收方法區

方法區,或者說 HotSpot 虛擬機中的永久代,進行垃圾回收的效率通常比較低。回收主要包括兩部份內容:廢棄常量和無用的類。

判斷一個常量是不是廢棄常量比較簡單,與回收 Java 堆中的對象相似。而斷定一個類是不是無用的類須要知足三個條件:

  • 該類全部的實例都已經被回收;
  • 加載該類的 ClassLoader 已經被回收;
  • 該類對象的 java.lang.Class 對象沒有在任何地方被引用,沒法在任何地方經過反射訪問該類的方法。

垃圾收集算法

標記-清除算法(Mark-Sweep)

標記-清除算法分爲兩個標記和清除階段:首先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象。標記過程也就是對象存活斷定算法。

它是最基礎的收集算法,主要有兩個缺點:

  • 效率問題:標記和清除兩個過程的效率都不高。
  • 空間問題:標記清除以後會產生大量不連續的內存碎片,空間碎片太多可能會致使之後在程序運行過程當中須要分配較大的對象時,沒法找到足夠的連續內存而不得不提早觸發另外一次垃圾收集動做。

複製算法(Copying)

複製算法將可用內存分爲大小相等的兩塊,每次只使用其中的一塊。在一塊內存用完後,將仍存活的對象賦值到另外一塊上面,再把已使用過的內存一次清理掉。

複製算法的優缺點以下:

  • 優勢:每次對半個分區進行內存回收,內存分配時也不用考慮內存碎片等狀況,實現簡單,運行高效。
  • 缺點:可以使用的內存縮小爲一半,代價較大。

標記-整理算法(Mark-compact)

標記-整理算法分爲標記和整理兩個階段,標記階段和「標記-清除算法」同樣,但在整理階段,不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉端邊界之外的內存。

標記-整理算法的優缺點以下:

  • 避免了空間碎片,空間利用率較高。
  • 效率不高,標記和清除過程的效率較低。

分代算法(Generational Collection)

分代算法根據對象存活週期將內存劃分爲幾塊。通常是將 Java 對分爲新生代和老年代,根據各個年代的特色採用適當的收集算法。

新生代中,每次垃圾收集時只有少許對象存活,選擇複製算法;老年代中,對象存活率較高、沒有額外空間進行分配,使用「標記-清理」或「標記-整理」算法。

爲了對不一樣生命週期的對象採用不一樣的回收算法,因此垃圾收集器都採用分代收集算法,將堆分爲新生代和老年代。

  

內存分配和回收策略

新生代

新生代主要用來存放新建立的對象,通常佔堆 1/3 的空間。因爲不少對象生命週期很短,每次 Minor GC 後只有少許對象存活,因此選用複製算法。

新生代又被分爲一塊較大的 Eden 區和兩塊較小的大小相等的 Survivor 區,使用 fromto 來分別指代兩個 Survivor 區。HotSpot 虛擬機默認 Eden 和兩塊 Survivor 的大小比例爲 8:1:1。每次只會使用 Eden 和其中一塊 Survivor 區爲對象服務,因此老是有一塊 Survivor 區是空閒的,新生代實際可用的內存空間也就爲 90%

一般,對象會分配在 Eden 區中,當 Eden 區沒法在分配對象時,JVM 便會觸發一次 Minor GC,將存活下來的對象複製到 from 指向的 Survivor 區中。

from 指向的 Survivor 區也沒法分配時,對 Edenfrom 指向的 Survivor 區執行 Minor GC,將存活下來的對象複製到 to 指向的 Survivor 區中,而後交換 fromto 指針,使 to 指向的 Survivor 區爲空,以保證下次 Minor GC 有複製的空閒空間。

老年代

老年代用於存放大對象,或年齡超過必定程度的對象。通常佔據堆 2/3 的空間。

若是對象須要大量連續的內存空間,例如很長的字符串及數組,這些對象會直接分配在老年代,以免在 Eden 區及兩個 Survivor 區之間發生大量的內存複製。

虛擬機爲每一個對象定義了一個對象年齡計數器,若是對象分配在 Eden 區,在通過一次 Minor GC 後仍然存活,以後移動到 Survivor 空間中,將其年齡設置爲 1。對象在 Survivor 區中每通過一次 Minor GC,年齡就增長一次,當它的年齡增長到必定程度(默認爲 15)時,也會被晉升到老年代中。

若是在 Survivor 區中相同年齡全部對象大小的總和大於 Survivor 區的一半,年齡大於或等於該年齡的對象就能夠直接進入老年代。

老年代的對象通常都比較穩定,Major GC 不會頻繁執行。Major GC 採用標記—清除算法:首先掃描一次全部老年代,標記出存活的對象,而後回收沒有標記的對象。MajorGC 的耗時較長,並且會產生內存碎片。

三種清理方式

Minor GC(Young GC)

指發生在新生代的垃圾收集動做。當 Eden 區沒有足夠的空間分配時,就會觸發一次 Minor GC。因爲 Java 對象大多生命週期較短,因此 Minor GC 很是頻繁,通常回收速度也比較快。

Major GC

指發生在老年代的垃圾收集動做,在進行 Major GC 前,通常都會進行至少一次 Minor GCMajor GC 的速度通常會比 Minor GC10 倍以上。

Full GC

指回收整個新生代和老年代的垃圾收集動做。成本較高,對系統性能產生影響。FULL GC 的時候會 STOP THE WORD

它的觸發條件主要有:

  • 在執行 Minor GC 以前,若是老年代最大可用的連續空間小於歷次晉升到老生代對象的平均大小,則觸發一次 Full GC
  • 大對象直接進入老年代,或從年輕代晉升上來的老對象,在老年代嘗試分配內存,但老年代內存空間不夠時。
  • 顯式調用 System.gc() 方法時。

參考資料

相關文章
相關標籤/搜索