JVM性能優化系列-(2) 垃圾收集器與內存分配策略

JVM.jpg

目前已經更新完《Java併發編程》和《Docker教程》,歡迎關注【後端精進之路】,輕鬆閱讀所有文章。java

後端精進之路.png

Java併發編程:git

Docker教程:程序員

JVM性能優化:github

2. 垃圾收集器與內存分配策略

垃圾收集(Garbage Collection, GC)是JVM實現裏很是重要的一環,JVM成熟的內存動態分配與回收技術使Java(固然還有其餘運行在JVM上的語言,如Scala等)程序員在提高開發效率上得到了驚人的便利。理解GC,對於理解JVM和Java語言有着很是重要的做用。而且當咱們須要排查各類內存溢出、內存泄漏問題時,當垃圾收集稱爲系統達到更高併發量的瓶頸時,只有深刻理解GC和內存分配,才能對這些「自動化」的技術實施必要的監控和調節。面試

GC主要須要解決如下三個問題:算法

  • 哪些內存須要回收?
  • 何時回收?
  • 如何回收?

下面將對這些問題進行一一介紹。編程

2.1 如何判斷對象存活

在堆裏存放着Java世界中幾乎全部的對象實例,垃圾收集器在對堆進行回收前,首要的就是肯定這些對象中哪些還「存活」着,哪些已經「死去」(即不可能再被任何途徑使用的對象)。後端

引用計數算法

引用計數器判斷對象是否存活的過程是這樣的:給對象中添加一個引用計數器,每當有一個地方引用它時,計數器加1;當引用失效時,計數器減1;任什麼時候刻計數器爲0的對象就是不可能再被使用的。數組

引用計數算法的實現簡單,斷定效率也很高,大部分狀況下是一個不錯的算法。它沒有被JVM採用的緣由是它很難解決對象之間循環引用的問題。瀏覽器

可達性分析算法

在主流商用程序語言的實現中,都是經過可達性分析(tracing GC)來斷定對象是否存活的。

算法的基本思路是:經過一系列的稱爲「GC Roots」的對象做爲起點,從這些節點向下搜索,搜索所走過的路徑稱爲引用鏈(Reference Chain),當一個對象到GC Roots沒有任何引用鏈相連(用圖論的話來講,就是GC Roots 到這個對象不可達)時,則證實此對象時不可用的。用下圖來加以說明:

0635cbe8.png

做爲GC Roots的對象包括下面幾種:

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

2.2 各類引用

強引用

通常的Object obj = new Object() ,就屬於強引用。被強引用關聯的對象不會被回收。

軟引用

一些有用可是並不是必需,用軟引用關聯的對象,系統將要發生OOM以前,這些對象就會被回收。

下面的例子中,當程序發生OOM以前,嘗試去回收軟引用所關聯的對象,致使後面獲取到的值爲null。

public class TestSoftRef {
	
	public static class User{
		public int id = 0;
		public String name = "";
		public User(int id, String name) {
			super();
			this.id = id;
			this.name = name;
		}
		@Override
		public String toString() {
			return "User [id=" + id + ", name=" + name + "]";
		}
		
	}
	
	public static void main(String[] args) {

		User u = new User(1,"Vincent");
		SoftReference<User> userSoft = new SoftReference<>(u);
		u = null;//保證new User(1,"Vincent")這個實例只有userSoft在軟引用
		
		System.out.println(userSoft.get());
		System.gc();//展現gc的時候,SoftReference不必定會被回收
		System.out.println("AfterGc");
		System.out.println(userSoft.get());//new User(1,"Vincent")沒有被回收
		List<byte[]> list = new LinkedList<>();
		
		try {
			for(int i=0;i<100;i++) {
				//User(1,"Vincent")實例一直存在
				System.out.println("********************"+userSoft.get());
				list.add(new byte[1024*1024*1]);
			}
		} catch (Throwable e) {
			//拋出了OOM異常後打印的,User(1,"Vincent")這個實例被回收了
			System.out.println("Throwable********************"+userSoft.get());
		}
		
	}
}
複製代碼

程序輸出結果:

Screen Shot 2019-12-19 at 8.52.43 PM.png

弱引用 WeakReference

一些有用(程度比軟引用更低)可是並不是必需,用弱引用關聯的對象,只能生存到下一次垃圾回收以前,GC發生時,無論內存夠不夠,都會被回收。

下面的例子中,發生gc後,弱引用所關聯的對象被回收。

public class TestWeakRef {
	public static class User{
		public int id = 0;
		public String name = "";
		public User(int id, String name) {
			super();
			this.id = id;
			this.name = name;
		}
		@Override
		public String toString() {
			return "User [id=" + id + ", name=" + name + "]";
		}
		
	}
	
	public static void main(String[] args) {
		User u = new User(1,"Vincent");
		WeakReference<User> userWeak = new WeakReference<>(u);
		u = null;
		System.out.println(userWeak.get());
		System.gc();
		System.out.println("AfterGc");
		System.out.println(userWeak.get());
		
	}
}
複製代碼

輸出結果以下:

Screen Shot 2019-12-19 at 8.56.46 PM.png

虛引用

又稱爲幽靈引用或者幻影引用。一個對象是否有虛引用的存在,徹底不會對其生存時間構成影響,也沒法經過虛引用取得一個對象。

爲一個對象設置虛引用關聯的惟一目的,就是能在這個對象被回收時收到一個系統通知

Object obj = new Object();
PhantomReference<Object> pf = new PhantomReference<Object>(obj);
obj = null;
複製代碼

注意:軟引用 SoftReference和弱引用 WeakReference,能夠用在內存資源緊張的狀況下以及建立不是很重要的數據緩存。當系統內存不足的時候,緩存中的內容是能夠被釋放的。 例如,一個程序用來處理用戶提供的圖片。若是將全部圖片讀入內存,這樣雖然能夠很快的打開圖片,但內存空間使用巨大,一些使用較少的圖片浪費內存空間,須要手動從內存中移除。若是每次打開圖片都從磁盤文件中讀取到內存再顯示出來,雖然內存佔用較少,但一些常用的圖片每次打開都要訪問磁盤,代價巨大。這個時候就能夠用軟引用構建緩存。

2.3 方法區回收

不少人認爲方法區沒有垃圾回收,Java虛擬機規範中確實說過不要求,並且在方法區中進行垃圾收集的「性價比」較低:在堆中,尤爲是新生代,常規應用進行一次垃圾收集能夠回收70%~95%的空間,而方法區的效率遠低於此。在JDK 1.8中,JVM摒棄了永久代,用元空間來做爲方法區的實現,下面介紹的將是元空間的垃圾回收。

元空間的內存管理由元空間虛擬機來完成。先前,對於類的元數據咱們須要不一樣的垃圾回收器進行處理,如今只須要執行元空間虛擬機的C++代碼便可完成。在元空間中,類和其元數據的生命週期和其對應的類加載器是相同的。

話句話說,只要類加載器存活,其加載的類的元數據也是存活的,於是不會被回收掉。當一個類加載器被垃圾回收器標記爲再也不存活,其對應的元空間會被回收。

2.4 垃圾收集算法

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

算法分紅「標記」、「清除」兩個階段:首先標記出全部須要回收的對象,在標記完成後統一回收全部被標記的對象。

算法的執行過程以下圖所示:

efc6204a.jpg

標記-清除算法的不足主要有如下兩點:

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

  • 效率問題,由於內存碎片的存在,操做會變得更加費時,由於查找下一個可用空閒塊已再也不是一個簡單操做。

複製算法(Copying)

將可用內存按容量分紅大小相等的兩塊,每次只使用其中的一塊。當這一塊內存用完,就將還存活着的對象複製到另外一塊上面,而後再把已使用過的內存空間一次清理掉。

這樣作使得每次都是對整個半區進行內存回收,內存分配時也就不用考慮內存碎片等複雜狀況,只要移動堆頂指針,按順序分配內存便可,實現簡單,運行高效。只是這種算法的代價是將內存縮小爲原來的一半。 複製算法的執行過程以下圖所示:

f1cada8a.jpg

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

根據老年代的特色,標記-整理(Mark-Compact)算法被提出來,主要思想爲:此算法的標記過程與標記-清除算法同樣,但後續步驟不是直接對可回收對象進行清理,而是讓全部存活的對象都向一端移動,而後直接清理掉邊界之外的內存。具體示意圖以下所示:

d3d3277f.jpg

分代收集算法(Generational Collection)

當前商業虛擬機的垃圾收集都採用分代收集(Generational Collection)算法,此算法相較於前幾種沒有什麼新的特徵,主要思想爲:根據對象存活週期的不一樣將內存劃分爲幾塊,通常是把Java堆分爲新生代和老年代,這樣就能夠根據各個年代的特色採用最適合的收集算法:

  • 新生代

在新生代中,每次垃圾收集時都發現有大批對象死去,只有少許存活,那就選用複製算法,只須要付出少許存活對象的複製成本就能夠完成收集。

  • 老年代

在老年代中,由於對象存活率高、沒有額外空間對它進行分配擔保,就必須使用「標記-清除」或「標記-整理」算法來進行回收。

Minor GC與複製算法

如今的商業虛擬機都使用複製算法來回收新生代。新生代的GC又叫「Minor GC」,IBM公司的專門研究代表:新生代中的對象98%是「朝生夕死」的,因此Minor GC很是頻繁,通常回收速度也比較快,同時「朝生夕死」的特性也使得Minor GC使用複製算法時不須要按照1:1的比例來劃分新生代內存空間。

  • Minor GC過程

事實上,新生代將內存分爲一塊較大的Eden空間兩塊較小的Survivor空間(From Survivor和To Survivor),每次Minor GC都使用Eden和From Survivor,當回收時,將Eden和From Survivor中還存活着的對象都一次性地複製到另一塊To Survivor空間上,最後清理掉Eden和剛使用的Survivor空間。一次Minor GC結束的時候,Eden空間和From Survivor空間都是空的,而To Survivor空間裏面存儲着存活的對象。在下次MinorGC的時候,兩個Survivor空間交換他們的標籤,如今是空的「From」 Survivor標記成爲「To」,「To」 Survivor標記爲「From」。所以,在MinorGC結束的時候,Eden空間是空的,兩個Survivor空間中的一個是空的,而另外一個存儲着存活的對象。

HotSpot虛擬機默認的Eden : Survivor的比例是8 : 1,因爲一共有兩塊Survivor,因此每次新生代中可用內存空間爲整個新生代容量的90%(80%+10%),只有10%的容量會被「浪費」。

  • 分配擔保

上文說的98%的對象可回收只是通常場景下的數據,咱們沒有辦法保證每次回收都只有很少於10%的對象存活,當Survivor空間不夠用時,須要依賴老年代內存進行分配擔保(Handle Promotion)。若是另一塊Survivor上沒有足夠空間存放上一次新生代收集下來的存活對象,這些對象將直接經過分配擔保機制進入老年代。

2.5 HotSpot的算法實現

枚舉根節點

  • GC鏈逐個檢查引用,會消耗比較多時間
  • GC停頓,爲了保持「一致性」,須要「Stop the world」
  • HotSpot使用一組稱爲OopMap的數據結構來記錄哪些地方存着對象的引用。在類加載過程當中,HotSpot就把對象內什麼偏移量上是什麼類型的數據計算出來,在JIT編譯過程當中會在特定的位置記錄下棧和寄存器中哪些位置是引用。

安全點

HotSpot沒有爲每條指令都生成OopMap,只是在特定位置記錄了這些信息,這些位置稱爲安全點。程序執行時並不是在全部地方都能停頓下來開始GC,只有在到達安全點時才能暫停。

安全區域

安全區域是指在一段代碼片斷之中,引用關係不會發生變化,在這個區域內的任何地方進行GC都是安全的。能夠當作是擴展的安全點。

2.6 垃圾收集器

目前爲止並無一個最好的收集器,也沒有萬能的收集器,一般是根據具體狀況選擇合適的收集器。

接下來要介紹的收集器以下圖所示,7種收集器分別做用於不一樣的區域,若是兩個收集器之間存在連線,就說明能夠搭配使用。虛擬機所處的位置,表明是屬於新生代收集器仍是老年代收集器。

All-GC.jpg

基本概念

1. 並行與併發

  • 並行(Parallel):指多條垃圾收集線程並行工做,但此時用戶線程仍然處於等待狀態。

  • 併發(Concurrent):指用戶線程與垃圾收集線程同時執行(但不必定是並行的,可能會交替執行),用戶程序在繼續運行。而垃圾收集程序運行在另外一個CPU上。

2. 吞吐量(Throughput)

吞吐量就是CPU用於運行用戶代碼的時間與CPU總消耗時間的比值,即

吞吐量 = 運行用戶代碼時間 /(運行用戶代碼時間 + 垃圾收集時間)

假設虛擬機總共運行了100分鐘,其中垃圾收集花掉1分鐘,那吞吐量就是99%。

3. Minor GC 和 Full GC

新生代GC(Minor GC):指發生在新生代的垃圾收集動做,由於Java對象大多都具有朝生夕滅的特性,因此Minor GC很是頻繁,通常回收速度也比較快。

老年代GC(Major GC / Full GC):指發生在老年代的GC,出現了Major GC,常常會伴隨至少一次的Minor GC(但非絕對的,在Parallel Scavenge收集器的收集策略裏就有直接進行Major GC的策略選擇過程)。Major GC的速度通常會比Minor GC慢10倍以上。

Serial/Serial Old 收集器

Serial是一個「單線程」的新生代收集器,使用複製算法,它只會使用一個CPU或者一條收集器線程去完成垃圾收集工做,而且它在垃圾收集時,必須暫停全部其餘的工做線程,直到它收集結束。「Stop The World」會在用戶不可見的狀況下,把用戶的工做線程所有停掉。

Serial Old是Serial收集器的老年代版本,一樣是一個「單線程」收集器,使用標記-整理算法。這個收集器主要是給Client模式下的虛擬機使用,Server模式下還有兩個用途,一個是在JDK1.5及以前的版本中與Parallel Scavenge收集器搭配使用,另外一個是做爲CMS收集器的後備預案,在併發收集發生Concurrent Mode Failure時使用。

下圖是 Serial/Serial Old 收集器運行示意圖:

GC-Serial.jpg

上圖中,新生代是Serial收集器採用複製算法老年代是Serial Old收集器採用標記-整理算法。Serial雖然是一個缺點鮮明的收集器,但它依然是虛擬機在Client模式下的默認收集器,它也有優勢,好比簡單高效(與其餘收集器單線程相比),對於單個CPU來講,Serial因爲沒有線程交互的開銷,效率比較高

ParNew 收集器

ParNew收集器是Serial收集器的多線程版本,也是使用複製算法的新生代收集器,它除了使用多條線程進行垃圾收集之外,其餘的好比收集器的控制參數、收集算法、Stop-The-World、對象分配規則、回收策略都和Serial收集器徹底同樣。

下圖是 ParNew/Serial Old 收集器運行示意圖:

GC-ParNew.jpg

上圖中,新生代是ParNew收集器採用複製算法,老年代是Serial Old收集器採用標記-整理算法。ParNew是許多Server模式下虛擬機的首選新生代收集器,由於它能與CMS收集器配合工做。CMS收集器是HotSpot虛擬機中第一個併發的垃圾收集器,CMS第一次實現了讓用戶線程與垃圾收集線程同時工做。

Parallel Scavenge(ParallerGC)/ Parallel Old 收集器

Parallel Scavenge也是使用複製算法的新生代收集器,而且也是一個並行的多線程收集器。Parallel收集器跟其它收集器關注GC停頓時間不一樣,它關注的是吞吐量。低停頓時間適合須要與用戶交互的程序,而高吞吐量能夠高效率的利用CPU時間,能儘快完成運算任務,適合用於後臺計算較多而交互較少的任務。

Parallel收集器提供了兩個虛擬機參數用以控制吞吐量,-XX:MaxGCPauseMillis參數能夠控制垃圾收集的最大停頓時間,-XX:GCTimeRatio參數能夠直接設置吞吐量大小。

-XX:MaxGCPauseMillis的值是一個大於0的毫秒數,使用它減少GC停頓時間是犧牲吞吐量和新生代空間換來的,例如系統把新生代調小,收集300M的新生代確定比500M的快,這也致使垃圾收集發生的更頻繁,原來10秒收集一次每次停頓100毫秒,如今5秒收集一次每次停頓70毫秒,停頓時間降低了,可是吞吐量也降低了。

-XX:GCTimeRatio的值是一個0到100的整數,經過它咱們告訴JVM吞吐量要達到的目標值,-XX:GCTimeRatio=N指定目標應用程序線程的執行時間(與總的程序執行時間)達到N/(N+1)的目標比值。例如,它的默認值是99,就是說要求應用程序線程在整個執行時間中至少99/100是活動的(GC線程佔用其他的1/100),也就是說,應用程序線程應該運行至少99%的總執行時間。

除這兩個參數外,還有一個參數-XX:-UseAdaptiveSizePolicy值得關注,這是一個開關參數,當它打開以後,就不須要手工指定新生代大小(-Xmn)、Eden與Survivor區的比例(-XX:SurvivorRatio)、晉升老年代對象年齡(-XX:PretenureSizeThreshold)等細節參數了,虛擬機會根據系統的運行狀況收集性能監控信息,動態的調整這些參數來提升GC性能,這種調節方式稱爲GC自適應調節策略。這個參數是默認激活的,自適應行爲也是JVM優點之一。

Parallel Old收集器是Parallel Scavenge的老年代版本,使用多線程和標記-整理算法。此收集器在JDK1.6中開始出現,在Parallel Old出現以前,只有Serial Old可以與Parallel Scavenge收集器配合使用。因爲Serial Old這種單線程收集器的性能拖累,致使在老年代比較大的場景下,Parallel Scavenge和Serial Old的組合吞吐量甚至還不如ParNew加CMS的組合。而有了Parallel Old收集器以後,Parallel Scavenge與Parallel Old成了名副其實的吞吐量優先的組合,在注重吞吐量和CPU資源敏感的場景下,均可以優先考慮這對組合。

下圖是 Parallel Scavenge(ParallerGC)/ Parallel Old 收集器運行示意圖:

GC-Parallel.jpg

CMS 收集器

CMS(Concurrent Mark Sweep)收集器是基於標記-清除算法老年代收集器,它以獲取最短回收停頓時間爲目標。CMS是一款優秀的收集器,特色是併發收集、低停頓,它的運行過程稍微複雜些,分爲4個步驟:

  1. 初始標記(CMS initial mark)
  2. 併發標記(CMS concurrent mark)
  3. 從新標記(CMS remark)
  4. 併發清除(CMS concurrent sweep)

4個步驟中只有初始標記、從新標記這兩步須要「Stop The World」。初始標記只是標記一下GC Roots能直接關聯的對象,速度很快。併發標記是進行GC Roots Tracing的過程,也就是從GC Roots開始進行可達性分析。從新標記則是爲了修正併發標記期間因用戶線程繼續運行而致使標記發生變更的那一部分記錄。併發清理固然就是進行清理被標記對象的工做。

下圖是 CMS 收集器運行示意圖:

GC-CMS.jpg

整個過程當中,併發標記與併發清除過程耗時最長,但它們均可以與用戶線程一塊兒工做,因此總體上說,CMS收集器的內存回收過程是與用戶線程一塊兒併發執行的。

可是CMS收集器也並不完美,它有如下3個缺點:

  1. CMS收集時對CPU資源很是敏感,併發階段雖然不會致使用戶線程停頓,可是會由於佔用CPU資源致使應用程序變慢、總吞吐量變低。
  2. CMS收集器沒法處理浮動垃圾(Floating Garbage),可能會產生Full GC。浮動垃圾就是在併發清理階段,依然在運行的用戶線程產生的垃圾。這部分垃圾出如今標記過程以後,CMS沒法在當次集中處理它們,只能等下一次GC時清理。
  3. CMS是基於標記-清除算法的收集器,可能會產生大量的空間碎片,從而沒法分配大對象而致使Full GC提早產生。

因爲存在浮動垃圾,以及用戶線程正在運行,所以CMS收集器不能像其餘收集器那樣等到老年代幾乎徹底被填滿了再進行收集,須要預留一部分空間提供併發收集時的程序運做使用。可使用-XX:CMSInitialOccupyFraction參數調整默認CMS收集器的啓動閾值。要是CMS運行期間預留的內存沒法知足程序須要,就會出現一次「Concurrent Mode Failure」失敗,這時虛擬機將啓動後備預案:臨時啓用Serial Old收集器來從新進行老年代的垃圾收集,這樣停頓時間就很長了。因此說參數-XX:CMSInitiatingOccupancyFraction設置得過高很容易致使大量「Concurrent Mode Failure」失敗,性能反而下降。 -XX:+UseCMSCompactAtFullCollection用於在CMS收集器頂不住要進行FullGC時開啓內存碎片的合併整理過程,內存整理的過程是沒法併發的,空間碎片問題沒有了,但停頓時間不得不變長。-XX:CMSFullGCsBeforeCompaction,這個參數是用於設置執行多少次不壓縮的Full GC後,跟着來一次帶壓縮的(默認值爲0,表示每次進入FullGC時都進行碎片整理)。

G1收集器

G1(Garbage-First)收集器是面向服務端應用的垃圾收集器,它被寄予厚望以用來替換CMS收集器。在G1以前的收集器中,收集的範圍要麼是整個新生代要麼就是老年代,而G1再也不從物理上區分新生代老年代,G1能夠獨立管理整個Java堆。它將Java堆劃分爲多個大小相等的獨立區域(Region),雖然還有新生代老年代的概念,但再也不是物理隔離的,而都是一部分Region(不須要連續)的集合。

與其餘收集器相比,G1收集器的特色有:

  • 並行與併發:G1能充分利用多CPU或者多核心的CPU,來縮短Stop The World的停頓時間。
  • 分代收集:雖然G1收集器能夠獨立管理整個GC堆,但它能採用不一樣的方式處理「新對象」和「老對象」,以達到更好的收集效果。
  • 空間整合:G1從總體看是基於標記-整理算法的,從局部看(兩個Region之間)是基於複製算法實現的,這兩個算法在收集時都不會產生空間碎片,這樣就有連續可用的內存用以分配大對象。
  • 可預測的停頓:G1除了追求低停頓外,還能創建可預測的停頓時間模型,能夠明確指定一個最大停頓時間(-XX:MaxGCPauseMillis),停頓時間須要不斷調優找到一個理想值,過大太小都會拖慢性能。

G1收集器之因此能創建可預測的停頓時間模型,是由於它能夠避免在整個Java堆中進行全區域的垃圾收集,G1根據各個Region裏垃圾堆積的價值大小(回收所獲空間大小及所需時間的經驗值),在後臺維護一個優先列表,每次根據容許的收集時間,優先回收價值最大的Region,這也是Garbage-First名稱的由來。

G1收集器的Region以下圖所示:

GC-G1-Region.jpg

圖中的E表明是Eden區,S表明Survivor,O表明Old區,H表明humongous表示巨型對象(大於Region空間的對象)。從圖中能夠看出各個區域邏輯上並非連續的,而且一個Region在某一個時刻是Eden,在另外一個時刻就可能屬於老年代。G1在進行垃圾清理的時候就是將一個Region的對象拷貝到另一個Region中。

避免全堆掃描:G1中引入了Remembered Set(記憶集)。每一個Region中都有一個Remembered Set,記錄的是其餘Region中的對象引用本Region對象的關係(誰引用了個人對象)。因此在垃圾回收時,在GC根節點的枚舉範圍中加入Remembered Set便可保證不對全堆掃描也不會有遺漏。G1裏面還有另一種數據結構叫Collection Set,Collection Set記錄的是GC要收集的Region的集合,Collection Set裏的Region能夠是任意代的。在GC的時候,對於跨代對象引用,只要掃描對應的Collection Set中的Remembered Set便可。

G1收集器的收集過程以下圖所示:

G1.jpg

如圖所示,G1收集過程有以下幾個階段:

  • 初始標記(Initial Marking):標記一下GC Roots能關聯到的對象,須要停頓線程可是耗時短,會停頓用戶線程(Stop the World)
  • 併發標記(Concurrent Marking):從GC Root開始對堆中對象進行可達性分析,找出存活對象,這階段耗時長可是能夠與用戶線程併發執行。
  • 最終標記(Final Marking):修正在併發標記階段,因用戶線程繼續運行而致使標記產生變更的那一部分標記記錄,這階段須要停頓用戶線程(Stop the World),可是可並行執行。
  • 篩選回收(Live Data Counting and Evacuation):會對各個Region的回收價值和成本進行排序,根據用戶指望的GC停頓時間來制定回收計劃,該階段也是會停頓用戶線程(Stop the World)。

如下是對全部垃圾收集器的總結:

Screen Shot 2019-12-19 at 11.06.27 PM.png

Screen Shot 2019-12-19 at 11.06.40 PM.png

經常使用的垃圾收集器參數

如下是JVM中經常使用的垃圾收集器參數:

VM參數 描述
-XX:+UseSerialGC 指定Serial收集器+Serial Old收集器組合執行內存回收
-XX:+UseParNewGC 指定ParNew收集器+Serilal Old組合執行內存回收
-XX:+UseParallelGC 指定Parallel收集器+Serial Old收集器組合執行內存回收
-XX:+UseParallelOldGC 指定Parallel收集器+Parallel Old收集器組合執行內存回收
-XX:+UseConcMarkSweepGC 指定CMS收集器+ParNew收集器+Serial Old收集器組合執行內存回收。優先使用ParNew收集器+CMS收集器的組合,當出現ConcurrentMode Fail或者Promotion Failed時,則採用ParNew收集器+Serial Old收集器的組合
-XX:+UseG1GC 指定G1收集器併發、並行執行內存回收
-XX:+PrintGCDetails 打印GC詳細信息
-XX:+PrintGCTimeStamps 輸出GC的時間戳(以基準時間的形式)
-XX:+PrintGCDateStamps 輸出GC的時間戳(以日期的形式)
-XX:+PrintHeapAtGC 在進行GC的先後打印出堆的信息
-XX:+PrintTenuringDistribution 在進行GC時打印survivor中的對象年齡分佈信息
-Xloggc:$CATALINA_HOME/logs/gc.log 指定輸出路徑收集日誌到日誌文件
-XX:NewRatio 新生代與老年代(new/old generation)的大小比例(Ratio). 默認值爲 2
-XX:SurvivorRatio eden/survivor 空間大小的比例(Ratio). 默認值爲 8
-XX:GCTimeRatio GC時間佔總時間的比率,默認值99%,僅在Parallel Scavenge收集器時生效
-XX:MaxGCPauseMills 設置GC最大停頓時間,僅在Parallel Scavenge收集器時生效
-XX:PretensureSizeThreshold 直接晉升到老年代的對象大小,大於這個參數的對象直接在老年代分配
-XX:MaxTenuringThreshold 提高老年代的最大臨界值(tenuring threshold). 默認值爲 15
-XX:UseAdaptiveSizePolicy 動態調整Java堆中各個區域的大小及進入老年代的年齡
-XX:HandlePromotionFailure 是否容許分配擔保失敗,即老年代的剩餘空間不足以應付新生代整個Eden和Survivor中對象都存活的極端狀況
-XX:ParallelGCThreads 設置垃圾收集器在並行階段使用的線程數,默認值隨JVM運行的平臺不一樣而不一樣
-XX:ParallelCMSThreads 設定CMS的線程數量
-XX:ConcGCThreads 併發垃圾收集器使用的線程數量. 默認值隨JVM運行的平臺不一樣而不一樣
-XX:CMSInitiatingOccupancyFraction 設置CMS收集器在老年代空間被使用多少後觸發垃圾收集,默認68%
-XX:+UseCMSCompactAtFullCollection 設置CMS收集器在完成垃圾收集後是否要進行一次內存碎片的整理
-XX:CMSFullGCsBeforeCompaction 設定進行多少次CMS垃圾回收後,進行一次內存壓縮
-XX:+CMSClassUnloadingEnabled 容許對類元數據進行回收
-XX:CMSInitiatingPermOccupancyFraction 當永久區佔用率達到這一百分比時,啓動CMS回收
-XX:UseCMSInitiatingOccupancyOnly 表示只在到達閥值的時候,才進行CMS回收
-XX:InitiatingHeapOccupancyPercent 指定當整個堆使用率達到多少時,觸發併發標記週期的執行,默認值是45%
-XX:G1HeapWastePercent 併發標記結束後,會知道有多少空間會被回收,再每次YGC和發生MixedGC以前,會檢查垃圾佔比是否達到此參數,達到了纔會發生MixedGC
-XX:G1ReservePercent 設置堆內存保留爲假天花板的總量,以下降提高失敗的可能性. 默認值是 10
-XX:G1HeapRegionSize 使用G1時Java堆會被分爲大小統一的的區(region)。此參數能夠指定每一個heap區的大小. 默認值將根據 heap size 算出最優解. 最小值爲 1Mb, 最大值爲 32Mb

2.7 內存分配策略

對象優先在Eden區分配

大多數狀況下,對象在新生代的Eden區中分配。當Eden區沒有足夠空間進行分配時,虛擬機將發起一次Minor GC。

大對象直接進入老年代

所謂的大對象是指,須要大量連續內存空間的Java對象,最典型的大對象就是很長的字符串以及數組。大對象對虛擬機的內存分配來講是一個壞消息,常常出現大對象容易致使內存還有很多空間時,就提早觸發GC以獲取足夠的連續空間來安置它們。

虛擬機提供了一個-XX:PretenureSizeThreshold參數,令大於這個設置值的對象直接在老年代分配。這樣作的目的是避免在Eden區及兩個Survivor區之間發生大量的內存複製。缺省爲0,表示毫不會直接分配在老年代。

長期存活的對象將進入老年代

虛擬機給每一個對象定義了一個對象年齡(Age)計數器。若是對象在Eden出生,並通過第一次Minor GC後仍然存活,而且能被Survivor容納的話,將被移動到Survivor空間中,而且對象年齡設爲1。對象在Survivor區中每「熬過」一次Minor GC,年齡就增長1歲,當它的年齡增長到必定程度(默認爲15歲),就將會被晉升到老年代中。對象晉升老年代的年齡閾值,能夠經過參數-XX:MaxTenuringThreshold設置。

動態對象年齡斷定

爲了能更好地適應不一樣程序的內存情況,虛擬機並非永遠地要求對象的年齡必須達到了MaxTenuringThreshold才能晉升老年代,若是在Survivor空間中相同年齡全部對象大小的總和大於Survivor空間的一半,年齡大於或等於該年齡的對象就能夠直接進入老年代,無須等到MaxTenuringThreshold中要求的年齡。

空間分配擔保

新生代中有大量的對象存活,survivor空間不夠,當出現大量對象在MinorGC後仍然存活的狀況(最極端的狀況就是內存回收後新生代中全部對象都存活),就須要老年代進行分配擔保,把Survivor沒法容納的對象直接進入老年代.只要老年代的連續空間大於新生代對象的總大小或者歷次晉升的平均大小,就進行Minor GC,不然FullGC。

2.8 Full GC的觸發條件

對於Minor GC,其觸發條件很是簡單,當Eden區空間滿時,就將觸發一次Minor GC。而Full GC則相對複雜,所以本節咱們主要介紹Full GC的觸發條件。

  • 調用System.gc()

此方法的調用是建議JVM進行Full GC,雖然只是建議而非必定,但不少狀況下它會觸發 Full GC,從而增長Full GC的頻率,也即增長了間歇性停頓的次數。所以強烈建議能不使用此方法就不要使用,讓虛擬機本身去管理它的內存,可經過-XX:+ DisableExplicitGC來禁止RMI調用System.gc()。

  • 老年代空間不足

老年代空間不足的常見場景爲前文所講的大對象直接進入老年代、長期存活的對象進入老年代等,當執行Full GC後空間仍然不足,則拋出以下錯誤: Java.lang.OutOfMemoryError: Java heap space 爲避免以上兩種情況引發的Full GC,調優時應儘可能作到讓對象在Minor GC階段被回收、讓對象在新生代多存活一段時間及不要建立過大的對象及數組。

  • 空間分配擔保失敗

前文介紹過,使用複製算法的Minor GC須要老年代的內存空間做擔保,若是出現了HandlePromotionFailure擔保失敗,則會觸發Full GC。

  • JDK 1.7及之前的永久代空間不足

在JDK 1.7及之前,HotSpot虛擬機中的方法區是用永久代實現的,永久代中存放的爲一些class的信息、常量、靜態變量等數據,當系統中要加載的類、反射的類和調用的方法較多時,Permanet Generation可能會被佔滿,在未配置爲採用CMS GC的狀況下也會執行Full GC。若是通過Full GC仍然回收不了,那麼JVM會拋出以下錯誤信息: java.lang.OutOfMemoryError: PermGen space 爲避免PermGen佔滿形成Full GC現象,可採用的方法爲增大PermGen空間或轉爲使用CMS GC。

在JDK 1.8中用元空間替換了永久代做爲方法區的實現,元空間是本地內存,所以減小了一種Full GC觸發的可能性。

  • Concurrent Mode Failure

執行CMS GC的過程當中同時有對象要放入老年代,而此時老年代空間不足(有時候「空間不足」是CMS GC時當前的浮動垃圾過多致使暫時性的空間不足觸發Full GC),便會報Concurrent Mode Failure錯誤,並觸發Full GC。

2.9 新生代配置實戰

關於新生代的配置,主要有下面三種參數:

-XX:NewSize/MaxNewSize : 新生代的size和最大size,該參數優先級最高。 -Xmn(能夠當作NewSize= MaxNewSize):新生代的大小,該參數優先級次高。 -XX:NewRatio: 表示比例,例如=2,表示 新生代:老年代 = 1:2,該參數優先級最低。

還有參數:-XX:SurvivorRatio 表示Eden和Survivor的比值,缺省爲8,表示 Eden:FromSurvivor:ToSurvivor= 8:1:1

下面舉例參數配置進行實戰,程序中生成了10個大小爲1M的數組,

public class NewSize {

	public static void main(String[] args) {
		int cap = 1*1024*1024;//1M
		byte[] b1 = new byte[cap];
		byte[] b2 = new byte[cap];
		byte[] b3 = new byte[cap];
		byte[] b4 = new byte[cap];
		byte[] b5 = new byte[cap];
		byte[] b6 = new byte[cap];
		byte[] b7 = new byte[cap];
		byte[] b8 = new byte[cap];
		byte[] b9 = new byte[cap];
		byte[] b0 = new byte[cap];
	}
}
複製代碼
  1. -Xms20M -Xmx20M -XX:+PrintGCDetails –Xmn2m -XX:SurvivorRatio=2

沒有垃圾回收,數組都在老年代。

Screen Shot 2019-12-20 at 2.49.47 PM.png

  1. -Xms20M -Xmx20M -XX:+PrintGCDetails -Xmn7m -XX:SurvivorRatio=2

發生了垃圾回收,新生代存了部分數組,老年代也保存了部分數組,發生了晉升現象。

Screen Shot 2019-12-20 at 2.52.01 PM.png

  1. -Xms20M -Xmx20M -XX:+PrintGCDetails -Xmn15m -XX:SurvivorRatio=8

新生代能夠放下全部的數組,老年代沒放。

Screen Shot 2019-12-20 at 2.55.46 PM.png

  1. -Xms20M -Xmx20M -XX:+PrintGCDetails -XX:NewRatio=2

發生了垃圾回收,出現了空間分配擔保,並且發生了FullGC。

Screen Shot 2019-12-20 at 2.58.30 PM.png

2.10 內存泄漏和內存溢出

  • 內存溢出:實實在在的內存空間不足致使;

  • 內存泄漏:該釋放的對象沒有釋放,多見於本身使用容器保存元素的狀況下。

下面舉例說明,例子中實現了一個基本的棧,注意看出棧的部分,爲了幫助GC,當出棧完成後,手動將棧頂的引用清空,有助於後續元素的gc。這裏若是不清空,當元素出棧後,棧頂原來的位置還有該元素的引用,因此可能形成沒法對已經出棧的元素進行回收,形成內存泄露。

public class Stack {
	
	public  Object[] elements;
	private int size = 0;//指示器,指示當前棧頂的位置

    private static final int Cap = 16;

    public Stack() {
    	elements = new Object[Cap];
    }

    //入棧
    public void push(Object e){
    	elements[size] = e;
    	size++;
    }

    //出棧
    public Object pop(){
    	size = size-1;
    	Object o = elements[size];
    	elements[size] = null;//help gc
        return o;
    }
    
    public static void main(String[] args) {
        Stack stack = new Stack();
        Object o = new Object();
        System.out.println("o="+o);
        stack.push(o);
        Object o1 =  stack.pop();
        System.out.println("o1="+o1);
        
        System.out.println(stack.elements[0]);
    }
}
複製代碼

2.11 淺堆和深堆

淺堆 :(Shallow Heap)是指一個對象所消耗的內存。例如,在32位系統中,一個對象引用會佔據4個字節,一個int類型會佔據4個字節,long型變量會佔據8個字節,每一個對象頭須要佔用8個字節。

深堆 :這個對象被GC回收後,能夠真實釋放的內存大小,也就是隻能經過對象被直接或間接訪問到的全部對象的集合。通俗地說,就是指僅被對象所持有的對象的集合。

舉例:對象A引用了C和D,對象B引用了C和E。那麼對象A的淺堆大小隻是A自己,不含C和D,而A的實際大小爲A、C、D三者之和。而A的深堆大小爲A與D之和,因爲對象C還能夠經過對象B訪問到,所以不在對象A的深堆範圍內。

2.12 jdk工具

jps

列出當前機器上正在運行的虛擬機進程 -p:僅僅顯示VM 標示,不顯示jar,class, main參數等信息. -m:輸出主函數傳入的參數. 下的hello 就是在執行程序時從命令行輸入的參數 -l: 輸出應用程序主類完整package名稱或jar完整名稱. -v: 列出jvm參數, -Xms20m -Xmx50m是啓動程序指定的jvm參數

jstat

是用於監視虛擬機各類運行狀態信息的命令行工具。它能夠顯示本地或者遠程虛擬機進程中的類裝載、內存、垃圾收集、JIT編譯等運行數據,在沒有GUI圖形界面,只提供了純文本控制檯環境的服務器上,它將是運行期定位虛擬機性能問題的首選工具。

假設須要每250毫秒查詢一次進程2764垃圾收集情況,一共查詢20次,那命令應當是:jstat-gc 2764 250 20

經常使用參數: -class (類加載器) -compiler (JIT) -gc (GC堆狀態) -gccapacity (各區大小) -gccause (最近一次GC統計和緣由) -gcnew (新區統計) -gcnewcapacity (新區大小) -gcold (老區統計) -gcoldcapacity (老區大小) -gcpermcapacity (永久區大小) -gcutil (GC統計彙總) -printcompilation (HotSpot編譯統計)

jinfo

查看和修改虛擬機的參數jinfo –sysprops 能夠查看由System.getProperties()取得的參數 jinfo –flag 未被顯式指定的參數的系統默認值 jinfo –flags(注意s)顯示虛擬機的參數 jinfo –flag +[參數] 能夠增長參數,可是僅限於由java -XX:+PrintFlagsFinal –version查詢出來且爲manageable的參數 jinfo –flag -[參數] 能夠去除參數 Thread.getAllStackTraces();

jmap

用於生成堆轉儲快照(通常稱爲heapdump或dump文件)。jmap的做用並不只僅是爲了獲取dump文件,它還能夠查詢finalize執行隊列、Java堆和永久代的詳細信息,如空間使用率、當前用的是哪一種收集器等。和jinfo命令同樣,jmap有很多功能在Windows平臺下都是受限的,除了生成dump文件的-dump選項和用於查看每一個類的實例、空間佔用統計的-histo選項在全部操做系統都提供以外,其他選項都只能在Linux/Solaris下使用。 jmap -dump:live,format=b,file=heap.bin Sun JDK提供jhat(JVM Heap Analysis Tool)命令與jmap搭配使用,來分析jmap生成的堆轉儲快照。

jhat

jhat dump文件名 後屏幕顯示「Server is ready.」的提示後,用戶在瀏覽器中鍵入http://localhost:7000/就能夠訪問詳情.

jstack

(Stack Trace for Java)命令用於生成虛擬機當前時刻的線程快照。線程快照就是當前虛擬機內每一條線程正在執行的方法堆棧的集合,生成線程快照的主要目的是定位線程出現長時間停頓的緣由,如線程間死鎖、死循環、請求外部資源致使的長時間等待等都是致使線程長時間停頓的常見緣由。 在代碼中能夠用java.lang.Thread類的getAllStackTraces()方法用於獲取虛擬機中全部線程的StackTraceElement對象。使用這個方法能夠經過簡單的幾行代碼就完成jstack的大部分功能,在實際項目中不妨調用這個方法作個管理員頁面,能夠隨時使用瀏覽器來查看線程堆棧。

jconsole

Java提供的GUI監視與管理平臺。

visualvm

和jconsole相似,可是經過插件擴展,能夠具有遠優於jconsole的可視化功能。


參考:


本文由『後端精進之路』原創,首發於博客 teckee.github.io/ , 轉載請註明出處

搜索『後端精進之路』關注公衆號,馬上獲取最新文章和價值2000元的BATJ精品面試課程

後端精進之路.png
相關文章
相關標籤/搜索