1. 程式人生 > 其它 >深入理解JVM

深入理解JVM

一 JVM概述

JVM全稱Java Virtual Machine,即Java虛擬機器。它本身是一個虛擬計算機。Java虛擬機器基於二進位制位元組碼執行,由一套位元組碼指令集、一組暫存器、一個棧、一個垃圾回收堆、一個方法區等組成。JVM遮蔽了與作業系統平臺相關的資訊,從而能夠讓Java程式只需要生成能夠在JVM上執行的位元組碼檔案。通過該機制實現的跨平臺性。

JVM的生命週期分為三個階段,分別為:啟動、執行、死亡。

啟動:當啟動一個Java程式時,JVM的例項就已經產生。對於擁有main函式的類就是JVM例項執行的起點。

執行:main()方法是一個程式的初始起點,任何執行緒均可由在此處啟動。在JVM內部有兩種執行緒型別,分別為:使用者執行緒和守護執行緒。JVM通常使用的是守護執行緒,而main()使用的則是使用者執行緒。守護執行緒會隨著使用者執行緒的結束而結束。

死亡:當程式中的使用者執行緒都中止,JVM才會退出。

二 記憶體結構

虛擬機器棧:

執行緒私有的,虛擬機器棧對應方法呼叫到執行完成的整個過程。儲存執行方法時的區域性變數、動態連線資訊、方法返回地址資訊等等。方法開始執行的時候會進棧,方法執行完會出棧【相當於清空了資料】。不需要進行GC。

本地方法棧:

與虛擬機器棧類似。本地方法棧是為虛擬機器執行本地方法時提供服務的。不需要進行GC。本地方法一般是由其他語言編寫。

程式計數器:

執行緒私有的。內部儲存的位元組碼的行號。用於記錄正在執行的位元組碼指令的地址。

ava虛擬機器對於多執行緒是通過執行緒輪流切換並且分配執行緒執行時間。在任何的一個時間點上,一個處理器只會處理執行一個執行緒,如果當前被執行的這個執行緒它所分配的執行時間用完了【掛起】。處理器會切換到另外的一個執行緒上來進行執行。並且這個執行緒的執行時間用完了,接著處理器就會又來執行被掛起的這個執行緒。

那麼現在有一個問題就是,當前處理器如何能夠知道,對於這個被掛起的執行緒,它上一次執行到了哪裡?那麼這時就需要從程式計數器中來回去到當前的這個執行緒他上一次執行的行號,然後接著繼續向下執行。

程式計數器是JVM規範中唯一一個沒有規定出現OOM的區域,所以這個空間也不會進行GC。

本地記憶體:

它又叫做堆外記憶體,執行緒共享的區域,本地記憶體這塊區域是不會受到JVM的控制的,也就是說對於這塊區域是不會發生GC的。因此對於整個java的執行效率是提升非常大的。

堆:

執行緒共享的區域。主要用來儲存物件例項,陣列等,當堆中沒有記憶體空間可分配給例項,也無法再擴充套件時,則丟擲OutOfMemoryError異常。

在JAVA7中堆內會存在年輕代、老年代和方法區(永久代)

1)Young區被劃分為三部分,Eden區和兩個大小嚴格相同的Survivor區,其中,Survivor區間中,某一時刻只有其中一個是被使用的,另外一個留做垃圾收集時複製物件用。在Eden區變滿的時候, GC就會將存活的物件移到空閒的Survivor區間中,根據JVM的策略,在經過幾次垃圾收集後,任然存活於Survivor的物件將被移動到Tenured區間。

2)Tenured區主要儲存生命週期長的物件,一般是一些老的物件,當一些物件在Young複製轉移一定的次數以後,物件就會被轉移到Tenured區。

3)Perm代主要儲存儲存的類資訊、靜態變數、常量、編譯後的程式碼,在java7中堆上方法區會受到GC的管理的。方法區【永久代】是有一個大小的限制的。如果大量的動態生成類,就會放入到方法區【永久代】,很容易造成OOM。

為了避免方法區出現OOM,所以在java8中將堆上的方法區【永久代】給移動到了本地記憶體上,重新開闢了一塊空間,叫做元空間。那麼現在就可以避免掉OOM的出現了。

元空間的本質和永久代類似,都是對 JVM 規範中方法區的實現。不過元空間與永久代之間最大的區別在於:元空間並不在虛擬機器中,而是使用本地記憶體。因此,預設情況下,元空間的大小僅受本地記憶體限制。

三 類載入器

1Java檔案編譯執行的過程

類載入器:用於裝載位元組碼檔案(.class檔案)

執行時資料區:用於分配儲存空間

執行引擎:執行位元組碼檔案或本地方法

垃圾回收器:用於對JVM中的垃圾內容進行回收

2類載入器介紹

類載入器是java.lang.ClassLoader類的子類物件或者C++程式碼編寫的Bootstrap ClassLoader,它們的作用是載入位元組碼到JVM記憶體,得到Class類的物件。

類載入器根據各自載入範圍的不同,劃分為四種類載入器:

啟動類載入器(BootStrap ClassLoader):該類並不繼承ClassLoader類,其是由C++編寫實現。用於載入JAVA_HOME/jre/lib目錄下的類庫。

擴充套件類載入器(ExtClassLoader):該類是ClassLoader的子類,主要載入JAVA_HOME/jre/lib/ext目錄中的類庫。

應用類載入器(AppClassLoader):該類是ClassLoader的子類,主要用於載入classPath下的類,也就是載入開發者自己編寫的Java類。

自定義類載入器:開發者自定義類繼承ClassLoader,實現自定義類載入規則。

類載入器的體系並不是“繼承”體系,而是委派體系,類載入器首先會到自己的parent中查詢類或者資源,如果找不到才會到自己本地查詢。類載入器的委託行為動機是為了避免相同的類被載入多次。

3類載入模型

在JVM中,對於類載入模型提供了三種,分別為全盤載入、雙親委派、快取機制。

全盤載入:

即當一個類載入器負責載入某個Class時,該Class所依賴和引用的其他Class也將由該類載入器負責載入,除非顯示指定使用另外一個類載入器來載入。

雙親委派:

即先讓父類載入器試圖載入該Class,只有在父類載入器無法載入該類時才嘗試從自己的類路徑中載入該類。簡單來說就是,某個特定的類載入器在接到載入類的請求時,首先將載入任務委託給父載入器,依次遞迴,如果父載入器可以完成類載入任務,就成功返回;只有父載入器無法完成此載入任務時,才自己去載入。

1)通過雙親委派機制可以避免某一個類被重複載入,當父類已經載入後則無需重複載入,保證唯一性。

2)為了安全,保證類庫API不會被修改

快取機制:

會保證所有載入過的Class都會被快取,當程式中需要使用某個Class時,類載入器先從快取區中搜尋該Class,只有當快取區中不存在該Class物件時,系統才會讀取該類對應的二進位制資料,並將其轉換成Class物件,存入緩衝區中。從而可以理解為什麼修改了Class後,必須重新啟動JVM,程式所做的修改才會生效的原因。

垃圾回收機制

1Java語言的垃圾回收

為了讓程式設計師更專注於程式碼的實現,而不用過多的考慮記憶體釋放的問題,所以,在Java語言中,有了自動的垃圾回收機制,也就是我們熟悉的GC。

有了垃圾回收機制後,程式設計師只需要關心記憶體的申請即可,記憶體的釋放由系統自動識別完成。

在進行垃圾回收時,不同的物件引用型別,GC會採用不同的回收時機,對於物件的引用型別可檢視第三天的ThreadLocal部分。

換句話說,自動的垃圾回收的演算法就會變得非常重要了,如果因為演算法的不合理,導致記憶體資源一直沒有釋放,同樣也可能會導致記憶體溢位的。

2垃圾&垃圾定位

如果一個或多個物件沒有任何的引用指向它了,那麼這個物件現在就是垃圾。

2.1 引用計數法

一個物件被引用了一次,在當前的物件頭上遞增一次引用次數,如果這個物件的引用次數為0,代表這個物件可回收,當物件間出現了迴圈引用的話,則引用計數法就會失效

優點:

實時性較高,無需等到記憶體不夠的時候,才開始回收,執行時根據物件的計數器是否為0,就可以直接回收。

在垃圾回收過程中,應用無需掛起。如果申請記憶體時,記憶體不足,則立刻報OOM錯誤。

區域性,更新物件的計數器時,只是影響到該物件,不會掃描全部物件。

缺點:

每次物件被引用時,都需要去更新計數器,有一點時間開銷。

浪費CPU資源,即使記憶體夠用,仍然在執行時進行計數器的統計。

無法解決迴圈引用問題,會引發記憶體洩露。(最大的缺點)

2.2 可達性分析演算法

現在的虛擬機器採用的都是通過可達性分析演算法來確定哪些內容是垃圾。

會存在一個根節點【GC Roots】,引出它下面指向的下一個節點,再以下一個節點節點開始找出它下面的節點,依次往下類推。直到所有的節點全部遍歷完畢。

M,N這兩個節點是可回收的,但是並不會馬上的被回收!! 物件中存在一個方法【finalize】。當物件被標記為可回收後,當發生GC時,首先會判斷這個物件是否執行了finalize方法,如果這個方法還沒有被執行的話,那麼就會先來執行這個方法,接著在這個方法執行中,可以設定當前這個物件與GC ROOTS產生關聯,那麼這個方法執行完成之後,GC會再次判斷物件是否可達,如果仍然不可達,則會進行回收,如果可達了,則不會進行回收。

finalize方法對於每一個物件來說,只會執行一次。如果第一次執行這個方法的時候,設定了當前物件與RC ROOTS關聯,那麼這一次不會進行回收。 那麼等到這個物件第二次被標記為可回收時,那麼該物件的finalize方法就不會再次執行了。

GC ROOTS:

虛擬機器棧中引用的物件

本地方法棧中引用的物件

方法區中類靜態屬性引用的物件

方法區中常量引用物件

3垃圾回收演算法

3.1 標記清除演算法

標記清除演算法,是將垃圾回收分為2個階段,分別是標記和清除

1.根據可達性分析演算法得出的垃圾進行標記

2.對這些標記為可回收的內容進行垃圾回收

標記清除演算法解決了引用計數演算法中的迴圈引用的問題,沒有從root節點引用的物件都會被回收。

同樣,標記清除演算法也是有缺點的:

  效率較低,標記和清除兩個動作都需要遍歷所有的物件,並且在GC時,需要停止應用程式,對於互動性要求比較高的應用而言這個體驗是非常差的。

   重要)通過標記清除演算法清理出來的記憶體,碎片化較為嚴重,因為被回收的物件可能存在於記憶體的各個角落,所以清理出來的記憶體是不連貫的。

3.2 複製演算法

複製演算法的核心就是,將原有的記憶體空間一分為二,每次只用其中的一塊,在垃圾回收時,將正在使用的物件複製到另一個記憶體空間中,然後將該記憶體空間清空,交換兩個記憶體的角色,完成垃圾的回收。

如果記憶體中的垃圾物件較多,需要複製的物件就較少,這種情況下適合使用該方式並且效率比較高,反之,則不適合。

1)將記憶體區域分成兩部分,每次操作其中一個。

2)當進行垃圾回收時,將正在使用的記憶體區域中的存活物件移動到未使用的記憶體區域。當移動完對這部分記憶體區域一次性清除。

3)周而復始。

優點:

在垃圾物件多的情況下,效率較高

清理後,記憶體無碎片

缺點:

分配的2塊記憶體空間,在同一個時刻,只能使用一半,記憶體使用率較低

3.3 標記整理演算法

標記壓縮演算法是在標記清除演算法的基礎之上,做了優化改進的演算法。和標記清除演算法一樣,也是從根節點開始,對物件的引用進行標記,在清理階段,並不是簡單的直接清理可回收物件,而是將存活物件都向記憶體另一端移動,然後清理邊界以外的垃圾,從而解決了碎片化的問題。

1)標記垃圾。

2)需要清除向右邊走,不需要清除的向左邊走。

3)清除邊界以外的垃圾。

優缺點同標記清除演算法,解決了標記清除演算法的碎片化的問題,同時,標記壓縮演算法多了一步,物件移動記憶體位置的步驟,其效率也有有一定的影響。

3.4 分代收集演算法

在java8時,堆被分為了兩份:新生代和老年代【1:2】,在java7時,還存在一個永久代。

對於新生代,內部又被分為了三個區域。Eden區,S0區,S1區【8:1:1】

當對新生代產生GC:MinorGC【young GC】

當對老年代產生GC:FullGC【OldGC】

3.4.1 工作機制

1)當建立一個物件的時候,那麼這個物件會被分配在新生代的Eden區。當Eden區要滿了時候,觸發YoungGC。

2)當進行YoungGC後,此時在Eden區存活的物件被移動到S0區,並且當前物件的年齡會加1,清空Eden區。

3)當再一次觸發YoungGC的時候,會把Eden區中存活下來的物件和S0中的物件,移動到S1區中,這些物件的年齡會加1,清空Eden區和S0區。

4)當再一次觸發YoungGC的時候,會把Eden區中存活下來的物件和S1中的物件,移動到S0區中,這些物件的年齡會加1,清空Eden區和S1區。

3.4.2 物件何時晉升到老年代

1)物件的年齡達到了某一個限定的值(預設15歲,CMS預設6歲 ),那麼這個物件就會進入到老年代中。

2)大物件。

3)如果在Survivor區中相同年齡的物件的所有大小之和超過Survivor空間的一半,年齡大於或等於該年齡的物件就可以直接進入老年代。

當老年代滿了之後,觸發FullGCFullGC同時回收新生代和老年代,當前只會存在一個FullGC的執行緒進行執行,其他的執行緒全部會被掛起。

4 七種垃圾收集器

在jvm中,實現了多種垃圾收集器,包括:序列垃圾收集器、並行垃圾收集器、CMS(併發)垃圾收集器、G1垃圾收集器

4.1 Serial收集器

序列垃圾收集器,作用於新生代。是指使用單執行緒進行垃圾回收,採用複製演算法。垃圾回收時,只有一個執行緒在工作,並且java應用中的所有執行緒都要暫停,等待垃圾回收的完成。這種現象稱之為STW(Stop-The-World)。其應用在年輕代

對於互動性較強的應用而言,這種垃圾收集器是不能夠接受的。因此一般在Javaweb應用中是不會採用該收集器的。

4.2 ParallelNew收集器

並行垃圾收集器在序列垃圾收集器的基礎之上做了改進,採用複製演算法。將單執行緒改為了多執行緒進行垃圾回收,這樣可以縮短垃圾回收的時間。(這裡是指,並行能力較強的機器)。但是對於其他的行為(收集演算法、stop the world、物件分配規則、回收策略等)同Serial收集器一樣。其也是應用在年輕代。JDK8預設使用此垃圾回收器

當然了,並行垃圾收集器在收集的過程中也會暫停應用程式,這個和序列垃圾回收器是一樣的,只是並行執行,速度更快些,暫停的時間更短一些。

4.3 Parallel Scavenge收集器

其是一個應用於新生代並行垃圾回收器,採用複製演算法。它的目標是達到一個可控的吞吐量(吞吐量=執行使用者程式碼時間 /(執行使用者程式碼時間+垃圾收集時間))即虛擬機器總共運行了100分鐘,其中垃圾收集花掉1分鐘,吞吐量就是99%。這樣可以高效率的利用CPU時間,儘快完成程式的運算任務,適合在後臺運算而不需要太多互動的任務。

  • 停頓時間越短對於需要與使用者互動的程式來說越好,良好的響應速度能提升使用者的體驗。

  • 高吞吐量可以最高效率地利用CPU時間,儘快地完成程式的運算任務,主要適合在後臺運算而不太需要太多互動的任務。

4.4 Serial Old收集器

其是運行於老年代的單執行緒Serial收集器,採用標記-整理演算法,主要是給Client模式下的虛擬機器使用。

4.5 Parallel Old收集器

其是一個應用於老年代的並行垃圾回收器,採用標記-整理演算法。在注重吞吐量及CPU資源敏感的場合,都可以優先考慮Parallel Scavenge+Parallel Old收集器。

4.6 CMS垃圾收集器

4.6.1 概述

CMS全稱 Concurrent Mark Sweep,是一款併發的、使用標記-清除演算法的垃圾回收器,該回收器是針對老年代垃圾回收的,是一款以獲取最短回收停頓時間為目標的收集器,停頓時間短,使用者體驗就好。其最大特點是在進行垃圾回收時,應用仍然能正常執行。

CMS垃圾回收器的執行過程如下:

1)初始標記(Initial Mark):僅僅標記GC Roots能直接關聯到的物件,速度快,但是需要“Stop The World”

2)併發標記(Concurrent Mark):就是進行追蹤引用鏈的過程,可以和使用者執行緒併發執行。

3)重新標記(Remark):修正併發標記階段因使用者執行緒繼續執行而導致標記發生變化的那部分物件的標記記錄,比初始標記時間長但遠比並發標記時間短,需要“Stop The World”

4)併發清除(Concurrent Sweep):清除標記為可以回收物件,可以和使用者執行緒併發執行

由於整個過程耗時最長的併發標記和併發清除都可以和使用者執行緒一起工作,所以總體上來看,CMS收集器的記憶體回收過程和使用者執行緒是併發執行的。

4.6.2 CMS收集器缺點

對於CMS收集器的有三個:

  • 對CPU資源敏感:

併發收集雖然不會暫停使用者執行緒,但因為佔用CPU資源,仍會導致系統吞吐量降低、響應變慢。

CMS的預設收集執行緒數量是=(CPU數量+3)/4。當CPU數量多於4個,收集執行緒佔用的CPU資源多於25%,對使用者程式影響可能較大;不足4個時,影響更大,可能無法接受。

  • 無法處理浮動垃圾:

所謂浮動垃圾即在併發清除時,使用者執行緒新產生的垃圾叫浮動垃圾。併發清除時需要預留一定的記憶體空間,不能像其他收集器在老年代幾乎填滿再進行收集。如果CMS預留記憶體空間無法滿足程式需要,就會出現一次"Concurrent Mode Failure"失敗。這時JVM啟用後備預案:臨時啟用Serail Old收集器,而導致另一次Full GC的產生。

  • 垃圾回收演算法導致記憶體碎片:

因為CMS收集器採用標記-清除演算法,因此會導致垃圾從記憶體中被清除後,會出現記憶體空間碎片化。這樣會導致分配大記憶體物件時,無法找到足夠的連續記憶體,從而需要提前觸發另一次Full GC動作。

4.7 G1垃圾收集器

4.7.1 概述

對於垃圾回收器來說,前面的三種要麼一次性回收年輕代,要麼一次性回收老年代。而且現代伺服器的堆空間已經可以很大了。為了更加優化GC操作,所以出現了G1。

它是一款同時應用於新生代和老年代、採用標記-整理演算法、軟實時、低延遲、可設定目標(最大STW停頓時間)的垃圾回收器,用於代替CMS,適用於較大的堆(>4~6G),在JDK9之後預設使用G1

G1的設計原則就是簡化JVM效能調優,開發人員只需要簡單的三步即可完成調優:

  1. 第一步,開啟G1垃圾收集器

  2. 第二步,設定堆的最大記憶體

  3. 第三步,設定最大的停頓時間(stw)

4.7.2 G1的記憶體佈局

G1垃圾收集器相對比其他收集器而言,最大的區別在於它取消了年輕代、老年代的物理劃分

取而代之的是將堆劃分為若干個區域(Region),這些區域中包含了有邏輯上的年輕代、老年代區域。這樣做的好處就是,我們再也不用單獨的空間對每個代進行設定了,不用擔心每個代記憶體是否足夠。

此時可以看到,現在出現了一個新的區域Humongous,它本身屬於老年代區。當現在出現了一個巨大的物件,超出了分割槽容量的一半,則這個物件會進入到該區域。如果一個H區裝不下一個巨型物件,那麼G1會尋找連續的H分割槽來儲存。為了能找到連續的H區 ,有時候不得不啟動Full GC。

同時G1會估計每個Region中的垃圾比例,優先回收垃圾較多的區域。

在G1劃分的區域中,年輕代的垃圾收集依然採用暫停所有應用執行緒的方式,將存活物件拷貝到老年代或者Survivor空間,G1收集器通過將物件從一個區域複製到另外一個區域,完成了清理工作

這就意味著,在正常的處理過程中,G1完成了堆的壓縮(至少是部分堆的壓縮),這樣也就不會有cms記憶體碎片問題的存在了。

4.7.3 垃圾回收模式

其提供了三種模式垃圾回收模式: young GC、Mixed GC、Full GC。在不同的條件下被觸發。

4.7.3.1 Young GC

發生在年輕代的GC演算法,一般物件(除了巨型物件)都是在eden region中分配記憶體,當所有eden region被耗盡無法申請記憶體時,就會觸發一次young gc,這種觸發機制和之前的young gc差不多,執行完一次young gc,活躍物件會被拷貝到survivor region或者晉升到old region中,空閒的region會被放入空閒列表中,等待下次被使用。

4.7.3.2 Mixed GC

當越來越多的物件晉升到老年代old region時,為了避免堆記憶體被耗盡,虛擬機器會觸發一個混合的垃圾收集器,即mixed gc,該演算法並不是一個old gc,除了回收整個young region,還會回收一部分的old region,這裡需要注意:是一部分老年代,而不是全部老年代,可以選擇哪些old region進行收集,從而可以對垃圾回收的耗時時間進行控制。

在CMS中,當老年代的使用率達到80%就會觸發一次cms gc。在G1中,mixed gc也可以通過-XX:InitiatingHeapOccupancyPercent設定閾值,預設為45%。當老年代大小佔整個堆大小百分比達到該閾值,則觸發mixed gc。

其執行過程和cms類似:

  1. initial mark: 初始標記過程,整個過程STW,標記了從GC Root可達的物件。

  2. concurrent marking: 併發標記過程,整個過程gc collector執行緒與應用執行緒可以並行執行,標記出GC Root可達物件衍生出去的存活物件,並收集各個Region的存活物件資訊。

  3. remark: 最終標記過程,整個過程STW,標記出那些在併發標記過程中遺漏的,或者內部引用發生變化的物件。

  4. clean up: 垃圾清除過程,如果發現一個Region中沒有存活物件,則把該Region加入到空閒列表中。

4.7.3.3 Full GC

如果物件記憶體分配速度過快,mixed gc來不及回收,導致老年代被填滿,就會觸發一次full gc,G1的full gc演算法就是單執行緒執行的serial old gc,會導致異常長時間的暫停時間,需要進行不斷的調優,儘可能的避免full gc.

4.7.5 G1的最佳實踐

不斷調優暫停時間指標

通過XX:MaxGCPauseMillis=x可以設定啟動應用程式暫停的時間,G1在執行的時候會根據這個引數選擇CSet來滿足響應時間的設定。一般情況下這個值設定到100ms或者200ms都是可以的(不同情況下會不一樣),但如果設定成50ms就不太合理。暫停時間設定的太短,就會導致出現G1跟不上垃圾產生的速度。最終退化成Full GC。所以對這個引數的調優是一個持續的過程,逐步調整到最佳狀態。

不要設定新生代和老年代的大小

G1收集器在執行的時候會調整新生代和老年代的大小。通過改變代的大小來調整物件晉升的速度以及晉升年齡,從而達到我們為收集器設定的暫停時間目標。設定了新生代大小相當於放棄了G1為我們做的自動調優。我們需要做的只是設定整個堆記憶體的大小,剩下的交給G1自己去分配各個代的大小。