天天看點

深入了解Java虛拟機(二)——GC

1 概述

垃圾收集器(Garbage Collection)簡稱GC,1960年誕生與MIT的Lisp是第一門真正使用記憶體動态配置設定和垃圾收集技術的語言。上一節《深入了解Java虛拟機(一)——JVM記憶體結構》中了解到記憶體運作時區域的各個部分,其中程式計數器、本地方法棧、虛拟機棧3個區域随着線程而生存死亡,棧幀随着方法的進入和退出做入棧和出棧操作,實作了自動的記憶體清理。是以GC主要的收集區域為Java堆和方法區,這部分的記憶體區域的配置設定和回收都是動态的。

虛拟機如何進行回收的呢,主要從三個方面考慮。

  • 哪些記憶體是需要回收的(如何判斷對象為垃圾對象)?
  • 什麼時候回收?
  • 怎樣進行回收?

2 如何判斷對象為垃圾對象

GC如何确定哪些對象還”存活“,哪些對象”死去“(即對象不再被任何對象使用)。主要通過以下兩種方式:

  1. 引用計數法:在對象中添加一個引用計數器,當有地方引用這個對象的時候,引用計數器的值就+1,當引用失效的時候計數器的值就-1。具有實作簡單,判斷效率高好處。但是這種算法不能實作對象之間的互相引用問題。
/**
 * 
 * @desc 對象 objA 和 objB 互相引用,引用計數均不為 0,除此之外無其他引用,是以如果用引用計數法則GC無法回收
 * VM Args -XX:+PrintGCDetails
 * -XX:+PrintGCDetails 列印gc詳情資訊 
 * @author lb
 * @date 2019年3月18日
 */
public class ReferenceCountGC {
	public Object instance = null;
	// 建立一個2M的記憶體
	private byte[] size = new byte[2 * 1024 * 1024];

	public static void main(String[] args) {
		ReferenceCountGC objA = new ReferenceCountGC();
		ReferenceCountGC objB = new ReferenceCountGC();
		objA.instance = objB;
		objB.instance = objA;
		objA = null;
		objB = null;

		// 手動調用GC
		System.gc();
	}
}

運作結果:
[GC (System.gc()) [PSYoungGen: 6717K->720K(76288K)] 6717K->728K(251392K), 0.0007462 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
[Full GC (System.gc()) [PSYoungGen: 720K->0K(76288K)] [ParOldGen: 8K->512K(175104K)] 728K->512K(251392K), [Metaspace: 2505K->2505K(1056768K)], 0.0042440 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
Heap
 PSYoungGen      total 76288K, used 1966K [0x000000076b500000, 0x0000000770a00000, 0x00000007c0000000)
  eden space 65536K, 3% used [0x000000076b500000,0x000000076b6eba30,0x000000076f500000)
  from space 10752K, 0% used [0x000000076f500000,0x000000076f500000,0x000000076ff80000)
  to   space 10752K, 0% used [0x000000076ff80000,0x000000076ff80000,0x0000000770a00000)
 ParOldGen       total 175104K, used 512K [0x00000006c1e00000, 0x00000006cc900000, 0x000000076b500000)
  object space 175104K, 0% used [0x00000006c1e00000,0x00000006c1e80198,0x00000006cc900000)
 Metaspace       used 2514K, capacity 4486K, committed 4864K, reserved 1056768K
  class space    used 269K, capacity 386K, committed 512K, reserved 1048576K
           

從運作結果看可以看到,GC 執行後有記憶體從6717k到720k,是以表示Jvm沒有因為對象互相引用而不回收,是以,Jvm并不是使用引用計數法判斷對象是否為垃圾對象。

  1. 可達性分析法:利用 GC Roots 向下鍊路查找,搜尋的路徑稱為下路鍊,如果查不到則将查不到的對象及以下鍊路全部回收。可做為 GC Roots 的對象:虛拟機棧(局部變量表)中引用的對象、方法區的類屬性所引用的對象、方法區中常量所引用的對象、本地方法棧中所引用的對象

3 垃圾收集算法

  1. 标記 - 清除算法:首先标記出需要回收的對象,在标記完成後,統一回收被标記的對象。存在不足:效率不高、空間問題(标記清除後會産生大量不連續的記憶體碎片,在後續産生大對象時,無法找到足夠大的連續空間)
  2. 複制算法:将記憶體等分為兩塊,每次使用一塊,當記憶體使用完了,就将存活的對象複制當另一塊,然後将已使用的記憶體空間一次清理掉。實作簡單、效率高,但是記憶體使用浪費太大。

    堆記憶體主要分為Eden空間和兩個較小的Survivor空間,每次使用Eden和一個Survivor。Hotspot虛拟機預設Eden區域和Survivor區域為(8:1:1),也就是每次使用的記憶體為90%。當每次進行GC時,将Eden區的存活對象及Survivor的存活對象複制到另一塊在Survivor區域中,當Survivor空間不夠時,需要依賴其它内儲存(老年代)進行配置設定擔保。

  3. 标記 - 整理算法:主要使用在老年代的計算方式,标記過程與”标記 - 清除算法“一樣,但是後續不是直接回收死亡的對象,而是讓存活的對象向一端移動,然後直接清理掉端邊界以外的記憶體。
  4. 分代收集算法:将Java堆分為新生代、老年代,根據不通特點采用不同的收集算法。新生代中,每次收集都會有大量的對象死去,選用複制算法。老年代對象存活率高,采用标記-清除或标記-整理算法。

4 垃圾收集器

4.1 Serial

  • Serial是最基本、單線程的收集器。進行垃圾收集時,必須停下來其它工作線程,直至收集結束。這種收集方式具有簡單高效的優點。對于桌面應用場景,收集比較少的新生代記憶體,效率也是很高的。Serial 收集器也是虛拟機在Client模式下預設的新生代收集器。
    深入了解Java虛拟機(二)——GC

4.2 ParNew

  • ParNew 就是 Serial 收集器的多線程版本,在性能無關的前提下,ParNew 是作為Server模式下Jvm首選的新生代收集器。
    深入了解Java虛拟機(二)——GC

4.3 Paraller Scavenge

  • Paraller Scavenge為新生代收集器,采用的為複制算法,也是并行的多線程收集器,與 ParNew 基本一緻。主要的特點為達到可控制的吞吐量(吞吐量 = 運作使用者代碼時間 / (運作使用者代碼時間 + 垃圾收集時間)),-XX:MaxGCPauseMillis 垃圾收集器停頓的最大時間(大于0的毫秒數) -XX:GCTimeRatio 吞吐量大小(0,100)

4.4 CMS

  • CMS 收集器是一種以擷取最短回收停頓時間為目标的收集器。是基于”标記 - 清除“算法實作的,主要包含初始标記、并發标記、重新标記、并發清除四個過程。優點:并發收集、低停頓,缺點:并發消耗CPU資源、無法收集浮動垃圾(并發标記的過程中,會有新的垃圾産生)、”标記 - 清除“算法的清除後空間不連續确定
    深入了解Java虛拟機(二)——GC

4.5 G1

  • G1 收集器是一款面向服務端應用的垃圾收集器,優點:并發與并行、分代收集、空間整合(采用"标記 - 整理"算法,不會産生記憶體空間碎片)、可預測的停頓(降低停頓時間)。主要包含初始标記、并發标記、最終标記、篩選回收四個步驟。
    深入了解Java虛拟機(二)——GC

5 垃圾收集器常用參數

jdk1.7中各種垃圾回收器參數使用:

參數 描述
UseSerialGC 虛拟機運作在Client模式下的預設值,打開此開關後,使用 Serial+Serial Old 的收集器組合進行記憶體回收
UseParNewGC 打開此開關後,使用 ParNew + Serial Old 的收集器組合進行記憶體回收
UseConcMarkSweepGC 打開此開關後,使用 ParNew + CMS + Serial Old 的收集器組合進行記憶體回收。Serial Old 收集器将作為 CMS 收集器出現 Concurrent Mode Failure 失敗後的後備收集器使用
UseParallelGC 虛拟機運作在 Server 模式下的預設值,打開此開關後,使用 Parallel Scavenge + Serial Old(PS MarkSweep) 的收集器組合進行記憶體回收
UseParallelOldGC 打開此開關後,使用 Parallel Scavenge + Parallel Old 的收集器組合進行記憶體回收
SurvivorRatio 新生代中 Eden 區域與 Survivor 區域的容量比值,預設為8,代表 Eden : Survivor = 8 : 1
PretenureSizeThreshold 直接晉升到老年代的對象大小,設定這個參數後,大于這個參數的對象将直接在老年代配置設定
MaxTenuringThreshold 晉升到老年代的對象年齡,每個對象在堅持過一次 Minor GC 之後,年齡就增加1,當超過這個參數值時就進入老年代
UseAdaptiveSizePolicy 動态調整 Java 堆中各個區域的大小以及進入老年代的年齡
HandlePromotionFailure 是否允許配置設定擔保失敗,即老年代的剩餘空間不足以應付新生代的整個 Eden 和 Survivor 區的所有對象都存活的極端情況
ParallelGCThreads 設定并行GC時進行記憶體回收的線程數
GCTimeRatio GC 時間占總時間的比率,預設值為99,即允許 1% 的GC時間,僅在使用 Parallel Scavenge 收集器生效
MaxGCPauseMillis 設定 GC 的最大停頓時間,僅在使用 Parallel Scavenge 收集器時生效
CMSInitiatingOccupancyFraction 設定 CMS 收集器在老年代空間被使用多少後觸發垃圾收集,預設值為 68%,僅在使用 CMS 收集器時生效
UseCMSCompactAtFullCollection 設定 CMS 收集器在完成垃圾收集後是否要進行一次記憶體碎片整理,僅在使用 CMS 收集器時生效

6 記憶體配置設定和回收政策

  • 優先配置設定到Eden
  • 大對象直接配置設定到老年代:-XX:PretenureSizeThreshold 指定多大的對象配置設定到老年代,預設值是虛拟機根據記憶體計算
  • 長期存活的對象配置設定到老年代:-XX:MaxTenuringThreshold 指定垃圾回收多少次進入老年代,預設15次
  • 空間配置設定擔保:-XX:+HandlePromotionFailure 是否允許使用空間配置設定擔保
  • 逃逸分析及棧上配置設定

7 虛拟機工具

7.1 jdk 指令行工具

名稱 主要作用
jps JVM Process Status Tool,顯示指定系統内所有的HotSpot虛拟機程序
jstat JVM Statistics Monitoring Tool,用于收集HotSpot虛拟機各方面的運作資料
jinfo Configuration Info for Java,顯示虛拟機配置資訊
jmap Memory Map for Java,生成虛拟機的記憶體轉儲快照(heapdump檔案)
jhat JVM Heap Dump Browser,用于分析heapdump檔案,他會建立一個HTTP/HTML伺服器,讓使用者可以在浏覽器上檢視分析結果
jstack Stack Trace for Java,顯示虛拟機的線程快照

7.1.1 jps 虛拟機程序狀況工具

  • jps(JVM Process Status Tool)可以列出正在運作的虛拟機程序,并顯示虛拟機執行主類(Main Class,main()函數所在的類)名稱以及這些程序的本地虛拟機唯一ID。
指令格式 	
 	jps [options] [hostid]
 	 	
option參數
	-l : 輸出主類全名或jar路徑
	-q : 隻輸出LVMID,省略主類名稱
	-m : 輸出JVM啟動時傳遞給main()的參數
	-v : 輸出JVM啟動時顯示指定的JVM參數
	-mlv :  全部參數
	
其中[option]、[hostid]參數也可以不寫。
           

7.1.2 jstat 虛拟機統計資訊監控工具

  • jstat(JVM statistics Monitoring)用于監視虛拟機運作時狀态資訊的指令,它可以顯示出虛拟機程序中的類裝載、記憶體、垃圾收集、JIT編譯等運作資料。
指令格式	
 	jstat [option] LVMID [interval] [count]
 	
參數
	[option] : 操作參數
	LVMID : 本地虛拟機程序ID
	[interval] : 連續輸出的時間間隔
	[count] : 連續輸出的次數
           

官方文檔介紹(具體參數詳解):https://docs.oracle.com/javase/8/docs/technotes/tools/unix/jstat.html

深入了解Java虛拟機(二)——GC

7.1.3 jinfo java配置資訊工具

  • jinfo(JVM Configuration info)作用是實時檢視和調整虛拟機運作參數。jps -v密碼隻能檢視到顯示指定的參數,如果想要檢視未被顯示指定的參數的值就要使用jinfo密碼。
指令格式
	jinfo [option] [args] LVMID
	
option參數
	-flag : 輸出指定args參數的值
	-flags : 不需要args參數,輸出所有JVM參數的值
	-sysprops : 輸出系統屬性,等同于System.getProperties()
           

7.1.4 jmap java記憶體印象工具

  • jmap(Memory Map for Java)指令用于生成堆轉儲快照(一般稱為heapdump或dump檔案)。jmap的作用并不僅僅是為了擷取dump檔案,它還可以查詢finalize執行隊列、Java堆和永久代的詳細資訊,如空間使用率、目前用的是哪種收集器等。
指令格式
	 jmap [option] LVMID

option參數
	dump : 生成堆轉儲快照,格式為:-dump:[live, ] format=b,file=<filename>,其中live子參數說明是否隻dump出存活的對象。
	finalizerinfo : 顯示在F-Queue隊列等待Finalizer線程執行finalizer方法的對象,隻在Linux / Solaris平台有效
	heap : 顯示Java堆詳細資訊,如使用哪種回收器、參數設定、分代狀況等。隻在Linux / Solaris平台有效
	histo : 顯示堆中對象的統計資訊,包括類、執行個體數量、合計容量
	permstat : 以ClassLoader為統計口徑顯示永久帶記憶體狀态。隻在Linux / Solaris平台有效
	F : 當-dump沒有響應時,強制生成dump快照。隻在Linux / Solaris平台有效
           

7.1.5 jhat 虛拟機堆轉儲快照分析工具

  • jhat(JVM Heap Analysis Tool)指令與jmap搭配使用,來分析jmap生成的堆轉儲快照。jhat内置了一個微型的HTTP/HTML伺服器,生成dump檔案的分析結果後,可以在浏覽器中檢視。
指令格式
	jhat [option] [dumpfile]

參數
	-stack false|true 關閉對象配置設定調用棧跟蹤(tracking object allocation call stack)。 如果配置設定位置資訊在堆轉儲中不可用. 則必須将此标志設定為 false. 預設值為 true.
	-refs false|true 關閉對象引用跟蹤(tracking of references to objects)。 預設值為 true. 預設情況下, 傳回的指針是指向其他特定對象的對象,如反向連結或輸入引用(referrers or incoming references), 會統計/計算堆中的所有對象。
	-port port-number 設定 jhat HTTP server 的端口号. 預設值 7000.
	-exclude exclude-file 指定對象查詢時需要排除的資料成員清單檔案(a file that lists data members that should be excluded 	from the reachable objects query)。 例如, 如果檔案列列出了 java.lang.String.value , 那麼當從某個特定對象 Object o 計算可達的對象清單時, 引用路徑涉及 java.lang.String.value 的都會被排除。
	-baseline exclude-file 指定一個基準堆轉儲(baseline heap dump)。 在兩個 heap dumps 中有相同 object ID 的對象會被标記為不是新的(marked as not being new). 其他對象被标記為新的(new). 在比較兩個不同的堆轉儲時很有用.
	-debug int 設定 debug 級别. 0 表示不輸出調試資訊。 值越大則表示輸出更詳細的 debug 資訊.
	-version 啟動後隻顯示版本資訊就退出
	-J  因為 jhat 指令實際上會啟動一個JVM來執行, 通過 -J 可以在啟動JVM時傳入一些啟動參數. 例如, -J-Xmx512m 則指定運作 jhat 的Java虛拟機使用的最大堆記憶體為 512 MB. 如果需要使用多個JVM啟動參數,則傳入多個 -Jxxxxxx.
           
示例:
jhat 快照位址(jmap後儲存在硬碟的位置)
浏覽器打開 http://localhost:7000
           

7.1.6 jhat Java堆棧跟蹤工具

  • jstack(Stack Trace for Java)指令用于生成虛拟機目前時刻的線程快照(一般稱為threaddump或者javacore檔案)。線程快照就是目前虛拟機内每一條線程正在執行的方法堆棧的集合,生成線程快照的主要目的是定位線程出現長時間停頓的原因,如線程間死鎖、死循環、請求外部資源導緻的長時間等待等都是導緻線程長時間停頓的常見原因。線程出現停頓的時候通過jstack來檢視各個線程的調用堆棧,就可以知道沒有響應的線程到底在背景做些什麼事情,或者等待着什麼資源。
指令格式
	stack [option] LVMID
	
option參數

	-F : 當正常輸出請求不被響應時,強制輸出線程堆棧
	-l : 除堆棧外,顯示關于鎖的附加資訊
	-m : 如果調用到本地方法的話,可以顯示C/C++的堆棧
           

7.2 jdk 可視化工具

7.2.1 JConsole Java監視與管理控制台

  • JConsole 是一種基于 JMX 的可視化監控管理工具。

啟動JConsole

  • 通過jdk/bin/下的“jconsole.exe”來啟動,啟動後将搜尋出本機運作的所有虛拟機程序,不需要再使用 jps 來查詢。輕按兩下程序即可開始監控。
    深入了解Java虛拟機(二)——GC
    深入了解Java虛拟機(二)——GC

7.2.2 VisualVM 多合一故障處理工具

  • VisualVM(All-in-One Java Troubleshooting Tool)是到目前為止随JDK釋出的功能最強大的運作監視和故障處理程式,并且可以遇見在未來一段時間内都是官方主力發展的虛拟機故障處理工具。官方在VisualVM的軟體說明中寫上了“All-in-One”的描述字樣,預示着他除了運作監視、故障處理外,還提供了很多其他方面的功能。如性能分析(Profiling),VisualVM的性能分析功能甚至比起JProfiler、YourKit等專業且收費的Profiling工具都不會遜色多少,而且VisualVM還有一個很大的優點:不需要被監視的程式基于特殊Agent運作,是以他對應用程式的實際性能的影響很小,使得他可以直接應用在生産環境中。這個優點是JProfiler、YourKit等工具無法與之媲美的。

下載下傳位址:https://visualvm.github.io/download.html

軟體示例圖:

深入了解Java虛拟機(二)——GC

8 性能調優

jvm調優沒有固定的方式,根據不同的伺服器、應用部署情況選擇恰當的參數設定,執行最優的性能。以下為調優建議:

  • 當伺服器性能高、記憶體大,應用中建立大對象過多,此時大對象會直接放入老年代,當老年代溢出時,會進行垃圾回收(Full GC),如果伺服器的堆記憶體過大,則會回收時間很長,出現卡頓現象。解決方式:可部署多個web節點,每個web節點适當減少堆記憶體的大小。
  • 處理不對等資料處理時,例如一台伺服器不斷被請求,請求的速度大于了處理請求的速度,請求被積壓在堆中,導緻記憶體溢出。解決方式:可在兩台裝置中間添加一個消息隊列,實作異步處理。
  • 建立對象盡量在方法内,而不是使用用成員變量。