【jvm】8-垃圾回收
教程:https://www.bilibili.com/video/BV1PJ411n7xZ
垃圾:指在運行程序中沒有任何指針指枸的對象,這個對象就是需要被回收的垃圾。
垃圾回收器可以對年輕代回收,也可以對老年代回收,甚至是全堆和方法區的回收。
- Java堆是垃圾收集器的工作重點。從次數上講:
- 頻繁收集Young區
- 較少收集old區
- 基本不動Pern區
垃圾標記階段
在堆里存放著幾乎所有的Java對象實例,在Gc執行垃圾回收之前,首先需要區分出內存中哪些是存活對象,哪些是已經死亡的對象。只有被標記為己經死亡的對象,GC才會在執行垃圾回收時,釋放掉其所占用的內存空間,因此這個過程我們可以稱為垃圾標記階段。
- 那么在JVM中究竟是如何標記一個死亡對象呢?簡單來說,當一個對象已經不再被任何的存活對象繼續引用時,就可以宣判為已經死亡。
- 判斷對象存活一般有兩種方式:引用計數算法和可達性分析算法。
引用計數算法
- 引用計數算法(Reference Counting)比較簡單,對每個對象保存一個整型的引用計數器屬性。用于記錄對象被引用的情況。
- 對于一個對象A,只要有任何一個對象引用了A,則A的引用計數器就加1;當引用失效時,引用計數器就減1。只要對象A的引用計數器的值為0,即表示對象A不可能再被使用,可進行回收。
- 優點:
- 實現簡單,垃圾對象便于辨識
- 判定效率高,回收沒有延遲性。
- 缺點:
- 它需要單獨的字段存儲計數器,這樣的做法增加了存儲空間的開銷。(空間角度)
- 每次賦值都需要更新計數器,伴隨著加法和減法操作,這增加了時間開銷。(時間角度)
- 引用計數器有一個嚴重的問題,即無法處理循環引用的情況。這是一條致命缺陷,導致在Java的垃圾回收器中沒有使用這類算法。
循環引用:p指向null,其余三個對象都不再使用但由于循環引用,導致無法被gc回收。
可達性分析(根搜索算法、追蹤性垃圾收集)
- 相對于引用計數算法而言,可達性分析算法不僅同樣具備實現簡單和執行高效等特點,更重要的是該算法可以有效地解決在引用計數算法中循環引用的問題,防止內存泄漏的發生。
- 相較于引用計數算法,這里的可達性分析就是Java、C#選擇的。這種類型的垃圾收集通常也叫作追蹤性垃圾收集(Tracing Garbage collection) 。
GC Roots:根集合就是一組必須活躍的引用。
基本思路:
- 可達性分析算法是以根對象集合(GC Roots)為起始點,按照從上/至下的方式搜索被根對象集合所連接的目標對象是否可達。l
- 使用可達性分析算法后,內存中的存活對象都會被根對象集合直接或間接連接著,搜索所走過的路徑稱為引用鏈(Reference Chain)
- 如果目標對象沒有任何引用鏈相連,則是不可達的,就意味著該對象己經死亡,可以標記為垃圾對象。
- 在可達性分析算法中,只有能夠被根對象集合直接或者間接連接的對象才是存活對象。
在Java語言中,GC Roots包括以下幾類元素:
- 虛擬機棧中引用的對象
- 比如:各個線程被調用的方法中使用到的參數、局部變量等。
- 本地方法棧內JNI(通常說的本地方法)引用的對象
- 方法區中類靜態屬性引用的對象
- 比如: Java類的引用類型靜態變量
- 方法區中常量引用的對象
- 比如:字符串常量池〈String Table)里的引用·所有被同步鎖synchronized持有的對象
- Java虛擬機內部的引用。
- 基本數據類型對應的class對象,一些常駐的異常對象(如:NullPointerException、OutofMemoryError),系統類加載器。
- 反映java虛擬機內部情況的JMXBean、JVMTI中注冊的回調、本地代碼緩存等。
藍色可達,紅色不可達作為垃圾。
tips:由于Root采用棧方式存放變量和指針,所以如果一個指針,它保存了堆內存里面的對象,但是自己又不存放在堆內存里面,那它就是一個RootI
注意
- 如果要使用可達性分析算法來判斷內存是否可回收,那么分析工作必須一個能保障一致性的快照中進行。
- 這點不滿足的話分析結果的準確性就法保證。
- 這點也是導致Gc進行時必須"stop The world"的一個重要原因。
- 即使是號稱(幾乎)不會發生停頓的CMS 收集器中,枚舉根節點時也是必須要停頓的。
對象finalization機制
-
Java語言提供了對象終止(finalization)機制來允許開發人員提供對象被銷毀之前的自定義處理邏輯。
-
當垃圾回收器發現沒有引用指向一個對象,即:垃圾回收此對象之前,總會先調用這個對象的finalize()方法。
-
finalize ()方法允許在子類中被重寫,用于在對象被回收時進行資源釋放通常在這個方法中進行一些資源釋放和清理的工作,比如關閉文件、套接字和數據庫連接等。
-
永遠不要主動調用某個對象的linalize()方法,應該交給垃圾回收機制調用。理由包括下面三點:
- 在finalize ( )時可能會導致對象復活。
- finalize ()方法的執行時間是沒有保障的,它完全由cc線程決定,極端情況下,若不發生Gc,則finalize ()方法將沒有執行機會。
- 一個糟糕的finalize()會嚴重影響Gc的性能。
-
從功能上來說,finalize ()方法與C++中的析構函數比較相似,但是Java采用的是基于垃圾回收器的自動內存管理機制,所以finalize()方法在本質上不同于c++中的析構函數。
-
由于finalize()方法的存在,虛擬機中的對象一般處于三種可能的狀態。
對象銷毀:根節點無法訪問某對象,此對象并非“非死不可”,這時處于“緩刑”階段。一個無法觸及的對象有可能在某一個條件下“復活”自己,如果這樣,那么對它的回收就是不合理的。為此,定義虛擬機中的對象可能的三種狀態如下:
- 可觸及:從根節點開始,可以到達這個對象。
- 可復活:對象的所有引用都被釋放,但是對象有可能在finalize ()中復活。
- 不可觸及:對象的finalize ( )被調用,并且沒有復活,那么就會進入不可觸及狀態。不可觸及的對象不可能被復活,因為finalize ()只會被調用一次。
以上3種狀態中,是由于finalize()方法的存在,進行的區分。只有在對象不可觸及時才可以被回收。
執行過程:判定一個對象objA是否可回收,至少要經歷兩次標記過程:
- 如果對象objA到 GC Roots沒有引用鏈,則進行第一次標記。
- 進行篩選,判斷此對象是否有必要執行finalize ()方法
- 如果對象objA沒有重寫finalize()方法,或者finalize()方法已經被虛擬機調用過,則虛擬機視為“沒有必要執行”,objA被判定為不可觸及的。
- 如果對象objA重寫了finalize()方法,且還未執行過,那么objA會被插入到F-Queue隊列中,由一個虛擬機自動創建的、低優先級的Finalizer線程觸發其finalize()方法執行。
- finalize()方法是對象逃脫死亡的最后機會,稍后GC會對F-Queue隊列中的對象進行第二次標記。如果objA在finalize()方法中與引用鏈上的任何一個對象建立了聯系,那么在第二次標記時,objA會被移出“即將回收”集合。之后,對象會再次出現沒有引用存在的情況。在這個情況下,finalize方法不會被再次調用,對象會直接變成不可觸及的狀態,也就是說,一個對象的finalize方法只會被調用一次。
垃圾清除階段
當成功區分出內存中存活對象和死亡對象后,cC接下來的任務就是執行垃圾回收,釋放掉無用對象所占用的內存空間,以便有足夠的可用內存空間為新對象分配內存。目前在JVM中比較常見的三種垃圾收集算法:
- 標記-清除算法(Mark-Sweep)
- 復制算法(copying)
- 標記-壓縮算法(Mark-Compact)
標記-清除算法(Mark-Sweep)
背景:標記–清除算法( Mark-Sweep )是一種非常基礎和常見的垃圾收集算法,該算法被J.McCarthy等人在1960年提出并并應用Lisp語言。
執行過程:當堆中的有效內存空間(available memory)被耗盡的時候,就會停止整個程序(也被稱為stop the world),然后進行兩項工作,第一項則是標記,第二項則是清除。
- 標記:Collector從引用根節點開始遍歷,標記所有被引用的對象。一般是在對象的Header中記錄為可達對象。
- 清除:Collector對堆內存從頭到尾進行線性的遍歷,如果發現某個對象在其Header中沒有標記為可達對象,則將其回收。
缺點
- 效率不算高
- 在進行cc的時候,需要停止整個應用程序,導致用戶體驗差
- 這種方式清理出來的空閑內存是不連續的,產生內存碎片。需要維護一個空閑列表
注意:何為清除?
這里所謂的清除并不是真的置空,而是把需要清除的對象地址保存在空閑的地址列表里。下次有新對象需要加載時,判斷垃圾的位置空間是否夠,如果夠,就存放。
復制算法(copying)
核心思想:將活著的內存空間分為兩塊,每次只使用其中一塊,在垃圾回收時將正在使用的內存中的存活對象復制到未被使用的內存塊中,之后清除正在使用的內存塊中的所有對象,交換兩個內存的角色,最后完成垃圾回收。
A存活對象直接賦值到B,復制完后對于A直接整塊銷毀。AB兩塊內存如此往復使用。
優點:
- 沒有標記和清除過程,實現簡單,運行高效
- 復制過去以后保證空間的連續性,不會出現“碎片”問題。
缺點:
- 此算法的缺點也是很明顯的,就是需要兩倍的內存空間。
- 對于G1這種分拆成為大量region的Gc,復制而不是移動,意味著Gc需要維護region之間對象引用關系,不管是內存占用或者時間開銷也不小。
- 特別的:如果系統中的垃圾對象很多,復制算法需要復制的存活對象數量并不會太大,而如果垃圾對象很少,則需要復制的存活對象數量就會很多。
- 新生代中幸存者區由于大部分對象需要回收,使用復制算法比較好,老年代由于垃圾對象少,所以不使用復制算法比較好。
標記-壓縮算法(Mark-Compact)
也叫標記-整理算法。
執行過程:
- 第一階段和標記-清除算法一樣,從根節點開始標記所有被引用對象
- 第二階段將所有的存活對象壓縮到內存的一端,按順序排放。之后,清理邊界外所有的空間。
- 標記-壓縮算法的最終效果等同于標記-清除算法執行完成后,再進行一次內存碎片整理,因此,也可以把它稱為標記-清除-壓縮(Mark-Sweep-Compact)算法。
- 二者的本質差異在于標記-清除算法是一種非移動式的回收算法,標記-壓縮是移動式的。是否移動回收后的存活對象是一項優缺點并存的風險決策。
- 可以看到,標記的存活對象將會被整理,按照內存地址依次排列,而未被標記的內存會被清理掉。如此一來,當我們需要給新對象分配內存時,JVM只需要持有一個內存的起始地址即可,這比維護一個空閑列表顯然少了許多開銷。
優點:
- 消除了標記-清除算法當中,內存區域分散的缺點,我們需要給新對象內存時,JVM只需要持有一個內存的起始地址即可。
- 消除了復制算法當中,內存減半的高額代價。
缺點:
- 從效率上來說,標記-整理算法要低于復制算法。
- 移動對象的同時,如果對象被其他對象引用,則還需要調整引用的地址。
- 移動過程中,需要全程暫停用戶應用程序。即:STW
三種清除算法對比
| 速度 | 中等 | 最慢 | 最快 |
| 空間開銷 | 少(但會堆積碎片) | 少(不堆積碎片) | 通常需要活對象的2倍大小(不堆積碎片) |
| 移動對象 | 否 | 是 | 是 |
分代收集算法:是基于這樣一個事實:不同的對象的生命周期是不一樣的。因此,不同生命周期的對象可以采取不同的收集方式,以便提高回收效率。一般是把Java堆分為新生代和老年代,這樣就可以根據各個年代的特點使用不同的回收算法,以提高垃圾回收的效率。
增量收集算法:
上述現有的算法,在垃圾回收過程中,應用軟件將處于一種stop the world的狀態。在stop the world 狀態下,應用程序所有的線程都會掛起,暫停一切正常的工作,等待垃圾回收的完成。如果垃圾回收時間過長,應用程序會被掛起很久,將嚴重影響用戶體驗或者系統的穩定性。為了解決這個問題,即對實時垃圾收集算法的研究直接導致了增量收集(Incremental Collecting)算法的誕生。
-
基本思想:如果一次性將所有的垃圾進行處理,需要造成系統長時間的停頓,那么就可以讓垃圾收集線程和應用程序線程交替執行。每次,垃圾收集線程只收集一小片區域的內存空間,接著切換到應用程序線程。依次反復,直到垃圾收集完成。
-
總的來說,增量收集算法的基礎仍是傳統的標記-清除和復制算法。增量收集算法通過對線程間沖突的妥善處理,允許垃圾收集線程以分階段的方式完成標記、清理或復制工作。
-
缺點:使用這種方式,由于在垃圾回收過程中,間斷性地還執行了應用程序代碼,所以能減少系統的停頓時間。但是,因為線程切換和上下文轉換的消耗,會使得垃圾回收的總體成本上升,造成系統吞吐量的下降。
垃圾回收相關概念
System.gc()
- 在默認情況下,通過system.gc()或者Runtime. getRuntime ( ).gc()的調用,會顯式觸發Full Gc,同時對老年代和新生代進行回收,嘗試釋放被丟棄對象占用的內存。
- 然而system.gc()調用附帶一個免責聲明,無法保證對垃圾收集器的調用。
- JVM實現者可以通過system.gc ()調用來決定JVM的Gc行為。而一般情況下,垃圾回收應該是自動進行的,無須手動觸發,否則就太過于麻煩了。在一些特殊情況下,如正在編寫一個性能基準,可以在運行之間調用system.gc ( )。
內存溢出(OOM)
-
由于Gc一直在發展,所有一般情況下,除非應用程序占用的內存增長速度非常快,造成垃圾回收已經跟不上內存消耗的速度,否則不太容易出現ooM的情況。大多數情況下,Gc會進行各種年齡段的垃圾回收,實在不行了就放大招,來一次獨占式的Full Gc操作,這時候會回收大量的內存,供應用程序繼續使用。
-
javadoc中對outofMemoryError的解釋是,沒有空閑內存,并且垃圾收集器也無法提供更多內存。
內存溢出:說明java虛擬機堆內存不夠,原因有二:
- Java虛擬機的堆內存設置不夠。
- 比如:可能存在內存泄漏問題;也很有可能就是堆的大小不合理,比如我們要處理比較可觀的數據量,但是沒有顯式指定JVM堆大小或者指定數值偏小。我們可以通過參數-Xms、-Xmx來調整。
- 代碼中創建了大量大對象,并且長時間不能被垃圾收集器收集(存在被引用)
內存泄露(Memory Leak)
-
也稱作“存儲滲漏”。嚴格來說,只有對象不會再被程序用到了,但是GC又不能回收他們的情況,才叫內存泄漏。但實際情況很多時候一些不太好的實踐(或疏忽)會導致對象的生命周期變得很長甚至導致OOM,也可以叫做寬泛意義上的“內存泄漏”。
-
注意,這里的存儲空間并不是指物理內存,而是指虛擬內存大小,這個虛擬內存大小取決于磁盤交換區設定的大小。
可達性分析內存泄露舉例:
- 單例模式:單例的生命周期和應用程序是一樣長的,所以單例程序中,如果持有對外部對象的引用的話,那么這個外部對象是不能被回收的,則會導致內存泄漏的產生。
- 一些提供close的資源未關閉導致內存泄漏:數據庫連接(dataSourse.getconnection()),網絡連接(socket)和io連接必須手動close,否則是不能被回收的。
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的發生。
并發與并行
程序并發
-
在操作系統中,是指一個時間段中有幾個程序都處于已啟動運行到運行完畢之間,且這幾個程序都是在同一個處理器上運行。
-
并發不是真正意義上的“同時進行”,只是CP把一個時間段劃分成幾個時間片段(時間區間),然后在這幾個時間區間之間來回切換,由于CPT處理的速度非常快,只要時間間隔處理得當,即可讓用戶感覺是多個應用程序同時在進行。
程序并行
- 當系統有一個以上CPU時,當一個CPU執行一個進程時工另一個cP可以執行另一個進程,兩個進程互不搶占CPU資源,可以同時進行,我們稱之為并行(Parallel)。
- 其實決定并行的因素不是cP的數量,而是CPU的核心數量,比如一個cPU多個核也可以。并行。
- 適合科學計算,后臺處理等弱交互場景
對比:
- 并發:指的是多個事情,在同一時間段內同時發生了。多個任務之間是互相搶占資源的。
- 并行:指的是多個事情,在同一時間點上同時發生了。多個任務之間是不互相搶占資源的。
- 只有在多CPU或者一個CPU多核的情況中,才會發生并行。否則,看似同時發生的事情,其實都是并發執行的。
垃圾回收并發與并行
并行(Parallel):指多條垃圾收集線程并行工作,但此時用戶線程仍處于等待狀態。
-
如ParNew、Parallel Scavenge、Parallel old等垃圾回收器
-
串行(serial)
- 相較于并行的概念,單線程執行。
- 如果內存不夠,則程序暫停,啟動JVM垃圾回收器進行垃圾回收。回收完,再啟動程序的線程。
并發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不一定是并行的,可能會交替執行),垃圾回收線程在執行時不會停頓用戶程序的運行。
- 用戶程序在繼續運行,而垃圾收集程序線程運行于另一個cPU上
- 如:CMz、G1l
安全點與安全區域
安全點
程序執行時并非在所有地方都能停頓下來開始Gc,只有在特定的位置才能停頓下來開始Gc,這些位置稱為“安全點(Safepoint) ”。
- Safe Point的選擇很重要,如果太少可能導致Gc等待的時間太長,如果太頻繁可能導致運行時的性能問題。大部分指令的執行時間都非常短暫,通常會根據“是否具有讓程序長時間執行的特征”為標準。比如:選擇一些執行時間較長的指令作為Safe Point,如方法調用、循環跳轉和異常跳轉等。
如何在Gc發生時,檢查所有線程都跑到最近的安全點停頓下來呢?
- 搶先式中斷:(目前沒有虛擬機采用了)首先中斷所有線程。如果還有線程不在安全點,就恢復線程,讓線程跑到安全點。
- 主動式中斷:設置一個中斷標志,各個線程運行到safe Point的時候主動輪詢這個標志,如果中斷標志為真,則將自己進行中斷掛起。
安全區域
Safepoint機制保證了程序執行時,在不太長的時間內就會遇到可進入GC的 Safepoint 。但是,程序“不執行”的時候呢?例如線程處于sleep 狀態或Blocked 狀態,這時候線程無法響應JVM的中斷請求,“走”到安全點去中斷掛起,JVM也不太可能等待線程被喚醒。對于這種情況,就需要安全區域(safe Region)來解決。
- 安全區域是指在一段代碼片段中,對象的引用關系不會發生變化,在這個區域中的任何位置開始Gc都是安全的。我們也可以把safe Region看做是被擴展了的Safepoint。
實際執行
- 當線程運行到safe Region的代碼時,首先標識已經進入了Safe Region,如果這段時間內發生Gc,JVM會忽略標識為Safe Region狀態的線程;
- 當線程即將離開Safe Region時,會檢查JVM是否已經完成Gc,如果完成了,則繼續運行,否則線程必須等待直到收到可以安全離開Safe Region的信號為止;
引用
在JDK 1.2版之后,Java對引用的概念進行了擴充,將引用分為4種(引用強度依次逐漸減弱)
- 強引用(StrongReference):最傳統的“引用”的定義,是指在程序代碼之中普遍存在的引用賦值,即類似“object obj=new object()”這種引用關系。無論任何情況下,只要強引用關系還存在,垃圾收集器就永遠不會回收掉被引用的對象。
- 軟引用(SoftReference):在系統將要發生內存溢出之前,將會把這些對象列入回收范圍之中進行第二次回收。如果這次回收后還沒有足夠的內存,才會拋出內存溢出異常。
- 弱引用(WeakReference):被弱引用關聯的對象只能生存到下一次垃圾收集之前。當垃圾收集器工作時,無論內存空間是否足夠,都會回收掉被弱引用關聯的對象。
- 虛引用(PhantomReference):一個對象是否有虛引用的存在,完全不會對其生存時間構成影響,也無法通過虛引用來獲得一個對象的實例。為一個對象設置虛引用關聯的唯一目的就是能在這個對象被收集器回收時收到一個系統通知。
強引用–不回收
-
對于一個普通的對象,如果沒有其他的引用關系,只要超過了引用的作用域或者顯式地將相應(強)引用賦值為null,就是可以當做垃圾被收集了,當然具體回收時機還是要看垃圾收集策略。
-
強引用可以直接訪問目標對象。
-
強引用所指向的對象在任何時候都不會被系統回收,虛擬機寧愿拋出OOM異常,也不會回收強引用所指向對象。
-
強引用可能導致內存泄漏。
軟引用–內存不足即回收
- 軟引用是用來描述一些還有用,但非必需的對象。只被軟引用關聯著的對象,在系統將要發生內存溢出異常前,會把這些對象列進回收范圍之中進行第二次回收,如果這次回收還沒有足夠的內存,才會拋出內存溢出異常。
- 軟引用通常用來實現內存敏感的緩存。比如:高速緩存就有用到軟引用。如果還有空閑內存,就可以暫時保留緩存,當內存不足時清理掉,這樣就保證了使用緩存的同時,不會耗盡內存。
- 垃圾回收器在某個時刻決定回收軟可達的對象的時候,會清理軟引用,并可選地把引用存放到一個引用隊列(Reference Queue)。
- 類似弱引用,只不過Java虛擬機會盡量讓軟引用的存活時間長一些,迫不得已才清理。
弱引用–發現即回收
- 弱引用也是用來描述那些非必需對象,只被弱引用關聯的對象只能生存到下一次垃圾收集發生為止。在系統GC時,只要發現弱引用,不管系統堆空間使用是否充足,都會回收掉只被弱引用關聯的對象。
- 但是,由于垃圾回收器的線程通常優先級很低,因此,并不一定能很快地發現持有弱引用的對象。在這種情況下,弱引用對象可以存在較長的時間。
- 弱引用和軟引用一樣,在構造弱引用時,也可以指定一個引用隊列,當弱引用對象被回收時,就會加入指定的引用隊列,通過這個隊列可以跟蹤對象的回收情況。
- 軟引用、弱引用都非常適合來保存那些可有可無的緩存數據。如果這么做,當系統內存不足時,這些緩存數據會被回收,不會導致內存溢出。而當內存資源充足時,這些緩存數據又可以存在相當長的時間,從而起到加速系統的作用。
虛引用–對象回收跟蹤
-
也稱為“幽靈引用”或者“幻影引用”,是所有引用類型中最弱的一個。
-
一個對象是否有虛引用的存在,完全不會決定對象的生命周期。如果一個對象僅持有虛引用,那么它和沒有引用幾乎是一樣的,隨時都可能被垃圾回收器回收。
-
它不能單獨使用,也無法通過虛引用來獲取被引用的對象。當試圖通過虛引用的get()方法取得對象時,總是null。
-
為一個對象設置虛引用關聯的唯一目的在于跟蹤垃圾回收過程。比如:能在這個對象被收集器回收時收到一個系統通知。
-
虛引用必須和引用隊列一起使用。虛引用在創建時必須提供一個引用隊列作為參數。當垃圾回收器準備回收一個對象時,如果發現它還有虛引用,就會在回收對象后,將這個虛引用加入引用隊列,以通知應用程序對象的回收情況。
-
由于虛引用可以跟蹤對象的回收時間,因此,也可以將一些資源釋放操作放置在虛引用中執行和記錄。
-
在JDK 1.2版之后提供了PhantomReference類來實現虛引用。
垃圾回收器相關
評估gc性能指標
- 吞吐量:運行用戶代碼的時間占總運行時間的比例
- (總運行時間:程序的運行時間+內存回收的時間)
- 垃圾收集開銷:吞吐量的補數,垃圾收集所用時間與總運行
- 暫停時間:執行垃圾收集時,程序的工作線程被暫停的時間
- 收集頻率:相對于應用程序的執行,收集操作發生的頻率。
- 內存占用::Java 堆區所占的內存大小。
- 快速:一個對象從誕生到被回收所經歷的時間。
紅=垃圾回收 藍=用戶線程執行。
吞吐量vs暫停時間現在標準:在最大吞吐量優先的情況下,降低停頓時間。
java常見垃圾收集器
- 串行回收器: serial、serial old
- 并行回收器: ParNew、Parallel scavenge、Parallel old
- 并發回收器: CMS、G1
垃圾收集器與分代間關系
- 新生代收集器: serial、ParNew、Parallel scavenge
- 老年代收集器: Serial old、Parallel old、CMS
- 整堆收集器:G1
垃圾收集器的組合關系
- 兩個收集器間有連線,表明它們可以搭配使用:
- Seria1/Serial old
- Seria1/CNS
- ParNew/Serial old
- ParNew/CMS
- Parallel Scavenge/Serial old
- Parallel Scavenge/Parallel old
- G1;
- 其中serial old作為CMS出現"concurrent Mode Failure"失敗的后備預案。
- (紅色虛線)由于維護和兼容性測試的成本,在JDK8時將serial+CMS、ParNew+Serial old這兩個組合聲明為廢棄(JEP 173),并在JDK 9中完全取消了這些組合的支持(JEP 214),即:移除。
- (綠色虛線)JDK 14中:棄用Parallel Scavenge和serial0ld Gc組合(JEP 366)
- (青色虛線)JDK 14中:刪除CMS垃圾回收器(JEP 363)
為什么要有很多收集器?
因為Java的使用場景很多,移動I端,服務器等。所以需針對不同的場景,提供不同的垃圾收集器,提高垃圾收集的性能。選擇的只是對具體應用最合適的收集器。
-XX:+PrintCommandLineFlags 查看命令行相關參數(包含使用的垃圾收集器)使用命令行指令:jinfo -flag 相關垃圾回收器參數 進程ID 如: jinfo -flag UseParallelGC java進程id(可通過jps命令查看) jinfo -flag UseParallelOldGC java進程id垃圾回收器
Serial:串行回收
-
serial收集器是最基本、歷史最悠久的垃圾收集器了。JDK1.3之前回收新生代唯一的選擇。
-
serial收集器作為HotSpot中client模式下的默認新生代垃圾收集器。serial 收集器采用復制算法、串行回收和"stop-the-world"機制的方式執行內存回收。
-
除了年輕代之外,serial收集器還提供用于執行老年代垃圾收集的serial old收集器。serial old收集器同樣也采用了串行回收和"stop the world"機制,只不過內存回收算法使用的是標記-壓縮算法。
- Serial old是運行在client模式下默認的老年代的垃圾回收器
- Serial old在server模式下主要有兩個用途
- 1-與新生代的Parallel scavenge配合使用
- 2-作為老年代CMS收集器的后備垃圾收集方案
-
這個收集器是一個單線程的收集器,但它的“單線程”的意義并不僅僅說明它只會使用一個CPu或一條收集線程去完成垃圾收集工作,更重要的是在它進行垃圾收集時,必須暫停其他所有的工作線程,直到它收集結束(Stop The world)。
-
優勢:簡單而高效(與其他收集器單線程比)
- 這種垃圾收集器大家了解,現在已經不用串行的了。而且在限定單核cpu才可以用。現在都不是單核的了。
- 對于交互較強的應用而言,這種垃圾收集器是不能接受的。一般在Javaweb應用程序中是不會采用串行垃圾收集器的。
ParNew:并行回收
- 如果說serial GC是年輕代中的單線程垃圾收集器,那么ParNew收集器則是serial收集器的多線程版本。
- Par是Parallel的縮寫,New:只能處理的是新生代
- ParNew收集器除了采用并行回收的方式執行內存回收外,兩款垃圾收集器之間幾乎沒有任何區別。ParNew收集器在年輕代中同樣也是采用復制算法、"Stop-the-world"機制。
- ParNew是很多JVM運行在server模式下新生代的默認垃圾收集器。
-
對于新生代,回收次數頻繁,使用并行方式高效。對于老年代,回收次數少,使用串行方式節省資源。(CPu并行需要切換線程,串行可以省去切換線程的資源)
-
ParNew收集器運行在多CPU的環境下,由于可以充分利用多CPU、多核心等物理硬件資源優勢,可以更快速地完成垃圾收集,提升程序的吞吐量。但是在單個CPU的環境下,ParNew收集器不比serial 收集器更高效。雖然serial收集器是基于串行回收,但是由于CPU不需要頻繁址做任務切換,因此可以有效避免多線程交互過程中產生的一些額外開銷。
-
因為除serial外,目前只有ParNew Gc能與CMS收集器配合工作
Parallel Scavenge:吞吐量優先
-
HotSpot的年輕代中除了擁有ParNew收集器是基于并行回收的以外,Parallel scavenge收集器同樣也采用了復制算法、并行回收和"stopthe world"機制。
-
那么Parallel收集器的出現是否多此一舉?
- 和ParNew收集器不同,Parallel Scavenge收集器的目標則是達到一個可控制的吞吐量(Throughput),它也被稱為吞吐量優先的垃圾收集器。
- 自適應調節策略也是Parallel scavenge與ParNew一個重要區別。
-
高吞吐量則可以高效率地利用CPU 時間,盡快完成程序的運算任務,主要適合在后臺運算而不需要太多交互的任務。因此,常見在服務器環境中使用。例如,那些執行批量處理、訂單處理、工資支付、科學計算的應用程序。
-
Parallel 收集器在JDK1.6時提供了用于執行老年代垃圾收集的Parallel old收集器,用來代替老年代的serial old收集器。Parallel old收集器采用了標記-壓縮算法,但同樣也是基于并行回收和"stop-the-world"機制。
-
在程序吞吐量優先的應用場景中,Parallel 收集器和Parallel old收集器的組合,在server模式下的內存回收性能很不錯。在Java8中,默認是此垃圾收集器。
| -XX:+UseParallelGC | 手動指定年輕代使用Parallel并行收集器執行內存回收任務。 |
| -XX:+UseParallelOldGC | 手動指定老年代都是使用并行回收收集器。分別適用于新生代和老年代。默認jdk8是開啟的。 上面兩個參數,默認開啟一個,另一個也會被開啟。(互相激活) |
| -XX:Paralle1GCThreads | 設置年輕代并行收集器的線程數。一般地,最好與CPu數量相等,以避免過多的線程數影響垃圾收集性能。 在默認情況下,當CPU數量小于8個,ParallelGcThreads 的值等于CPU數量。 當CPU數量大于8個,ParallelGCThreads的值等于:3+[5*CPU_Count]/8] |
| -XX:MaxGCPauseMillis | 設置垃圾收集器最大停頓時間(即STW的時間)。單位是毫秒。 為了盡可能地把停頓時間控制在MaxGCPauseMills以內,收集器在工作時會調整Java堆大小或者其他一些參數。 對于用戶來講,停頓時間越短體驗越好。但是在服務器端,我們注重高并發,整體的吞吐量。所以服務器端適合Parallel,進行控制。 該參數使用需謹慎。 |
| -XX:GCTimeRatio | 垃圾收集時間占總時間的比例(=1/(N+1))。用于衡量吞吐量的大小。 取值范圍(0,100)。默認值99,也就是垃圾回收時間不超過1號。與前一個-XX:MaxGCPauseMillis參數有一定矛盾性。暫停時間越長,Radio參數就容易超過設定的比例。 |
| -XX:+UseAdaptivesizePolicy | 設置Parallel Scavenge收集器具有自適應調節策略 在這種模式下,年輕代的大小、Eden和survivor的比例、晉升老年代的對象年齡等參數會被自動調整,已達到在堆大小、吞吐量和停頓時間之間的平衡點。 在手動調優比較困難的場合,可以直接使用這種自適應的方式,僅指定虛擬機的最大堆、目標的吞吐量(GCTimeRatio)和停頓時間(MaxGCPauseMills) ,讓虛擬機自己完成調優工作。 |
CMS(concurrent-Mark-Sweep):低延遲
-
在JDK 1.5時期,HotSpot推出了一款在強交互應用中幾乎可認為有劃時代意義的垃圾收集器:CMS (concurrent-Mark-Sweep)收集器,這款收集器是HotSpot虛擬機中第一款真正意義上的并發收集器,它第一次實現了讓垃圾收集線程與用戶線程同時工作。
-
CMS收集器的關注點是盡可能縮短垃圾收集時用戶線程的停頓時間。停頓時間越短(低延遲)就越適合與用戶交互的程序,良好的響應速度能提升用)體驗。
- 目前很大一部分的Java應用集中在互聯網站或者B/s系統的服務端上,這類應用尤其重視服務的響應速度,希望系統停頓時間最短,以給用戶帶來較好的體驗。CMs收集器就非常符合這類應用的需求。
-
CMS的垃圾收集算法采用標記-清除算法,并且也會”Stop-the-world"
-
不幸的是,CMS作為老年代的收集器,卻無法與JDK 1.4.0中已經存在的新生代收集器Parallel scavenge配合工作,所以在JDK 1.5中使用CMS來收集老年代的時候,新生代只能選擇ParNew或者serial收集器中的一個。
-
在G1出現之前,CMS使用還是非常廣泛的。一直到今天,仍然有很多系統使用CMS GC。
工作原理
- 初始標記(Initial-Mark)階段:在這個階段中,程序中所有的工作線程都將會因為“Stop-the-world”機制而出現短暫的暫停,這個階段的主要任務僅僅只是標記出GC Roots能直接關聯到的對象。一旦標記完成之后就會恢復之前被暫停的所有應用線程。由于直接關聯對象比較小,所以這里的速度非常快。
- 并發標記(Concurrent-Mark)階段:從GC Roots的直接關聯對象開始遍歷整個對象圖的過程,這個過程耗時較長但是不需要停頓用戶線程,可以與垃圾收集線程一起并發運行。
CMS弊端
- 1)會產生內存碎片:導致并發清除后,用戶線程可用的空間不足。在無法分配大對象的情況下,不得不提前觸發Full GC。
- 2)CMS收集器對cPu資源非常敏感。在并發階段,它雖然不會導致用戶停頓,但是會因為占用了一部分線程而導致應用程序變慢,總吞吐量會降低。
- 3)CMS收集器無法處理浮動垃圾。可能出現“Concurrent Mode Failure"失敗而導致另一次 Full GC 的產生。在并發標記階段由于程序的工作線程和垃圾收集線程是同時運行或者交叉運行的,那么在并發標記階段如果產生新的垃圾對象,CMS將無法對這些垃圾對象進行標記,最終會導致這些新產生的垃圾對象沒有被及時回收,從而只能在下一次執行Gc時釋放這些之前未被回收的內存空間。
| -XX:+UseConcMarkSweepec | 手動指定使用cMS收集器執行內存回收任務。 開啟該參數后會自動將-XX:+UseParNewGc打開。即: ParNew(Young區用)+CMs(old區用)+serial old的組合。 |
| -XX:CMS1nitiatingOccupanyFraction | 設置堆內存使用率的閾值,一旦達到該閾值,便開始進行回收。 JDK5及以前版本的默認值為68,即當老年代的空間使用率達到68%時,會執行一次CMS 回收。JDK6及以上版本默認值為92% 如果內存增長緩慢,則可以設置一個稍大的值,大的閾值可以有效降低cMs的觸發頻率,減少老年代回收的次數可以較為明顯地改善應用程序性能。反之,如果應用程序內存使用率增長很快,則應該降低這個閾值,以避免頻繁觸發老年代串行收集器。因此通過該選項便可以有效降低Full GC的執行次數。 |
| -XX:+UseCMSCompactAtFullCollection | 用于指定在執行完FullGC后對內存空間進行壓縮整理,以此避免內存碎片的產生。不過由于內存壓縮整理過程無法并發執行,所帶來的問題就是停頓時間變得更長了。 |
| -XX:CMSFulLGCsBeforeCompaction | 設置在執行多少次Full GC后對內存空間進行壓縮整理 |
| -XX:ParallelCMSThreads | 設置cMs的線程數量。CMs 默認啟動的線程數是(ParallelGCThreads+3)/4, ParallelGCThreads是年輕代并行收集器的線程數。當CPU資源比較緊張時,受到CMS收集器線程的影響,應用程序的性能在垃圾回收階段可能會非常糟糕。 |
-
DK9新特性:CNS被標記為Deprecate了(EP291)
-
JDK14新特性:刪除CMS垃圾回收器(JEP363)
小結:Hotspot有這么多的垃圾回收器,那么如果有人問,Serial GC、Parallel Gc、Concurrent Mark Sweep cc這三個cc有什么不同呢?請記住以下口令:
- 如果你想要最小化地使用內存和并行開銷,請選serial GC
- 如果你想要最大化應用程序的吞吐量,請選Parallel Gc
- 如果你想要最小化cC的中斷或停頓時間,請選CMS Gc。
G1(Garbage First):區域分代化
-
官方給G1設定的目標是在延遲可控的情況下獲得盡可能高的吞吐量,所以才擔當起“全功能收集器”的重任與期望。
-
G1是一個并行回收器,它把堆內存分割為很多不相關的區域(Region)(物理不連續的)。使用不同的Region來表示Eden、幸存者0區,幸存者1區,老年代等。G1 Gc有計劃地避免在整個Java 堆中進行全區域的垃圾收集。G1 跟蹤各個Region里面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在后臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region。由于這種方式的側重點在于回收垃圾最大量的區間(Region),所以我們給c1一個名字:垃圾優先(Garbage First) 。
-
G1 (Garbage-First)是一款面向服務端應用的垃圾收集器,主要針對配備多核cPU及大容量內存的機器,以極高概率滿足cc停頓時間的同時,還兼具高吞吐量的性能特征。在JDK1.7版本正式啟用,移除了Experimental的標識,是JDK 9以后的默認垃圾回收器,取代了cMS回收器以及Parallel + Parallel old組合。被oracle官方稱為“全功能的垃圾收集器”。
并行與并發
- 并行性:G1在回收期間,可以有多個Gc線程同時工作,有效利用多核計算能力。此時用戶線程sTw;
- 并發性:G1擁有與應用程序交替執行的能力,部分工作可以和應用程序同時執行,因此,一般來說,不會在整個回收階段發生完全阻塞應用程序的情況;
分代收集
- 從分代上看,G1依然屬于分代型垃圾回收器,它會區分年輕代和老年代,年輕代依然有Eden區和survivor區,但從堆的結構上看,它不要求整個Eden區、年輕代或者老年代都是連續的,也不再堅持固定大小和固定數量。
- 將堆空間分為若干個區域(Region),這些區域中包含了邏輯上的年輕代和老年代。
- 和之前的各類回收器不同,它同時兼顧年輕代和老年代。對比其他回收器,或者工作在年輕代,或者工作在老年代;
空間整合
- CMS:“標記-清除”算法、內存碎片、若干次cc后進行一次碎片整理
- G1將內存劃分為一個個的region。內存的回收是以region作為基本單位的。Region之間是復制算法,但整體上實際可看作是標記-壓縮(Mark-Compact)算法,兩種算法都可以避免內存碎片。這種特性有利于程序長時間運行,分配大對象時不會因為無法找到連續內存空間而提前觸發下一次Gc。尤其是當Java堆非常大的時候,G1的優勢更加明顯。
可預測的停頓時間模型(即:軟實時soft real-time)
這是 G1 相對于CMS 的另一大優勢,c1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內,消耗在垃圾收集上的時間不得超過N毫秒。|
- 由于分區的原因,G1可以只選取部分區域進行內存回收,這樣縮小了回收的范圍,因此對于全局停頓情況的發生也能得到較好的控制。
- G1跟蹤各個 Region里面的垃圾堆積的價值大小(回收所獲得的空間大小以及回收所需時間的經驗值),在后臺維護一個優先列表,每次根據允許的收集時間,優先回收價值最大的Region。保證了 G1收集器在有限的時間內可以獲取盡可能高的收集效率。
- 相比于cMs cc,G1未必能做到cMs在最好情況下的延時停頓,但是最差情況要好很多。
缺點
- 相較于CMS,G1還不具備全方位、壓倒性優勢。比如在用戶程序運行過程中,G1無論是為了垃圾收集產生的內存占用(Footprint)還是程序運行時的額外執行負載(overload)都要比CMS要高。
- 從經驗上來說,在小內存應用上CNS的表現大概率會優于G1,而G1在大內存應用上則發揮其優勢。平衡點在6-8GB之間。
| -XX:+UseG1cc | 手動指定使用G1收集器執行內存回收任務。 |
| -XX:G1HeapRegionsize | 設置每個Region的大小。值是2的冪,范圍是1MB到32MB之間,目標是根據最小的Java堆大小劃分出約2048個區域。默認是堆內存的1/2000。 |
| -XX:MaxGCPauseMi1lis | 設置期望達到的最大Gc停頓時間指標(JVM會盡力實現,但不保證達到)。默認值是200ms |
| -XX:Paralle1GcThread | 設置sTw工作線程數的值。最多設置為8 |
| -XX:ConcGCThreads | 設置并發標記的線程數。將n設置為并行垃圾回收線程數(ParallelGCThreads)的1/4左右。 |
| -XX: InitiatingHeapOccupancyPercent | 設置觸發并發Gc周期的Java堆占用率閾值。超過此值,就觸發Gc。默認值是45。 |
分區region:化整為零
- 使用c1收集器時,它將整個Java堆劃分成約2048個大小相同的獨立Region塊,每個Region塊大小根據堆空間的實際大小而定,整體被控制在1MB到32MB之間,且為2的N次冪,即1MB,2MB,4MB,8MB,16MB,32MB。可以通過-XX:G1HeapRegionsize設定。所有的Region大小相同,且在JVM生命周期內不會被改變。
GC發展階段:Serial => Paralle1(并行)=> cMS(并發)=> G1 =>zGC
GC其他
怎么選擇垃圾收集器?
- 優先調整堆的大小讓JVM自適應完成。
- 如果內存小于100M,使用串行收集器
- 如果是單核、單機程序,并且沒有停頓時間的要求,串行收集器
- 如果是多CPU、需要高吞吐量、允許停頓時間超過1秒,選擇并行或者JVM自己選擇
- 如果是多CPU、追求低停頓時間,需快速響應(比如延遲不能超過1秒,如聯網應用),使用并發收集器官方推薦G1,性能高。
內存分配與垃圾回收的參數列表
| -XX:+Printcc | 輸出Gc日志。類似:-verbose:gc(打開GC日志) |
| -XX:+PrintGCDetails | 輸出GC的詳細日志 |
| -XX:+PrintGCrimeStamps | 輸出Gc的時間戳(以基準時間的形式) |
| -XX:+PrintGCDatestamps | 輸出Gc的時間戳(以日期的形式,如2013-05-04T21:53:59.234+0800) |
| -XX:+PrintHeapAtGC | 在進行Gc的前后打印出堆的信息 |
| -Xloggc:…/ logs/gc. log | 日志文件的輸出路徑 |
gc日志分析
- MinorGc
- Full Gc
- 常用的日志分析工具有:Gcviewer、GCEasy、GCHisto、GCLogviewer.Hpjmeter、garbagecat等。
總結
以上是生活随笔為你收集整理的【jvm】8-垃圾回收的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 全球37%手机或因芯片漏洞遭攻击、黑客利
- 下一篇: dlp监控开除员工_说一说DLP的那些事