想理解JVM看了這篇文章,就知道了!(一)

道阻且長啊發表於2020-07-25

前言

​ 本章節屬於Java進階系列,前面關於設計模式講解完了,有興趣的童鞋可以翻看之前的博文,後面會講解JVM的優化,整個系列會完整的講解整個java體系與生態相關的中介軟體知識。本次將對jvm有更深入的學習,我們不僅要讓程式能跑起來,而且是可以跑的更快!可以分析解決在生產環境中所遇到的各種“棘手”的問題,比如執行的應用卡住了,日誌不輸出,程式沒有反應,CPU負載突然升高,多執行緒應用下,如何分配執行緒數量等。

JVM介紹

什麼是JVM

​ 作為java工程師,對於jvm肯定不陌生。JVM是Java Virtual Machine的縮寫,通俗來說也就是執行java程式碼的容器。當專案啟動時,會根據jvm相關配置引數,在計算機的記憶體中開啟一片空間用於執行JVM。之後java相關程式碼就會被載入進JVM中執行。

百度百科對JVM的定義:

為什麼要了解JVM

​ 對於Java程式設計師來說,在虛擬機器自動記憶體管理機制的幫助下,不再需要為每一個new操作去寫配對的delete/free程式碼,不容易出現記憶體洩漏和記憶體溢位問題,看起來由虛擬機器管理記憶體一切都很美好。不過,也正是因為Java程式設計師把控制記憶體的權力交給了Java虛擬機器,一旦出現記憶體洩漏和溢位方面的問題,如果不瞭解虛擬機器是怎樣使用記憶體的,那排查錯誤、修正問題將會成為一項異常艱難的工作。

JVM記憶體模型

JVM整體架構

​ 由上面的圖可以看出,JVM虛擬機器中主要是由三部分構成,分別是類載入子系統、執行時資料區、執行引擎。
類載入子系統
​ Java虛擬機器把描述類的資料從Class檔案載入到記憶體,並對資料進行校驗、轉換解析和初始化,最終形成可以被虛擬機器直接使用的Java型別。
執行時資料區
​ Java虛擬機器在執行Java程式的過程中會把它所管理的記憶體劃分為若干個不同的資料區域。這些區域有各自的用途,以及建立和銷燬的時間,有的區域隨著虛擬機器程式的啟動而一直存在,有些區域則是依賴使用者執行緒的啟動和結束而建立和銷燬。
執行引擎
​ 執行引擎用於執行JVM位元組碼指令,主要有兩種方式,分別是解釋執行和編譯執行,區別在於,解釋執行是在執行時翻譯成虛擬機器指令執行,而編譯執行是在執行之前先進行編譯再執行。解釋執行啟動快,執行效率低。編譯執行,啟動慢,執行效率高。垃圾回收器就是自動管理執行資料區的記憶體,將無用的記憶體佔用進行清除,釋放記憶體資源。
本地方法庫、本地庫介面
​ 在jdk的底層中,有一些實現是需要呼叫本地方法完成的(使用c或c++寫的方法),就是通過本地庫介面呼叫完成的。比如:System.currentTimeMillis()方法。

執行時資料區

​ 執行時資料區是jvm中最為重要的部門。也是我們在調優時需要重點關注的區域,下面我們一起了解下這個部分的具體內容。

​ 根據《Java虛擬機器規範》中的規定,在執行時資料區將記憶體分為方法區(Method Area)、Java堆區(Java
Heap)、Java虛擬機器棧(Java Virtual Machine Stack)、程式計數器(Program Counter Register)、本地方法
棧(Native Method Stacks)。

程式計數器

​ 程式計數器(Program Counter Register)是一塊較小的記憶體空間,它可以看作是當前執行緒所執行的位元組碼的行號指示器。位元組碼直譯器工作時就是通過改變這個計數器的值來選取下一條需要執行的位元組碼指令,它是程式控制流的指示器,分支、迴圈、跳轉、異常處理、執行緒恢復等基礎功能都需要依賴這個計數器來完成。

​ 由於Java虛擬機器的多執行緒是通過執行緒輪流切換、分配處理器執行時間的方式來實現的,在任何一個確定的時刻,一個處理器(對於多核處理器來說是一個核心)都只會執行一條執行緒中的指令。因此,為了執行緒切換後能恢復到正確的執行位置,每條執行緒都需要有一個獨立的程式計數器,各條執行緒之間計數器互不影響,獨立儲存,我們稱這類記憶體區域為“執行緒私有”的記憶體。

java虛擬機器棧

​ 與程式計數器一樣,Java虛擬機器棧也是執行緒私有的,它的生命週期與執行緒相同。Java虛擬機器棧描述的是Java方法執行的執行緒記憶體模型:每個方法被執行的時候,Java虛擬機器都會同步建立一個棧幀,用於儲存區域性變數表、運算元棧、動態連線、方法出口等資訊。每一個方法被呼叫直至執行完畢的過程,就對應著一個棧幀在虛擬機器棧中從入棧到出棧的過程。

區域性變數表

  • 區域性變數表是一組變數值的儲存空間,用於存放方法引數和方法內部定義的區域性變數。
  • 在Class檔案中,方法的Code屬性的max_locals資料項中確定了該方法所需分配的區域性變數表的最大容量。
  • 該表以變數槽(Variable Slot)為最小單位,一個slot可以存放32位以內的資料,比如:boolean、byte、
    char、short、int、float等資料,如果儲存long、double型別資料,需要佔用2個solt。
  • 虛擬機器通過索引定位的方式使用區域性變數表,索引值的範圍是從0開始至區域性變數表最大的變數槽數量。
  • 如果訪問的是32位資料型別的變數,索引N就代表了使用第N個變數槽,如果訪問的是64位資料型別的變數,則說明會同時使用第N和N+1兩個變數槽。
  • 區域性變數表中第0位索引的變數槽預設是用於傳遞方法所屬物件例項的引用,在方法中可以通過關鍵字“this”來訪問到這個隱含的引數。其餘引數則按照參數列順序排列,佔用從1開始的區域性變數槽,參數列分配完畢後,再根據方法體內部定義的變數順序和作用域分配其餘的變數槽。

運算元棧

  • 運算元棧也常被稱為操作棧,它是一個先進後出棧。
  • 運算元棧的最大深度也在編譯的時候被寫入到Code屬性的max_stacks資料項之中。
  • 運算元棧的每一個元素都可以是包括long和double在內的任意Java資料型別。32位資料型別所佔的棧容量為1,64位資料型別所佔的棧容量為2。
  • 方法剛剛開始執行的時候,這個方法的運算元棧是空的,在方法的執行過程中,會有各種位元組碼指令往運算元棧中寫入和提取內容,也就是出棧和入棧操作。
  • 運算元棧中元素的資料型別必須與位元組碼指令的序列嚴格匹配,例如iadd指令,不能出現一個long和一個float使用iadd命令相加的情況。

動態連線

  • 每個棧幀都包含一個指向執行時常量池中該棧幀所屬方法的引用,持有這個引用是為了支援方法呼叫過程中
    的動態連線。
  • Class檔案的常量池中存有大量的符號引用,位元組碼中的方法呼叫指令就以常量池裡指向方法的符號引用作為
    引數。這些符號引用一部分會在類載入階段或者第一次使用的時候就被轉化為直接引用,這種轉化被稱為靜
    態解析。另外一部分將在每一次執行期間都轉化為直接引用,這部分就稱為動態連線。

方法出口

  • 當一個方法開始執行後,只有兩種方式退出這個方法。
  • 第一種方式是執行引擎遇到任意一個方法返回的位元組碼指令,這時候可能會有返回值傳遞給上層的方法呼叫
    者,方法是否有返回值以及返回值的型別將根據遇到何種方法返回指令來決定,這種退出方法的方式稱為“正
    常呼叫完成”。
  • 另外一種退出方式是在方法執行的過程中遇到了異常,並且這個異常沒有在方法體內得到妥善處理。無論是
    Java虛擬機器內部產生的異常,還是程式碼中使用throw位元組碼指令產生的異常,只要在本方法的異常表中沒有搜
    索到匹配的異常處理器,就會導致方法退出,這種退出方法的方式稱為“異常呼叫完成”。這種方法的返回是不
    會給它的上層呼叫者提供任何返回值的。
  • 無論採用何種退出方式,在方法退出之後,都必須返回到最初方法被呼叫時的位置,程式才能繼續執行,方
    法返回時可能需要在棧幀中儲存一些資訊,用來幫助恢復它的上層主調方法的執行狀態。
  • 方法退出的過程實際上等同於把當前棧幀出棧,因此退出時可能執行的操作有:恢復上層方法的區域性變數表
    和運算元棧,把返回值(如果有的話)壓入呼叫者棧幀的運算元棧中,調整PC計數器的值以指向方法呼叫指
    令後面的一條指令等。

圖解

​ 以 int i = 1; 這樣程式碼為例,看看虛擬機器棧的執行

本地方法棧

​ 本地方法棧(Native Method Stacks)與虛擬機器棧所發揮的作用是非常相似的,其區別只是虛擬機器棧為虛擬機器執行Java方法(也就是位元組碼)服務,而本地方法棧則是為虛擬機器使用到的本地(Native)方法服務。

Java堆區

​ Java堆是被所有執行緒共享的一塊記憶體區域,在虛擬機器啟動時建立。此記憶體區域的唯一目的就是存放物件例項,Java世界裡“幾乎”所有的物件例項都在這裡分配記憶體。
​ 需要注意的是,《Java虛擬機器規範》並沒有對堆進行細緻的劃分,所以對於堆的講解要基於具體的虛擬機器,我們以使用最多的HotSpot虛擬機器為例進行講解。
​ Java堆是垃圾收集器管理的記憶體區域,因此它也被稱作“GC堆”,這就是我們做JVM調優的重點區域部分。

jdk1.7中堆記憶體的劃分

  • Young 年輕區(代)
    Young區被劃分為三部分,Eden區和兩個大小嚴格相同的Survivor區,其中,Survivor區間中,某一時刻只有其中一個是被使用的,另外一個留做垃圾收集時複製物件用,在Eden區間變滿的時候,GC就會將存活的物件移到空閒的Survivor區間中,根據JVM的策略,在經過幾次垃圾收集後,任然存活於Survivor的物件將被移動到Tenured區間。
  • Tenured 年老區
    Tenured區主要儲存生命週期長的物件,一般是一些老的物件,當一些物件在Young複製轉移一定的次數以後,物件就會被轉移到Tenured區,一般如果系統中用了application級別的快取,快取中的物件往往會被轉移到這一區間。
  • Perm 永久區
    Perm代主要儲存class,method,filed物件,這部份的空間一般不會溢位,除非一次性載入了很多的類,不過在涉及到熱部署的應用伺服器的時候,有時候會遇到java.lang. OutOfMemoryError : PermGen space 的誤,造成這個錯誤的很大原因就有可能是每次都重新部署,但是重新部署後,類的class沒有被解除安裝掉,這樣就造成了大量的class物件儲存在了perm中,這種情況下,一般重新啟動應用伺服器可以解決問題。
  • Virtual區:
    最大記憶體和初始記憶體的差值,就是Virtual區。

jdk1.8中堆記憶體的劃分

由上圖可以看出,jdk1.8的記憶體模型是由2部分組成,年輕代+ 年老代。
年輕代:Eden + 2*Survivor
年老代:OldGen
在jdk1.8中變化最大的Perm區,用Metaspace(後設資料空間)進行了替換。
需要特別說明的是:Metaspace所佔用的記憶體空間不是在虛擬機器內部,而是在本地記憶體空間中,這也是與1.7的永
久代最大的區別所在。

空間分配

如果沒有指定堆記憶體大小,預設初始堆記憶體為實體記憶體的1/64,最大不超過實體記憶體的1/4或1G。注意的是元空間會自動擴容,預設情況下不收限制。

為什麼廢棄1.7中的永久區

官方給出的解釋是:移除永久代是為融合HotSpot JVM與 JRockit VM而做出的努力,因為JRockit沒有永久代,不需要配置永久代。

方法區

  • 方法區(Method Area)與Java堆一樣,是各個執行緒共享的記憶體區域,它用於儲存已被虛擬機器載入的類資訊、
    常量、靜態變數、即時編譯器編譯後的程式碼快取等資料。
  • 《Java虛擬機器規範》中把方法區描述為堆的一個邏輯部分,它卻有一個別名叫作“非堆”(Non-Heap),目的
    是與Java堆區分開來。
  • JDK8之前將HotSpot虛擬機器把收集器的分代設計擴充套件至方法區,所以可以將永久代看做是方法區,JDK8之後
    廢棄永久代,用元空間來代替。

物件的訪問

  • Java程式會通過棧上的reference資料來操作堆上的具體物件。

  • 主流的訪問方式主要有使用控制程式碼和直接指標兩種:

  • 控制程式碼訪問
    Java堆中將可能會劃分出一塊記憶體來作為控制程式碼池,reference中儲存的就是物件的控制程式碼地址,而控制程式碼中包含了物件例項資料與型別資料各自具體的地址資訊.使用直接指標訪問Java堆中物件的記憶體佈局就必須考慮如何放置訪問型別資料的相關資訊,reference中儲存的直接就是物件地址,如果只是訪問物件本身的話,就不需要多一次間接訪問的開銷

  • 指標訪問

    使用控制程式碼來訪問的最大好處就是reference中儲存的是穩定控制程式碼地址,在物件被移動(垃圾收集時移動物件是非常普遍的行為)時只會改變控制程式碼中的例項資料指標,而reference本身不需要被修改。使用直接指標來訪問最大的好處就是速度更快,它節省了一次指標定位的時間開銷。HotSpot虛擬機器採用的是指標訪問方式實現。

相關文章