JVM虛擬機總結
讀了周志明老師的《深入理解Java虛擬機:JVM高級特性與最佳實踐》第三版,總結一下里面的知識點。一方面是知識儲備更多一些,另外是也為接下來的面試準備一下。
全書分為13個章節,共5部分內容。我著重是看了jvm的內管管理、垃圾收集與內存分配策略、虛擬機故障工具和調優實戰、類加載機制、Java內存模型以及線程安全鎖這幾個章節,一方面這幾個章節里面的知識點在實際的工作里面有所適用,另外一方面是這里面的知識點基本都是面試經常面到的。
Java虛擬機把內存分為若干個不同的區域,包括程序計數器、虛擬機棧、本地方法棧、方法區和堆。方法區和堆屬于所有線程共享的數據區,其他三個則是線程私有的。
程序計數器:它是當前線程所執行的字節碼的行號指示器,此區域是唯一一個不會出現OutofMemoryError的區域
虛擬機棧:虛擬機棧是描述Java方法執行的線程內存模型:每個方法被執行時候,Java虛擬機都會創建一個棧幀,用戶存儲局部變量、操作數棧、動態鏈接、方法出口等信息。每個方法被調用直至執行完畢的過程,就對應的一個棧幀在虛擬機中從入棧到出棧的過程。如果線程請求的棧深度大于虛擬機所允許的深度,則會拋出StackOverflowError 異常
本地方法棧:本地方法棧和虛擬機棧作用是類似的,區別是虛擬機棧是服務Java(也就是字節碼),而本地方法棧是服務native方法的
Java堆:對于Java程序來說,Java堆是虛擬機所管理的內存中最大的一塊。Java堆是被所有線程共享的區域,在虛擬機啟動時創建,此區域的唯一目的就是存儲對象。 接下來的基本都是在Java堆里面的操作,下面會在重點來說。當Java堆內存不足時候,虛擬機會拋出OutofMemoryError異常
方法區:方法區也是線程共享的,主要是存儲已經被虛擬機加載的類型信息、常量、靜態變量、即時編譯器編譯后的代碼緩存等
了解了Java虛擬機的構成之后,接下來就是針對Java虛擬機的重點-Java堆來展開介紹了。Java里面有大量的對象,這些對象怎么在堆里面創建、回收、生命周期是怎么樣的,這些都是Java堆的處理。
在Java里面,創建一個對象是通過new的方式來創建的,創建之后Java堆為其分配好內存的大小,當該對象不在使用或者怎么知道該對象不在使用后,Java回收期會對該對象進行回收呢? Java虛擬機通過GcRoot算法和引用計數算法來確定一個對象是否需要進行回收。
引用計數算法:在對象中添加一個引用計算器,每當有一個地方引用他時候,計數器加1,當引用失效時候,計數器減一。但其很難解決對象互相循環引用的問題
可達性分析算法:這個算法的思路是通過一系列成為Gc Roots 的跟對象作為起始點合集,如果某個對象到Gc Roots沒有任何的引用鏈相連,則認為此對象不在使用。
當知道一個對象是否是出于要回收還是繼續存活后,就可以對該對象進行垃圾回收了。虛擬機在對對象回收的過程中,使用了很多種垃圾回收器,隨著計算機以及虛擬機的發展,垃圾回收器也紛繁多樣,適用不同的場景。
垃圾收集算法目前都是基于這幾種方式為基礎的:
標記-清除算法(mark-sweep):算法分為標記和清除階段,首先標記出所有需要回收的對象,在標記完成后,統一回收所有標記的對象。 該算法是最基礎的收集算法,他的缺點主要有兩個:第一個是執行效率不穩定,如果Java對象中含有大量的對象,而且其中大部分是需要回收的,這時會進行大量的標記和清除動作,導致標記和清除都會隨著對象的數量增長執行效率的降低;第二個則是會產生內存空間碎片化問題,導致后面如果需要大對象分配時候,沒有連續的內存空間而需要提前觸發一次full gc
標記-復制算法:將可用內存按照容量劃分為大小相同的兩塊,每次只使用一塊,當這一塊使用完之后,將還存活的對象拷貝到另一塊上面,然后把已使用過的內存空間進行一次清理。如果內存中多數對象都是存活的,這種算法將會產生大量的內存間復制的開銷,但是對于少數的可回收的情況,則會很快,而且也沒有空間碎片 。標記-復制算法的優缺點也很明顯,優點是不會產生空間碎片,缺點則是內存的使用降低了
標記-整理算法(mark-compact):由于標記-復制算法存在內存使用率不高的問題,以及當有大量的對象都存活時候,復制算法和清除算法都有一些效率的問題,因此針對老年代的對象的死亡特征,提出來了標記-整理算法。標記整理算法的標記部分,和標記-清除算法一樣,但是比標記之后不在進行清理,還是讓所有存活的對象都向內存空間一端移動,然后直接清理掉邊界以外的內存。這樣的優點很明顯,首先是內存空間不在存在碎片化的問題,其次是也沒有浪費內存。但是也有缺點:就是整理時候的移動,這種對象移動的操作必須全程暫停用戶的應用程序才行。也就是 stop-the-world. 基于標記整理和標記清除算法的優缺點,是否移動都會存在弊端:移動則內存回收更加復雜,不移動則存在空間碎片導致內存分配更加復雜,從垃圾回收的停頓時間來看,不移動對應垃圾回收的停頓時間更短,但是從整個程序的吞吐量來說,移動對象則更劃算。
基于以上幾種垃圾回收的算法,出現了幾個比較經典的垃圾收集器:從早期的Serial收集器,parNew/Parallel以及接下來的 cms/g1/,以及低延遲的 zgc/shenandoash收集器等。下面會介紹下每種收集器。
Serial收集器:Serial收集器是歷史最悠久,最基礎的收集器。這個收集器是單線程收集器。新生代使用標記-復制算法,老年代使用標記-整理算法
ParNew收集器:parNew收集器是Serial收集器的多線程版本,除了同時使用多條線程進行垃圾收集外,其他的 收集算法、stop the world、對象分配規則、回收策略等都和Serial收集器一樣。
Paralel Scavenge收集器: Paralel Scavenge也是一款新生代收集器,同樣是基于標記=復制算法實現,也能并行收集的多線程收集器,和ParNew收集器很相似,但是它的目標是達到一個可控制吞吐量的收集器,
吞吐量 = 運行用戶代碼時間/(運營用戶代碼時間+垃圾回收收集時間)
高吞吐量可以最高效率的利用服務器資源,盡快完成程序的運算任務。Paralel收集器會根據當前系統的允許情況,動態的調整以提供最合適的停頓時間或最大的吞吐量。這種方式稱為垃圾收集的自適應的調整策略。
Serial Old 收集器:主要是Serial老年代版本,使用標記-整理算法
Parallel old收集器:是Paralel Scavenge的老年代版本,基于標記-整理算法
CMS收集器:cms 收集器是以最短回收停頓時間為目標的垃圾收集器。運作過程分為四個步驟:
初始標記:
并發標記
重新標記:
并發清除:使用并發-清除算法,會存在空間碎片化導致空間不連續
優點: 并發收集、低停頓、 缺點:由于在并發標記階段,會占用一部分系統資源,導致總吞吐量降低,特別的處理器的核數比較低(低于4個)的時候。另外就是由于使用了標記-清除算法,會產生空間碎片導致空間不連續的問題。特別是在老年代時候,會出現明明有很大的內存空間,但是無法找到足夠大的連續空間而觸發一次full GC
Garbage first(G1):g1收集器是面向局部收集以及基于Region的內存布局形式。R1把整個內存區域分為了多個Region,每個region 里面 有suvive/edon區域,以及專門存儲大對象的Humongous區域,在做垃圾收集時候,他是通過回收不同的region區域來進行局部垃圾回收的,這種方式保證了G1收集器在有限時間獲取盡可能高的收集效率。
G1收集器也是分為四個步驟:
初始標記:
并發標記:
最終標記:
帥選標記:
g1整體上是采用標記-整理算法來實現的,但是局部的region上看又是基于標記-復制算法來的,相比cms來說,g1不會產生空間碎片且垃圾回收時間可控。但是相比cms也有一些缺點,比如G1為了垃圾收集產生的內存占用,以及程序運行時的額外負載,都要比cms要高。
低延遲收集器:shenandoan ,基于Region布局,號稱收集時間不超過10ms,但是目前尚未實現
ZGC: 基于標記-整理算法,開源給了OpenJDK,zgc出現在jdk11 上。
垃圾回收的 可視化處理工具
1.JDK自帶的JCONSOLE以及使用dump 來分析,實際的工作上大部分是使用arthas 來分析。這個章節沒有細看

浙公網安備 33010602011771號