清華掃地僧帶你深入JVM調優實戰,深入淺出的機制,不要太清晰 1.前言 2.JVM介紹 3.JVM的GC 4.總結

1.前言

Java作爲一種熱門開發語言,在行業知名度也可謂是“家喻戶曉”。在使用的道友,也是不計其數。所以,應該都聽說過一句經典語句:Write once,run anywhere。而爲什麼java能達到如此強大的存在呢,這離不開我們今天要複習的重點-java虛擬機(JVM)。

2.JVM介紹

2.1 JVM是什麼

JVM就是Java虛擬機(Java virtual machine)。JVM是JRE的一部分,它是一個虛構出來的計算機,是通過在實際的計算機上仿真模擬各種計算機功能來實現的。JVM有自己完善的硬件架構,如處理器、堆棧、寄存器等,還具有相應的指令系統。

2.2 JVM工作流程

咱們先看兩張解析圖:

咱們平時編寫java程序後,大體工作流程就如上圖。下面我們再進一步詳解,請看下圖:

簡單解釋一下上面這個流程圖:

程序運行時,java文件通過java編譯器轉譯成class文件

class文件通過類裝載器以及java類庫,裝載到JVM中

JVM通過解釋器,即時編譯器等將裝載進來的class文件進行編譯操作。如內存分配,運行處理等

最後JVM將相應操作與操作系統、硬件交互

2.3 JVM內部結構

下面說說JVM虛擬機這塊的內部結構,從圖入手:

JVM內部結構分爲三部分:類加載器(加載.class文件),執行引擎(執行字節碼或執行本地方法),數據區(包含PC寄存器,棧,堆,方法區以及本地方法棧)。下面我們對前兩者進行一個介紹,待會着重講講數據區。

2.3.1 類加載器

類的加載由類加載器完成,類加載器通常由JVM提供,這些類加載器也是前面所有程序運行的基礎,JVM提供的這些類加載器通常被稱爲系統類加載器。除此之外,開發者可以通過繼承ClassLoader基類來創建自己的類加載器。

通過流程圖看看類加載器做了哪些事:

①Bootstrap ClassLoader

負責加載$JAVA_HOME中jre/lib/rt.jar裏所有的class,由C++實現,不是ClassLoader子類

②Extension ClassLoader

負責加載java平臺中擴展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar或-Djava.ext.dirs指定目錄下的jar包

③App ClassLoader

負責記載classpath中指定的jar包及目錄中class

④Custom ClassLoader

屬於應用程序根據自身需要自定義的ClassLoader,如tomcat、jboss都會根據j2ee規範自行實現ClassLoader

加載過程中會先檢查類是否被已加載,檢查順序是自底向上,從Custom ClassLoader到BootStrap ClassLoader逐層檢查,只要某個classloader已加載就視爲已加載此類,保證此類只所有ClassLoader加載一次。而加載的順序是自頂向下,也就是由上層來逐層嘗試加載此類。

2.3.2 執行引擎

執行引擎是 Java 虛擬機最核心的組成部分之一。“虛擬機” 是一個相對於 “物理機” 的概念,這兩種機器都有代碼執行能力,其區別是物理機的執行引擎是直接建立在處理器、硬件、指令集和操作系統層面上的,而虛擬機的執行引擎則是由自己實現的,因此可以自行制定指令集與執行引擎的結構體系,並且能夠執行哪些不被硬件直接支持的指令集格式。

所謂的「虛擬機字節碼執行引擎」其實就是 JVM 根據 Class 文件中給出的字節碼指令,基於棧解釋器的一種執行機制。通俗點來說,也就是 JVM 解析字節碼指令,輸出運行結果的一個過程。

2.4 JVM的運行時數據區

先通過兩張結構圖,瞭解一下不同版本下的運行時數據區的結構!

1.8版本之後的結構圖如下:

1.8版本之前的結構圖如下:

JVM中,運行時數據區具體是什麼?帶着這個疑問,咱們通過下面這張圖詳細瞭解一下:

看似複雜,其實咱們大致先分爲幾個部分來理解。上圖可分爲兩大類型:共享區域非共享區域。圖中左側都是所有線程共享的區域,右側爲每個線程私有的區域。

2.4.1 共享區域

堆(heap)

所有類的實例就放在這個區域,爲所有線程共享。可以想象你的一個系統會產生很多實例,因此Java堆的空間也是最大的。如果Java堆空間不足了,程序會拋出OutOfMemoryError異常。

方法區

各個線程共享的區域,存放類信息、常量、靜態變量。

2.4.2 非共享區域

程序計數器

指向當前線程正在執行的字節碼的地址 和行號(指令都是在cpu上面運行的,分配到時間片纔去運行的,當多個線程的時候其他線程會被掛起,程序計數器就是記錄被掛起之前的字節碼執行到哪行 地址等,等到重新分配到了時間片然後再繼續執行)。它的作用就是控制程序指令的執行順序。

虛擬機棧

存儲當前線程運行方法所需要的數據,指令,返回的地址

每個線程創建的同時會創建一個JVM棧,JVM棧中每個棧幀存放的爲當前線程中局部基本類型的變量、部分的返回結果,非基本類型的對象在JVM棧上僅存放一個指向堆上的地址。

而每個棧幀中,又包含如上圖中的:局部變量表,操作數棧,動態鏈接,方法出口,具體作用如圖所解釋。

本地方法棧

本地方法棧是用來存儲本地方法相關的數據。本地方法就是帶有native標識符修飾的方法;

native修飾符修飾的方法並不提供方法體,但因爲其實現體是由非java代碼在在外部實現的,因此不能與abstract連用;

存在的意義:不方便用java語言寫的代碼,使用更爲專業的語言寫更合適;甚至有些JVM的實現就是用c編寫的,所以只能使用c來寫

2.4.3 JMM(java內存模型)

通過一張圖來認識JMM的內部結構:

JMM主要分爲新生代 ( Young ) 與老年代 ( Old ) ,二者比例的值爲 1:2 ( 該值可以通過參數 –XX:NewRatio 來指定 ),即:新生代 ( Young ) = 1/3 的堆空間大小,老年代 ( Old ) = 2/3 的堆空間大小。

其中,新生代 ( Young ) 被細分爲 Eden 和 兩個 Survivor 區域,這兩個 Survivor 區域分別被命名爲 from 和 to以示區分。 默認的,Edem : from : to = 8 : 1 : 1 ( 可以通過參數 –XX:SurvivorRatio 來設定 ),即: Eden = 8/10 的新生代空間大小,from = to = 1/10 的新生代空間大小。

JVM 每次只會使用 Eden 和其中的一塊 Survivor 區域來爲對象服務,所以無論什麼時候,總是有一塊 Survivor 區域是空閒着的。 因此,新生代實際可用的內存空間爲 9/10 ( 即90% )的新生代空間。新生代是 GC 收集垃圾的頻繁區域。

當對象在 Eden ( 包括一個 Survivor 區域,這裏假設是 from 區域 ) 出生後,在經過一次 Minor GC 後,如果對象還存活,並且能夠被另外一塊 Survivor 區域所容納 ( 上面已經假設爲 from 區域,這裏應爲 to 區域,即 to 區域有足夠的內存空間來存儲 Eden 和 from 區域中存活的對象 ),則使用複製算法將這些仍然還存活的對象複製到另外一塊 Survivor 區域 ( 即 to 區域 ) 中,然後清理所使用過的 Eden 以及 Survivor 區域 ( 即 from 區域 ),並且將這些對象的年齡設置爲1,以後對象在 Survivor 區每熬過一次 Minor GC,就將對象的年齡 + 1,當對象的年齡達到某個值時 ( 默認是 15 歲,可以通過參數 -XX:MaxTenuringThreshold 來設定 ),這些對象就會成爲老年代。但這也不是一定的,對於一些較大的對象 ( 即需要分配一塊較大的連續內存空間 ) 則是直接進入到老年代。

From Survivor區域與To Survivor區域是交替切換空間,在同一時間內兩者中只有一個不爲空。

永久代就是HotSpot虛擬機對虛擬機規範中方法區的一種實現方式。我們知道在HotSpot虛擬機中存在三種垃圾回收現象,minor GC、major GC和full GC。對新生代進行垃圾回收叫做minor GC,對老年代進行垃圾回收叫做major GC,同時對新生代、老年代和永久代進行垃圾回收叫做full GC。許多major GC是由minor GC觸發的,所以很難將這兩種垃圾回收區分開。major GC和full GC通常是等價的,收集整個GC堆。

在1.8之後已經取消了永久代,改爲元空間,類的元信息被存儲在元空間中。元空間沒有使用堆內存,而是與堆不相連的本地內存區域。所以,理論上系統可以使用的內存有多大,元空間就有多大,所以不會出現永久代存在時的內存溢出問題。這項改造也是有必要的,永久代的調優是很困難的,雖然可以設置永久代的大小,但是很難確定一個合適的大小,因爲其中的影響因素很多,比如類數量的多少、常量數量的多少等。永久代中的元數據的位置也會隨着一次full GC發生移動,比較消耗虛擬機性能。同時,HotSpot虛擬機的每種類型的垃圾回收器都需要特殊處理永久代中的元數據。將元數據從永久代剝離出來,不僅實現了對元空間的無縫管理,還可以簡化Full GC以及對以後的併發隔離類元數據等方面進行優化。

3.JVM的GC

上面簡單提到過,分別有三種回收現象:minor GC、major GC和full GC。

3.1 如何確定某個對象(垃圾)是可回收

3.1.1 引用計數法

給對象中添加一個引用計數器,每當有一個地方引用它時,計數器就加1;當引用失效時,計數器值就減1;任何時刻計數器都爲0的對象就是不可能再被使用的。

這種方式的問題是無法解決循環引用的問題,當兩個對象循環引用時,就算把兩個對象都設置爲null,因爲他們的引用計數都不爲0,這就會使他們永遠不會被清除。

3.1.2 根搜索算法(可達性分析/可達算法)

爲了解決引用計數法的循環引用問題,Java使用了可達性分析的方法。通過一系列的“GC roots”對象作爲起點搜索。如果在“GC roots”和一個對象之間沒有可達路徑,則稱該對象是不可達的。要注意的是,不可達對象不等價於可回收對象,不可達對象變爲可回收對象至少要經過兩次標記過程。兩次標記後仍然是可回收對象,則將面臨回收。

比較常見的將對象視爲可回收對象的原因:

顯式地將對象的唯一強引用指向新的對象。

顯式地將對象的唯一強引用賦值爲Null。

局部引用所指向的對象(如,方法內對象)。

只有弱引用與其關聯的對象。

3.2 幾種典型的垃圾回收算法

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

最基礎的垃圾回收算法,分爲“標註”和“清除”兩個階段:首先標記出所有需要回收的對象,在標記完成後統一回收掉所有被標記的對象。

標記過程:爲了能夠區分對象是live的,可以爲每個對象添加一個marked字段,該字段在對象創建的時候,默認值是false。

清除過程:去遍歷堆中所有對象,並找出未被mark的對象,進行回收。與此同時,那些被mark過的對象的marked字段的值會被重新設置爲false,以便下次的垃圾回收。

缺點:效率低,空間問題(產生大量不連續的內存碎片),後續可能發生大對象不能找到可利用空間的問題。

3.2.2 複製算法(Copying)

爲了解決Mark-Sweep算法內存碎片化的缺陷而被提出的算法。按內存容量將內存劃分爲大小相等的兩塊,每次只使用其中一塊。當這一塊內存滿後將尚存活的對象複製到另一塊上去,把已使用的內存空間一次清理掉。這種算法雖然實現簡單,內存效率高,不易產生碎片,但是最大的問題是可用內存被壓縮到了原本的一半。且存活對象增多的話,Copying算法的效率會大大降低。

3.2.3 標記-整理算法

標記-整理算法採用標記-清除算法一樣的方式進行對象的標記,但在清除時不同,在回收不存活的對象佔用的空間後,會將所有的存活對象往左端空閒空間移動,並更新對應的指針。標記-整理算法是在標記-清除算法的基礎上,又進行了對象的移動,因此成本更高,但是卻解決了內存碎片的問題。具體流程見下圖:

3.2.4 分代收集算法

分代收集算法是目前大部分JVM的垃圾收集器採用的算法。它的核心思想是根據對象存活的生命週期將內存劃分爲若干個不同的區域。一般情況下將堆區劃分爲老年代(Tenured Generation)和新生代(Young Generation),在堆區之外還有一個代就是永久代(Permanet Generation)。老年代的特點是每次垃圾收集時只有少量對象需要被回收,而新生代的特點是每次垃圾回收時都有大量的對象需要被回收,那麼就可以根據不同代的特點採取最適合的收集算法。

4.總結

java虛擬機的內容,本章就到此告一段落。只是小編個人的見解,如有哪裏不準確的地方,請大家多多評論指出。後續有時間,會相繼補充未完善的地方。

看完本章後,相信大家也能對JVM有了一定的認知與理解,不過,路漫漫,其修遠兮!繼續加油吧!在這裏再推薦一下清華掃地僧大佬整理的JVM調優實戰視頻,有需要深度獲取的朋友,請關注小編,並私信回覆【JVM】即可哦~~~

還在猶豫什麼,趕緊行動起來吧~~~

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章