前言
該讀書筆記用於記錄在學習《深入理解Java虛擬機器——JVM高階特性與最佳實踐》一書中的一些重要知識點,對其中的部分內容進行歸納,或者是對其中不明白的地方做一些註釋。主要是方便之後進行復習。
目錄
垃圾收集器與記憶體分配策略
哪些記憶體需要垃圾回收
在上一節中有提到在執行時資料區域包括:堆、虛擬機器棧、本地方法棧、程式計數器、方法區(JDK1.7及之前)、元空間(JDK1.8及之後)。在這些區域中,程式計數器佔用記憶體極小,可以忽略;棧區域在編譯期就可以確定下來,並且其宣告週期隨執行緒保持一致,也不用管;而Java堆和方法區、元空間中介面的不同實現類需要的記憶體不同,方法的不同實現需要的記憶體也不同,而且這些所需的記憶體需要在執行時才能確定,所以垃圾回收關注的主要內容就是這些區域。
物件是否不再使用
引用計數法
給物件新增一個引用計數器,每當有一個地方引用它時,計數器加一;引用失效的時候,計數器就減一;在任何時候只要計數器為0就代表該物件就是不會再被使用的。
該方法的優點:
- 實現較為簡單
- 判定效率很高,基本沒有其他額外的操作
缺點:
很難解決物件之間相互迴圈引用的問題。即兩個物件相互持有對方的引用,除此之外再沒有別的地方使用這兩個物件,但是因為相互引用導致計數器不可能為0,所以無法被回收
可達性分析演算法
演算法描述
通過選擇一些滿足一定條件的物件作為節點,從這些節點開始往下搜尋,搜尋經過的路徑被稱為引用鏈(有直接或間接引用關係的物件都在引用鏈上),這些物件被成為”GC Roots”,當一個物件達到GC Roots沒有任何引用鏈時則判定該物件不可用,即使該物件仍舊被其他物件引用,只要其與GC Roots沒有關係既是不可用的。
可作為GC Roots的物件
- 虛擬機器棧中引用的物件。
- 方法區中常量引用的物件。
- 方法區中類靜態屬性引用的物件。
- 本地方法區中JNI引用的物件。
簡單來說包括以下幾種型別:
- Class – 由系統類載入器(system class loader)載入的物件,這些類是不能夠被回收的
- Thread – 活著的執行緒
- Stack Local – Java方法的local變數或引數
- JNI Local – JNI方法的local變數或引數
- JNI Global – 全域性JNI引用
- Monitor Used – 用於同步的監控物件
Java中的引用
在最初的Java中,引用僅僅是指一個物件的資料中儲存的值是另外一塊記憶體的起始地址。在JDK1.2之後將引用分為多種:
-
強引用:強引用是類似於
User user = new User()
,是在程式碼中最常用的一種方式。只要強引用存在,那麼垃圾回收器就永遠不會回收掉被引用的物件。 -
軟引用:軟引用用於描述一些有用但是並不是一定需要的物件,對於軟引用的物件,當記憶體將要發生溢位時,會將這些物件列入回收範圍中進行一次回收,如果將軟引用的物件回收後記憶體還是不足才會丟擲記憶體溢位異常。在JDK中使用
SoftReference
類實現軟引用。SoftReference<Object> softReference = new SoftReference<Object>(new Object());
-
弱引用:弱引用用於描述非必須的物件,弱引用物件在下一次垃圾回收時一定會被回收,無論當前記憶體是否足夠。在JDK中使用
WeakReference
定義弱引用。 -
虛引用:一個物件是否存在虛引用對其生存時間不會有任何關係,只是在這個物件唄收集器回收時收到一個系統通知。在JDK中使用
PhantomReference
來實現虛引用。
物件的自救
實際上,在可達性演算法中即使是不可達的物件也並非一定會被回收的,判斷其是否會被回收還需要走以下流程:
-
如果物件在可達性分析中被判定沒有與GC Roots相連線的引用鏈那麼改物件將會被標記,然後進行一次篩選。
-
篩選的條件是判斷該物件是否有必要執行finalize()方法。是否有必要執行finalize()方法的條件是當物件沒有覆蓋finalize()方法或者該物件的finalize()方法已經被虛擬機器呼叫過,這兩種情況都會被判定為沒有必要執行。
-
如果被判定為有必要執行finalize方法,則會將其放在一個佇列中,稍後執行。在finalize()方法中是物件逃脫被回收的最後機會,只要重新與引用鏈中的任何一個物件建立關係即可。
public class FinalizeEscape {
private static FinalizeEscape escape = null;
public static void main(String[] args) throws InterruptedException {
escape = new FinalizeEscape();
//模擬物件使用後斷開引用鏈
escape = null;
//物件自救
System.gc();
Thread.sleep(500);
if(escape != null){
System.out.println("物件沒有被清除!");
}else {
System.out.println("物件已經被清除!");
}
//模擬第二次逃脫gc
escape = null;
System.gc();
Thread.sleep(500);
if(escape != null){
System.out.println("物件沒有被清除!");
}else {
System.out.println("物件已經被清除!");
}
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize execute!");
escape = this;
}
}
執行結果:
finalize execute!
物件沒有被清除!
物件已經被清除!
複製程式碼
在對同一物件進行兩次模擬逃脫gc,第一次成功第二次失敗,是因為一個物件的finalize()方法只會被呼叫一次。
方法區回收
在方法區的回收主要包括兩個方面:
- 廢棄常量
- 無用的類
廢棄常量
廢棄常量是指在常量池中存在一個值,假設為一個字面量,但是在當前系統中沒有任何的一個物件引用了該字面量。那麼久認為該字面量是廢棄的,在下一次垃圾回收的時候將其進行回收。同理常量池中的其他類、介面、方法、欄位等的符號引用的回收也是類似。
無用的類
要判定一個類是否可以被回收需要滿足以下幾個條件:
- 該類所有的例項都已經被回收
- 載入該類的ClassLoader已經被回收
- 該類對應的位元組碼物件Class沒有在任何地方被引用,無法再任何地方通過反射訪問到該類的方法
當一個類滿足以上條件後就允許被回收,但不是一定會被回收。是否對類進行回收再HotSpot虛擬機器中提供了-Xnoclassgc
引數進行控制。也可以使用-verbose:class,-XX:+TraceClassLoading,-XX:+TraceClassUnloading
引數檢視類載入和解除安裝資訊。
在使用反射、動態代理、動態生成jsp和OSGI等頻繁自定義ClassLoader的場景都需要虛擬機器具備解除安裝類的功能,保證永久代不會溢位。
需要注意的是在JDK1.7時HotSpot就已經將執行時常量池遷移到堆中,在JDK1.8中更是直接移除了方法區,所以上面的介紹需要對應到具體的版本,並不是指著一定是在方法區完成。雖然區域發生變化但是回收的原則基本還是這樣。
元空間回收
JDK1.8開始把類的後設資料放到本地堆記憶體(native heap)中,如果Metaspace的空間佔用達到了設定的最大值,那麼就會觸發GC來收集死亡物件和進行類解除安裝,這一塊的回收要求較高,上文中有簡單說過。
有關元空間的JVM引數:
- -XX:MetaspaceSize :是分配給類後設資料空間(位元組)的初始大小。該值設定的過大會延長垃圾回收時間。垃圾回收過後,引起下一次垃圾回收的類後設資料空間的大小可能會變大。
- -XX:MaxMetaspaceSize :是分配給類後設資料空間的最大值,超過此值就會觸發Full GC,此值預設沒有限制,但應取決於系統記憶體的大小。JVM會動態地改變此值。
- -XX:MinMetaspaceFreeRatio/-XX:MaxMetaspaceFreeRatio :表示一次GC以後,為了避免增加後設資料空間的大小,空閒的類後設資料的容量的最小/最大比例,不夠就會觸發垃圾回收。
垃圾收集演算法
標記-清除演算法
標記-清除演算法的基本內容就同其名字一樣,存在著標記和清除兩個階段:首先查詢與GC Roots無任何關聯的物件,標記處所需回收的物件(如何標記在記憶體回收中已經介紹了,通過判斷是否有必要或已經執行了finalize()方法),在標記完成之後再統一清除。
標記過程:虛擬機器從作為GC Roots的根節點出發進行搜尋,對可被訪問到的物件做一個標記,其他未被標記的物件就是需要被回收的。效率低是因為目前來說專案中的物件極多,單單是進行遍歷就需要耗費較長的時間。
好處:實現簡單,標記-清除演算法流程十分簡單,實現也沒有很複雜的地方。
缺點:
1.效率較低:因為標記和清除的過程效率都不高
2.浪費記憶體空間:在清除標記的物件後造成了記憶體中大量不連續的空間,一旦有大的物件進入可能會因為沒有合適的存放的地方而造成再一次的GC。
複製演算法(多用於新生代)
複製演算法的基本內容是要求虛擬機器並不將所有的記憶體空間用來存放物件。複製演算法將記憶體分為兩塊,每一次都只是使用其中的一塊,當觸發GC時,將存放物件的那一塊記憶體上還存活的物件複製到另一塊上去,然後將之前的記憶體塊全部清除。
優點:實現簡單,而且因為在將存活物件轉移時順序記憶體存放不用考慮記憶體碎片的問題,效率較高。
缺點:
1.始終有一部分記憶體沒有得到使用,造成空間浪費。要保證存活的物件能夠完全複製,那麼就要求兩塊記憶體大小一致(50%),因為可能存在沒有任何物件死亡的極端情況,但是這樣將會極其浪費,而如果不這樣分配,就必須引入其他機制保證物件能夠被完整的複製。
標記-整理演算法
標記整理演算法的標記階段同標記-清除演算法一致,不過標記後並不立即清除,而是將存活(不會被GC)的物件移向記憶體的一端,將存活的物件全部移動後將邊界外的清除掉。
優點:解決了記憶體碎片的問題
缺點:標記階段效率本身較低,還多加了一個整理階段,還是在於總體效率較低
分代收集演算法
分代收集演算法實際上並不是一個新的實現方式,只是將虛擬機器分成幾塊,每一塊根據它的實際作用來選擇適合的演算法,這些演算法可以是標記-清除,複製演算法等等。
基於分代的收集思想,將堆記憶體分為以下幾個部分:
將堆記憶體分為新生代(Young)和老年代(Old),新生代又分為Eden區、from區和to區,預設Eden:from:to=8:1:1。一般情況下,新建立的物件都會被分配到Eden區(一些大物件可能會直接放到老年代),具體的記憶體分配在後面記錄。
HotSpot中的演算法實現
列舉根節點
在可達性分析中,可作為GC Roots的節點主要是全域性性的引用與執行上下文,如果要逐個檢查引用,必然消耗時間。
另外可達性分析對執行時間的敏感還體現在GC停頓上,因為這項分析工作必須在一個能確保一致性的時間間隔中進行,這裡的“一致性”的意思是指整個分析期間整個系統執行系統看起來就像被暫停在某個時間點,不可以出現分析過程中物件引用關係還在不斷變化的情況,也就是在分析過程中使用者執行緒還在工作。這點是導致GC進行時必須暫停所有Java執行執行緒的其中一個重要原因。
但是目前主流的Java虛擬機器都是準確式GC(準確式GC是指就是讓JVM知道記憶體中某個位置資料的型別是什麼),所以在執行系統停頓下來之後,並不需要一個不漏的檢查執行上下文和全域性的引用位置,虛擬機器是有辦法得知哪些地方存放的是物件的引用。在HotSpot的實現中,是使用一組OopMap的資料結構來達到這個目的的。
安全點
在OopMap的協助下,HotSpot可以快速且準確的完成GC Roots的列舉,但可能導致引用關係變化的指令非常多,如果為每一條指令都生成OopMap,那將會需要大量的額外空間,這樣GC的空間成本會變的很高。
實際上,HotSpot也的確沒有為每條指令生成OopMap,只是在特定的位置記錄了這些資訊,這些位置被稱為安全點(SafePoint)。SafePoint的選定既不能太少,以致讓GC等待時間太久,也不能設定的太頻繁以至於增大執行時負荷。所以安全點的設定是以讓程式“是否具有讓程式長時間執行的特徵”為標準選定的。“長時間執行”最明顯的特徵就是指令序列的複用,例如方法呼叫、迴圈跳轉、異常跳轉等,所以具有這些功能的指令才會產生SafePoint。
對於SafePoint,另一個問題是如何在GC發生時讓所有執行緒都跑到安全點在停頓下來。這裡有兩種方案:搶先式中斷和主動式中斷。搶先式中斷不需要執行緒程式碼主動配合,當GC發生時,首先把所有執行緒中斷,如果發現執行緒中斷的地方不在安全點上,就恢復執行緒,讓他跑到安全點上。現在幾乎沒有虛擬機器實現採用搶先式中斷來暫停執行緒來響應GC。
而主動式中斷的思想是當GC需要中斷執行緒的時候,不直接對執行緒操作,僅僅簡單的設定一個標誌,各個執行緒執行時主動去輪詢這個標誌,發現中斷標誌為真時就自己中斷掛起,輪詢標誌的地方和安全點是重合的另外再加上建立物件需要分配的記憶體的地方。
安全區域
使用安全點似乎已經完美解決了如何進入GC的問題,但實際情況卻並不一定,安全點機制保證了程式執行時,在不太長的時間內就會進入到可進入的GC的安全點。但是程式如果不執行呢?所謂的程式不執行就是沒有分配cpu時間,典型的例子就是執行緒處於sleep狀態或者blocked狀態,這時候執行緒無法響應jvm中斷請求,走到安全的地方中斷掛起,jvm顯然不太可能等待執行緒重新分配cpu時間,對於這種情況,我們使用安全區域來解決。
安全區域是指在一段程式碼片段之中,你用關係不會發生變化。在這個區域的任何地方開始GC都是安全的,我們可以把安全區域看做是擴充套件了的安全點。
當執行緒執行到安全區域中的程式碼時,首先標識自己已經進入了安全區,那樣當在這段時間裡,JVM要發起GC時,就不用管標識自己為安全區域狀態的執行緒了。當執行緒要離開安全區域時,他要檢查系統是否完成了根節點列舉,如果完成了,那執行緒就繼續執行,否則他就必須等待,直到收到可以安全離開安全區域的訊號為止。
垃圾收集器
Serial收集器
Serial是一個單執行緒的收集器,這表示其Serial只會使用一個CPU或者是一條收集執行緒進行垃圾回收的工作,同時需要注意的是它在進行回收工作是會停掉所有的其他工作執行緒(Stop the World),知道它的回收工作結束。
Serial雖然存在上面的問題,但是這並不表示它是一個無用的收集器,反而到目前為止Serial收集器在Client模式下被用在新生代的收集(64位虛擬機器預設支援Server模式,並且無法切換;32位虛擬機器可在Client和Server之間切換。正常情況下,Server模式啟動較慢,但啟動後效能遠高於Client模式)。但是實際上使用也不多了。。。
Serial收集器的優點在於:在單CPU環境中,由於Serial由於沒有執行緒的開銷,專心做垃圾回收自然能獲得極高的回收效率。
ParNew收集器
ParNew實際上就是一個多執行緒版的Serial收集器,除了多執行緒進行垃圾回收外其他都和Serial基本一致。
ParNew在很多執行於Server模式下的虛擬機器中被用於新生代的首選。最大的原因在於目前為止只有其能CMS(Concurrent Mark Sweep)收集器配合使用。
併發與並行
- 併發:簡單來講是指一個處理器在同一時間間隔處理多個任務。放到垃圾回收這裡指垃圾回收執行緒和使用者工作執行緒同時進行工作。
- 並行:並行是指多個處理器在同一時刻處理多個任務。放到垃圾回收這裡指多個垃圾回收執行緒同時工作,但是使用者工作執行緒處於等待狀態。
Parallel Scavenge收集器
Parallel Scavenge是一個新生代的收集器,同時它是一個並行的多執行緒的收集器,其使用複製演算法。Parallel Scavenge的目標是達到一個可控制的吞吐量(throughput)。
吞吐量:CPU用於執行使用者程式碼的時間和CPU總共執行時間的比值
吞吐量越高表示停頓時間短,程式響應速度快,CPU利用率越高。
Parallel Scavenge提供了幾個用於精確控制吞吐量的引數:
- -XX:MaxGCPauseMillis:控制最大垃圾收集停頓時間,該引數的值執行為一個大於0的毫秒數,虛擬機器會盡量將垃圾回收花費的時間控制在設定的值之下。但是並不代表將每次垃圾回收的時間減小就一定是有利的,因為回收時間的降低是以降低吞吐量和新生代空間為代價的。為了保證每次垃圾回收的時間減小,就需要降低每一次垃圾回收的區域,所以需要減小新生代的大小。但在降低新生代大小的同時也增加了垃圾回收的次數。所以在設定該值是不能盲目的追求小的回收時間,而應該根據專案實際情況進行設定。
- -XX:GCTimeRatio:直接設定吞吐量大小。該值代表吞吐量比率,所以其應該是一個大於0並且小於100的數,這裡還要求其為整數。
- -XX:UseAdaptiveSizePolicy:用於確定是否開啟GC自適應的調整策略,如果開啟該策略,就不需要手工指定新生代的大小(-Xmn)、Eden區和Survivor區的比例(-XX:SurvivorRatio)、晉升老年代年齡(-XX:PretentureSizeThreshold)等引數。虛擬機器會監控當前系統執行狀態收集效能監控資訊,自動的調整系統中垃圾回收停頓時間或者最大的吞吐量。該方式適合不太清楚如何設定上面那些引數的同學。
Serial Old
Serial Old是Serial的老年代版本,它也是一個單執行緒收集器,使用“標記-整理”演算法。它的作用主要是兩個:一個是搭配Parallel Scavenge收集器使用;另外就是當CMS收集器發生Concurrency Mode Failure時作為備用收集器。
Parallel Old
同Serial Old一樣,Parallel Old是Parallel Scavenge的老年代版本。在注重吞吐量和CPU資源敏感的地方都可以優先考慮Parallel Old可以和Parallel Scavenge一起搭配使用。
CMS收集器
CMS(Concurrency Mark Sweep)是一個以獲取最短回收停頓時間為目標的收集器,允許垃圾回收執行緒和使用者工作執行緒同時執行。其使用“標記-清除”演算法。目前來說例如淘寶等大型網際網路企業都希望請求響應時間能儘量短,並且垃圾回收的停頓時間也儘量短,這種情況就可以使用CMS收集器。
CMS的“標記-清除”演算法分為多個步驟:
- 初始標記:初始標記是用於標記直接與GC Roots關聯的物件,不需要遍歷下去,所需的時間很短。這一過程會發生STW(Stop the World)。
- 併發標記:併發標記就是遍歷所有與GC Roots直接或者間接關聯的物件。
- 重新標記:前面說過CMS允許垃圾回收執行緒和使用者工作執行緒同時執行,所以這一過程是為了標記在前面標記過程中發生變動的物件。這一過程會發生STW(Stop the World)。
- 併發清除:清除掉那些沒有被標記的物件。
其中併發標記和併發清除過程耗費時間最長,但是這兩個階段都可以併發進行,所以對使用者的影響也不會太大。
雖然CMS確實是一款很不錯的垃圾收集器,但是其也還有幾個缺點:
- CMS收集器對CPU資源非常敏感。在併發階段,它雖然不會導致使用者執行緒停頓,但是會因為佔用了一部分執行緒而導致應用程式變慢,總吞吐量會降低。
- CMS收集器無法處理浮動垃圾,由於CMS併發清理階段使用者執行緒還在執行著,伴隨著程式執行自然就會有新的垃圾不斷產生,這部分垃圾出現的標記過程之後,CMS無法在當次收集中處理掉它們,只好留待下一次GC中再清理。這些垃圾就是“浮動垃圾”。同時為了保證在垃圾回收的同時使用者執行緒也可以正常工作,所以不可能對整個區域進行回收,需要預留一部分割槽域給使用者執行緒,如果在垃圾回收階段,預留的垃圾回收區域不足,就可能會出現“Concurrent Mode Failure(併發模式故障)”失敗而導致Full GC產生。
- CMS是一款“標記–清除”演算法實現的收集器,容易出現大量空間碎片。當空間碎片過多,將會給大物件分配帶來很大的麻煩,往往會出現老年代還有很大空間剩餘,但是無法找到足夠大的連續空間來分配當前物件,不得不提前觸發一次Full GC。
G1收集器
G1是一款面向服務端應用的垃圾收集器。G1具備如下特點:
- 並行與併發:G1收集器能充分利用CPU、多核環境下的硬體優勢,使用多個CPU(CPU或者CPU核心)來縮短STW停頓時間。部分其他收集器原本需要停頓Java執行緒執行的GC動作,G1收集器仍然可以通過併發的方式讓java程式繼續執行。
- 分代收集:雖然G1可以不需要其他收集器配合就能獨立管理整個GC堆,但是還是保留了分代的概念。它能夠採用不同的方式去處理新建立的物件和已經存活了一段時間,熬過多次GC的舊物件以獲取更好的收集效果。
- 空間整合:與CMS的“標記–清理”演算法不同,G1從整體來看是基於“標記整理”演算法實現的收集器。從區域性上來看是基於“複製”演算法實現的。
- 可預測的停頓:這是G1相對於CMS的另一個大優勢,降低停頓時間是G1和CMS共同的關注點,但G1除了追求低停頓外,還能建立可預測的停頓時間模型,能讓使用者明確指定在一個長度為M毫秒的時間片段內。
在G1中Heap被分成一塊塊大小相等的region,Region的大小可以通過引數-XX:G1HeapRegionSize
設定,取值範圍從1M到32M,且是2的指數。如果不指定,那麼G1會根據Heap大小自動決定。保留新生代和老年代的概念,但它們不需要物理上的隔離。每塊region都會被打唯一的分代標誌(eden,survivor,old),代表一個分代型別的region可以是不連續的。eden regions構成Eden空間,survivor regions構成Survivor空間,old regions構成了old 空間。通過命令列引數-XX:NewRatio=n
來配置新生代與老年代的比例,n為整數,預設為2,即比例為2:1;-XX:SurvivorRatio=n
可以配置Eden與Survivor的比例,預設為8。
G1收集器進行回收大致可分為以下幾個階段:
- 初始標記:同CMS功能基本一致初始標記是用於標記直接與GC Roots關聯的物件,不需要遍歷下去,所需的時間很短。這一過程會發生STW(Stop the World)。
- 併發標記:併發標記就是遍歷所有與GC Roots直接或者間接關聯的物件。遍歷整個堆尋找活躍物件,這個發生在應用執行時,這個階段可以被年輕代垃圾回收打斷。
- 重新標記:這一過程是為了標記在前面標記過程中發生變動的物件,和CMS的重新標記過程功能上基本保持一致。但是G1使用一個叫作snapshot-at-the-beginning(SATB)的比CMS收集器的更快的演算法。
- 篩選回收:進行垃圾回收,G1保留了YGC並加上了一種全新的MIXGC用於收集老年代。G1中沒有Full GC,G1中的Full GC是採用serial old的Full GC。
Young GC
當Eden空間不足時就會觸發YGC。在G1中YGC也是採用複製存活物件到survivor空間,對於物件的存活年齡滿足晉升條件時,把物件移到老年代。
在對新生代進行垃圾回收時,需要判斷哪些物件能夠會被回收。這裡判斷的方法也是採用可達性分析,標記與GC Roots直接或間接關聯的物件。在CMS中使用了Card Table的結構,裡面記錄了老年代物件到新生代引用。G1也是使用這個思路,定義了一個新的資料結構:Remembered Set。在G1收集器中,Region之間的物件引用以及其他收集器中的新生代與老年代之間的物件引用,虛擬機器都是使用Remembered Set來避免全堆掃描的。G1中每個Region都有一個與之對應的Remembered Set,虛擬機器發現程式在對Reference型別的資料進行寫操作時,會產生一個Write Barrier暫時中斷寫操作,檢查Reference引用的物件是否處於不同的Region之中(在分代的例子中就是檢查是否老年代中的物件引用了新生代中的物件),如果是,便通過CardTable把相關引用資訊記錄到被引用物件所屬的Region的Remembered Set之中。在進行記憶體回收時,在GC根節點的列舉範圍中加入Remembered Set即可保證不對全堆掃描也不會有遺漏。
Full GC
full gc是指對包括新生代、老年代和方法區(元空間)等地區進行垃圾回收。
full gc的觸發包括以下幾種情況:
- 老年代空間不足
- 新生代物件晉升到老年代時,老年代剩餘空間低於新生代晉升為老年代的速率,會觸發老年代回收
- minor gc之後,survior區記憶體不足,將存活物件放入老年代,老年代也不足,觸發Full GC。本質上還是老年代記憶體不足。
- System.gc().
理解GC日誌
這裡介紹一些列印出的gc日誌的資訊:
為了觸發gc寫一段程式碼,實際上也可以直接使用System.gc()
:
public class Test {
public static void main(String[] args) {
byte[] bytes1 = new byte[1024 * 1024];
byte[] bytes2 = new byte[1024 * 1024];
byte[] bytes3 = new byte[1024 * 1024];
byte[] bytes4 = new byte[1024 * 1024];
byte[] bytes5 = new byte[1024 * 1024];
}
public static void test(){
test();
}
}
複製程式碼
要在控制檯列印gc資訊需要我們手動的配一些引數:
- -XX:+PrintGCDetails 輸出GC的詳細日誌
- -XX:+PrintGCTimeStamps/PrintGCDateStamps 輸出GC的時間戳
- -XX:+PrintHeapAtGC 在進行GC的前後列印出堆的資訊
- -Xloggc:../logs/gc.log 日誌檔案的輸出路徑
我這裡使用Idea,直接在VM args配置即可:
現在執行上面的程式即可在控制檯獲得gc資訊:
2019-01-24T20:08:25.811+0800: [GC (Allocation Failure) [PSYoungGen: 1019K->488K(1536K)] 1019K->608K(5632K), 0.0036115 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-01-24T20:08:25.872+0800: [GC (Allocation Failure) [PSYoungGen: 1504K->488K(1536K)] 1624K->780K(5632K), 0.0016239 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-01-24T20:08:25.879+0800: [GC (Allocation Failure) [PSYoungGen: 653K->504K(1536K)] 4017K->3940K(5632K), 0.0009844 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-01-24T20:08:25.880+0800: [GC (Allocation Failure) [PSYoungGen: 504K->504K(1536K)] 3940K->3948K(5632K), 0.0006796 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-01-24T20:08:25.881+0800: [Full GC (Allocation Failure) [PSYoungGen: 504K->0K(1536K)] [ParOldGen: 3444K->3832K(4096K)] 3948K->3832K(5632K), [Metaspace: 3426K->3426K(1056768K)], 0.0076471 secs] [Times: user=0.02 sys=0.00, real=0.01 secs]
2019-01-24T20:08:25.888+0800: [GC (Allocation Failure) [PSYoungGen: 0K->0K(1536K)] 3832K->3832K(5632K), 0.0003390 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
2019-01-24T20:08:25.889+0800: [Full GC (Allocation Failure) Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
at Test.main(Test.java:17)
[PSYoungGen: 0K->0K(1536K)] [ParOldGen: 3832K->3814K(4096K)] 3832K->3814K(5632K), [Metaspace: 3426K->3426K(1056768K)], 0.0065960 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]
Heap
PSYoungGen total 1536K, used 65K [0x00000000ffe00000, 0x0000000100000000, 0x0000000100000000)
eden space 1024K, 6% used [0x00000000ffe00000,0x00000000ffe104d8,0x00000000fff00000)
from space 512K, 0% used [0x00000000fff00000,0x00000000fff00000,0x00000000fff80000)
to space 512K, 0% used [0x00000000fff80000,0x00000000fff80000,0x0000000100000000)
ParOldGen total 4096K, used 3814K [0x00000000ffa00000, 0x00000000ffe00000, 0x00000000ffe00000)
object space 4096K, 93% used [0x00000000ffa00000,0x00000000ffdb9a60,0x00000000ffe00000)
Metaspace used 3472K, capacity 4496K, committed 4864K, reserved 1056768K
class space used 377K, capacity 388K, committed 512K, reserved 1048576K
複製程式碼
上面的gc資訊取一條分析:
[GC/Full GC (Allocation Failure) [PSYoungGen: 1019K->488K(1536K)] 1019K->608K(5632K), 0.0036115 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
最前面的GC/FullGC表示gc型別,GC表示新生代gc(Minor GC),Full GC表示對新生代和老年代一起收集。
[PSYoungGen: 1019K->488K(1536K)]這個表示GC前該記憶體區域已使用容量–>GC後該記憶體區域已使用容量,後面圓括號裡面的1536K為該記憶體區域的總容量。
緊跟著後面的1019K->608K(5632K),表示GC前Java堆已使用容量->GC後Java堆已使用容量,後面圓括號裡面的5632K為Java堆總容量。
[Times: user=0.00 sys=0.00, real=0.00 secs]分別表示使用者消耗的CPU時間,核心態消耗的CPU時間和操作從開始到結束所經過的牆鍾時間,CPU時間和牆鍾時間的差別是,牆鍾時間包括各種非運算的等待耗時,例如等待磁碟I/O、等待執行緒阻塞,而CPU時間不包括這些耗時。因為這裡是測試在幾乎一開始就發生了gc,並且設定的堆疊容量都較小,所以看不出時間。
PSYoungGen和ParOldGen分別代表新生代和老年代所使用的垃圾收集器。PSYoungGen表示Parallel Scavenge收集器,ParOldGen表示Parallel Old。要檢視當前jvm使用那種收集器可以使用-XX:+PrintCommandLineFlags
,命令列下執行即可。
java -XX:PrintCommandLineFlags -version
-XX:InitialHeapSize=132485376 -XX:MaxHeapSize=2119766016 -XX:+PrintCommandLineFlags -XX:+UseCompressedClassPointers -XX:+UseCompressedOops -XX:-UseLargePagesIndividualAllocation -XX:+UseParallelGC
java version "1.8.0_152"
Java(TM) SE Runtime Environment (build 1.8.0_152-b16)
Java HotSpot(TM) 64-Bit Server VM (build 25.152-b16, mixed mode)
複製程式碼
其中的-XX:+UseParallelGC表示使用Parallel Scavenge+Serial Old的組合,但是上面是Parallel Scavenge+parallel old的組合,這是為什麼???
GC中的引數
這裡有一篇不錯的文章總結gc中的引數,比較詳細:GC
記憶體分配與回收策略
物件記憶體的分配,一般是在堆上進行分配,但是隨著JIT技術的發展,部分物件直接在棧上進行記憶體分配。
在前面的分代收集演算法小節處,已經描述了jvm中的分代,將堆分為新生代和老年代。在描述記憶體分配前,我們先來了解下不同的GC型別:
- Minor GC:當Eden區可分配的記憶體不足以建立物件時就會觸發一次Minor GC,Minor GC發生在新生代,由於新生代中大多數物件都是使用過後就不需要所以Minor GC的觸發非常頻繁。在Minor GC中存活下來的物件會被移到Survivor中,如果Survivor記憶體不夠就直接移動到老年代。
- full GC:當準備要觸發一次young GC時,如果發現統計資料說之前young GC的平均晉升大小比目前old gen剩餘的空間大,則不會觸發young GC而是轉為觸發full GC(因為HotSpot VM的GC裡,除了CMS的concurrent collection之外,其它能收集old gen的GC都會同時收集整個GC堆,包括young gen,所以不需要事先觸發一次單獨的young GC)。或者,如果有perm gen的話,要在perm gen分配空間但已經沒有足夠空間時,也要觸發一次full GC;或者System.gc()、heap dump帶GC,預設也是觸發full GC。本內容來源於R大回答。
物件分配
在大多數情況下,物件的記憶體分配都優先在Eden中進行分配,當Eden區可分配的記憶體不足以建立物件時就會觸發一次Minor GC。將Eden區和其中一塊Survivor區內尚存活的物件放入另一塊Survivor區域。如Minor
GC時survivor空間不夠,物件提前進入老年代,老年代空間不夠時就進行Full GC。大物件直接進入老年代,避免在Eden區和Survivor區之間產生大量的記憶體複製,虛擬機器提供了一個-XX:PretureSizeThreshold
引數,令大於這個值得物件直接進入老年代,但是該引數支隊Serial和ParNew收集器有效。 此
外大物件容易導致還有不少空閒記憶體就提前觸發GC以獲取足夠的連續空間。
這裡大物件主要是指那種需要大量連續記憶體的java物件,比如大陣列或者特別長的字串等。
物件晉級
年齡閾值:虛擬機器為每個物件定義了一個物件年齡(Age)計數器, 經第一次Minor GC後
仍然存活,被移動到Survivor空間中, 並將年齡設為1。以後物件在Survivor區中每熬
過一次Minor GC年齡就+1。 當增加到一定程度(預設
15),將會晉升到老年代(晉級的年齡可以通過-XX:MaxTenuringThreshold
進行設定)。
提前晉升: 動態年齡判定,如果在Survivor空間中相同年齡所有物件大小的總和大
於Survivor空間的一半, 年齡大於或等於該年齡的物件就可以直接進入老年代,而無
須等到晉升年齡。
空間分配擔保
在前面說垃圾收集演算法時關於複製物件有說過可能會存在存活下來的物件無法被survivor容納,這時就需要老年代容納無法被survivor容納的物件。而如果老年代也沒有足夠的空間來存放這些物件的話就會觸發一次Full GC。