1. 程式人生 > >JVM調優(3)之垃圾回收

JVM調優(3)之垃圾回收

從這篇開始我們開始探討一些jvm調優的問題。在jvm調優中一個離不開的重點是垃圾回收,當垃圾回收成為系統達到更高併發量的瓶頸時,我們就需要對jvm中如果進行“自動化”垃圾回收技術實施必要的監控和調節。

對於調優之前,我們必須要了解其執行原理,java 的垃圾收集Garbage Collection 通常被稱為“GC”,它誕生於1960年 MIT 的 Lisp 語言,經過半個多世紀,目前已經十分成熟了。因此本篇主要從這三個方面來了解:

  1. 哪些物件需要被回收?
  2. 什麼時候回收?
  3. 如何回收?

一、回收哪些資料

java虛擬機器在執行java程式的過程中會把它所管理的記憶體劃分為若干個不同是資料區域,這些區域有各自各自的用途。主要包含以下幾個部分組成:
這裡寫圖片描述


這裡寫圖片描述
這裡寫圖片描述

1、程式計數器
程式計數器佔用的記憶體空間我們可以忽略不計,它是每個執行緒所執行的位元組碼的行號指示器。

2、虛擬機器棧
java的虛擬機器棧是執行緒私有的,生命週期和執行緒相同。它描述的是方法執行的記憶體模型。同時用於儲存區域性變數、運算元棧、動態連結、方法出口等。
3、本地方法棧,類似虛擬機器棧,它呼叫的是是native方法。
4、堆是jvm中管理記憶體中最大一塊。它是被共享,存放物件例項。也被稱為“gc堆”。垃圾回收的主要管理區域
5、方法區也是共享的記憶體區域。它主要儲存已被虛擬機器載入的類資訊、常量、靜態變數、即時編譯器(jit)編譯後的程式碼資料。

以上就是jvm在執行時期主要的記憶體組成,我們看到常見的記憶體使用不但存在於堆中,還會存在於其他區域,雖然堆的管理對程式的管理至關重要,但我們不能只侷限於這一個區域,特別是當出現記憶體洩露的時候,我們除了要排查堆記憶體的情況,還得考慮虛擬機器棧的以及方法區域的情況。知道了要對誰以及那些區域進行記憶體管理,我還需要知道什麼時候對這些區域進行垃圾回收。

二、垃圾回收演算法

在垃圾回收之前,我們必須確定的一件事就是物件是否存活?這就牽扯到了判斷物件是否存活的演算法了。

引用計數(Reference Counting)
比較古老的回收演算法。原理是此物件有一個引用,即增加一個計數,刪除一個引用則減少一個計數。垃圾回收時,只用收集計數為0的物件。此演算法最致命的是無法處理迴圈引用的問題。。

優點:實現簡單,判定效率高效,被actionscript3和python中廣泛應用。
缺點:無法解決物件之間的相互引用問題。java沒有采納

根搜尋演算法
這裡寫圖片描述
根搜尋演算法是從離散數學中的圖論引入的,程式把所有的引用關係看作一張圖,從一個節點GC ROOT開始,尋找對應的引用節點,找到這個節點以後,繼續尋找這個節點的引用節點,當所有的引用節點尋找完畢之後,剩餘的節點則被認為是沒有被引用到的節點,即無用的節點。

java中可作為GC Root的物件有
1.虛擬機器棧中引用的物件(本地變量表)
2.方法區中靜態屬性引用的物件
3. 方法區中常量引用的物件
4.本地方法棧中引用的物件(Native物件)

tracing演算法
這裡寫圖片描述

標記-清除演算法
標記-清除演算法採用從根集合進行掃描,對存活的物件物件標記,標記完畢後,再掃描整個空間中未被標記的物件,進行回收,如上圖所示。標記-清除演算法不需要進行物件的移動,並且僅對不存活的物件進行處理,在存活物件比較多的情況下極為高效,但由於標記-清除演算法直接回收不存活的物件,因此會造成記憶體碎片。

複製(Copying):
這裡寫圖片描述
該演算法的提出是為了克服控制代碼的開銷和解決堆碎片的垃圾回收。它開始時把堆分成 一個物件 面和多個空閒面, 程式從物件面為物件分配空間,當物件滿了,基於copying演算法的垃圾 收集就從根集中掃描活動物件,並將每個 活動物件複製到空閒面(使得活動物件所佔的記憶體之間沒有空閒洞),這樣空閒面變成了物件面,原來的物件面變成了空閒面,程式會在新的物件面中分配記憶體。一種典型的基於coping演算法的垃圾回收是stop-and-copy演算法,它將堆分成物件面和空閒區域面,在物件面與空閒區域面的切換過程中,程式暫停執行。

標記-整理(Mark-Compact):
這裡寫圖片描述
此演算法結合了“標記-清除”和“複製”兩個演算法的優點。也是分兩階段,第一階段從根節點開始標記所有被引用物件,第二階段遍歷整個堆,把清除未標記物件並且把存活物件“壓縮”到堆的其中一塊,按順序排放。此演算法避免了“標記-清除”的碎片問題,同時也避免了“複製”演算法的空間問題。

generation演算法(Generational Collector)
這裡寫圖片描述

在java中,可以作為GCRoot的物件包括以下幾種:

  1. 虛擬機器棧中引用的物件。
  2. 方法區中靜態屬性引用的物件。
  3. 方法區中常量引用的物件。
  4. 本地方法中JNI引用的物件。

基於以上,我們可以知道,如果當前物件到GCRoot中不可達時候,即會滿足被垃圾回收的可能。
那麼是不是這些物件就非死不可,也不一定,此時只能宣判它們存在於一種“緩刑”的階段,要真正的宣告一個物件死亡。
至少要經歷兩次標記:
第一次:物件可達性分析之後,發現沒有與GCRoots相連線,此時會被第一次標記並篩選。
第二次:物件沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機器呼叫過,此時會被認定為沒必要執行。

三、收集器

上述的兩點講解之後,我們大概明白了,哪些物件會被回收,以及回收的依據是什麼,但回收的這個工作實現起來並不簡單,首先它需要掃描所有的物件,鑑別誰能夠被回收,其次在掃描期間需要 ”stop the world“ 物件能被凍結,不然你剛掃描,他的引用資訊有變化,你就等於白做了。

GC(垃圾收集器)

新生代收集器使用的收集器:Serial、PraNew、Parallel Scavenge
老年代收集器使用的收集器:Serial Old、Parallel Old、CMS
這裡寫圖片描述

Serial收集器(複製演算法)

新生代單執行緒收集器,標記和清理都是單執行緒,優點是簡單高效。

*Serial Old收集器(標記-整理演算法)

老年代單執行緒收集器,Serial收集器的老年代版本。

ParNew收集器(停止-複製演算法) 

新生代收集器,可以認為是Serial收集器的多執行緒版本,在多核CPU環境下有著比Serial更好的表現。

Parallel Scavenge收集器(停止-複製演算法)

並行收集器,追求高吞吐量,高效利用CPU。吞吐量一般為99%, 吞吐量= 使用者執行緒時間/(使用者執行緒時間+GC執行緒時間)。適合後臺應用等對互動相應要求不高的場景。

Parallel Old收集器(停止-複製演算法)

Parallel Scavenge收集器的老年代版本,並行收集器,吞吐量優先

CMS(Concurrent Mark Sweep)收集器(標記-清理演算法)

高併發、低停頓,追求最短GC回收停頓時間,cpu佔用比較高,響應時間快,停頓時間短,多核cpu 追求高響應時間的選擇

四、優缺點

垃圾收集器是記憶體回收的具體實現,不同的廠商提供的垃圾收集器有很大的差別,一般的垃圾收集器都會作用於不同的分代,需要搭配使用。以下是各種垃圾收集器的組合方式:
這裡寫圖片描述

組合的優缺點
這裡寫圖片描述

五、Java有了GC同樣會出現記憶體洩露問題

1.靜態集合類像HashMap、Vector等的使用最容易出現記憶體洩露,這些靜態變數的生命週期和應用程式一致,所有的物件Object也不能被釋放,因為他們也將一直被Vector等應用著。

Static Vector v = new Vector(); 
for (int i = 1; i<100; i++) 
{ 
    Object o = new Object(); 
    v.add(o); 
    o = null; 
}

在這個例子中,程式碼棧中存在Vector 物件的引用 v 和 Object 物件的引用 o 。在 For 迴圈中,我們不斷的生成新的物件,然後將其新增到 Vector 物件中,之後將 o 引用置空。問題是當 o 引用被置空後,如果發生 GC,我們建立的 Object 物件是否能夠被 GC 回收呢?答案是否定的。因為, GC 在跟蹤程式碼棧中的引用時,會發現 v 引用,而繼續往下跟蹤,就會發現 v 引用指向的記憶體空間中又存在指向 Object 物件的引用。也就是說盡管o 引用已經被置空,但是 Object 物件仍然存在其他的引用,是可以被訪問到的,所以 GC 無法將其釋放掉。如果在此迴圈之後, Object 物件對程式已經沒有任何作用,那麼我們就認為此 Java 程式發生了記憶體洩漏。

2.各種連線,資料庫連線,網路連線,IO連線等沒有顯示呼叫close關閉,不被GC回收導致記憶體洩露。

3.監聽器的使用,在釋放物件的同時沒有相應刪除監聽器的時候也可能導致記憶體洩露。

參考文件:
深入理解 Java 垃圾回收機制
jvm優化—— 圖解垃圾回收
調優總結(三)-基本垃圾回收演算法