一文入門jvm虛擬機

一文帶你理解JVM

1、jdk、jre、jvm的區別與聯繫

jdk的全稱是Java Development kit(java開發工具包),我們可以把程序設計語言、java虛擬機、java類庫這三部分統稱爲jdk,jdk是用於支持java程序開發的最小環境。Developer可以很容易的使用裏面的方法以減少代碼量,裏面同時包含jre和一些開發的小工具(如編譯工具javac),同時包含了jre。

jre的全稱是Java Running Environment(java運行時環境 ),可以把java類庫API中的javaSE的API子集和java虛擬機這兩部分統稱爲JRE,JRE是支持java程序運行的標準環境。

jvm的全稱java virtual machine(java 虛擬機),它只認識XXX.class文件,虛擬機可以識別這種文件的字節碼指令並調用操作系統上的API,正是這個原因,java纔可以跨平臺使用。

2、代碼是如何執行的

jvm是一個軟件,它幫我們屏蔽了底層的操作系統、硬件、CPU指令層的細節

它的口號是Write Once,Run Everywhere.我們來看一下代碼的執行流程。

圖中的Test.java文件是按照java語法規則編寫的源文件,是一種高級語言,.java文件經javac編譯後就生成字節碼文件,字節碼文件是用於給java虛擬機執行用的,該文件的格式規範受到java虛擬機的定義。而jvm的目的就是將字節碼文件Test.class翻譯爲操作系統及硬件的指令,便於在不同的操作系統上執行。

NOTE:jvm虛擬機並不是僅僅只針對java語言,像一些其它編程語言如Groovy、Scala和Kotlin也可以在jvm虛擬機上運行上,這些語言僅僅需要實現一個編譯器,通過該編譯器把源代碼文件編譯成JVM能識別的字節碼文件即可。

3、JVM的內存結構

3.1類加載子系統

在java虛擬機中,負責查找並裝載類的部分稱爲裝載子系統,裝載子系統用於定位和加載譯碼後的class文件。在加載階段,虛擬機需要完成以下事情

  • 通過一個類的全限定名來獲取定義此類的二進制字節流

  • 將這個字節流所代表的靜態存儲結構轉化爲元空間中運行時的數據結構

  • 在內存中生成一個代表這個類的java.lang.Class對象,作爲方法這個類的各種數據訪問入口

加載過程下圖所示

類的加載是通過查詢路徑的方式進行的,加載階段既可以使用虛擬機裏內置的引導類加載器來完成,也可以由用戶自定義類加載器來完成。其加載順序如下

1、Bootstrap ClassLoader:啓動類加載器,加載存放在<JAVA_HOME>\lib目錄,或者被Xbootclasspath選項指定的jar包,如rt.jar、tools.jar

2、Extension ClassLoader:擴展類加載器,加載<JAVA_HOME>\lib\ext*.jar或者-java.ext.dirs指定目錄下的jar包

3、AppClassLoader:應用程序類加載器,加載Classpath或java.class.path所指定的目錄下的類和jar包

4、Custom ClassLoader:通過java.lang.ClassLoader的子類自定義加載class

實際上,上面描述的僅僅是類加載過程中的加載過程,類加載的整個過程包括:加載、驗證、準備、解析和初始化

字節碼--->加載--->驗證--->準備--->解析--->初始化,其中驗證、準備和解析階段可以統稱爲鏈接階段。

下面我們講解每個階段的作用

  • 驗證:驗證是鏈接階段的第一步,這一階段的目的是確保Class文件的字節流包含的信息符合《java虛擬機規範》的全部約束要求,確保這些信息被當做代碼運行後不會危害虛擬機自身的安全

  • 準備:正式爲類中定義的變量(靜態變量)分配內存並設置類變量初始值階段。

  • 解析:java虛擬機將常量池(元數據區的一部分)內的符號引用替換爲直接引用過程

  • 初始化:類的初始化是類加載過程的最後一步,它的作用是真正開始執行類中編寫的java程序代碼

類加載會將類的信息加入到元數據空間。

如果一個類型從被加載到虛擬機內存開始,到出卸載爲止,它的整個生命週期將在類加載的基礎上增加使用和卸載階段

3.2jvm內存部分(運行時數據區)

jvm在運行時會把它所管理的內存劃分爲若干不同的數據區域,宏觀上可以劃分爲兩部分

1、線程私有數據區(3個部分)

  • 程序計數器

①程序計數器是一塊內存較小的空間,它可以看做是當前線程執行的字節碼的行號指示器

②它是程序控制流的指示器,分支、循環、跳轉、異常處理、多線程恢復等基礎功能都需要依賴這個計數器來完成

③線程私有,各條線程之間計數器互不影響,獨立存儲,

④隨着線程的結束而結束,不需要垃圾回收

⑤不會出現OutOfMemoryError

  • 虛擬機棧

與程序計數器一樣,java虛擬機棧也是線程私有的,不會被GC回收,它的生命週期與線程相同,java虛擬機棧描述的是java方法執行線程的內存模型:每個方法被執行的時候(一個方法對應着一個棧幀),java虛擬機棧都會同步創建一個棧幀用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。當棧的深度大於虛擬機所允許的深度,將拋出StackOverflowError異常,如果java虛擬機的容量可以動態擴展,當棧擴展時無法申請到足夠的內存時將會拋出OutOfMemoryError異常。棧裏面運行方法,存放方法的局部變量名,變量名所指向的值(常量值、對象值等)都存放在堆上。每一個方法被調用直至執行完畢的過程,就對應着一個棧幀在虛擬機棧從入棧到出棧的過程。

①局部變量表:是一組變量的存儲空間,用於存放方法的參數和方法內部定義的局部變量,局部變量分爲兩種,分別是基本數據類型和引用類型(引用類型指向堆中對象的地址),常量值指向元空間。

②操作棧:操作棧也被稱爲操作數棧,它是一個後入先出的棧。當一個方法剛開始執行的時候,這個方法的操作數棧是空的,在方法的執行過程中,會有各種字節碼指令往操作數棧中寫入和提取內容,也就是出棧和入棧操作,一個完整的方法執行期間往往包含多個這樣入棧和出棧的過程。操作數棧中元素的數據類型必須與字節碼指令的序列嚴格匹配。

③動態鏈接:每個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程中的動態鏈接。一個方法要調用其它方法,需要將這些方法的符號引用轉化爲其內存地址的直接引用,而符號引用存在於方法區中的運行時常量池,所有需要在運行時動態的將這些符號引用轉化爲直接引用。

④返回地址:方法不管是正常執行結束還是異常退出,需要返回方法被調用的位置。

  • 本地方法棧

本地方法棧與虛擬機棧所發揮的作用是非常相似的,其區別在於虛擬機棧爲虛擬機執行java方法(也就是字節碼)服務,而本地方法棧則是爲虛擬機使用到的本地(Native)方法服務。與虛擬機棧一樣,本地方法棧也會在棧深度溢出或者棧擴展失敗時分別拋出StackOverflowError和OutOfMemoryError。線程私有,不會被GC回收。

有的虛擬機直接將虛擬機棧和本地方法棧合二爲一,不在單獨考慮。

總結:線程私有的三個部分都是隨着線程執行結束而結束(JVM就銷燬了虛擬機棧裏面的棧幀)。

2、線程公有數據區(2個部分)

  • 元空間

在jdk1.8之前,元空間所在的區域被稱爲方法區,方法區在jdk1.7時合併到了堆。

jdk1.8時,方法區所在的區域被稱爲元空間,但是1.8仍然保留着方法區的概念,只不過實現方式不同,元空間與堆不相連,但與堆共享物理內存,邏輯上可以認爲在堆中。

元空間的特點

①線程共享

②存儲類信息、常量、運行時常量池、靜態變量、即時編譯器編譯後的代碼等數據

③在jdk1.7之前,在HotSpot虛擬機上將方法區成爲永久代,在jdk1.8時,完全放棄了永久代,改用了元空間

④因爲效率問題,無垃圾回收

⑤空間不夠時,OutOfMemoryError

⑥設置元空間的大小--XX:Metaspace=10M-XX:MetaspaceSize=10M

運行時常量池的特點

運行時常量池是元空間的一部分,Class文件除了有類的版本、字段、方法、接口等描述信息外,還有一項信息是常量池表,用於存放編譯期生成的各種字面量的符號引用,這部分內容將在類加載後存放到元空間的運行時常量池中。

總結

jdk1.6及之前:有永久代,常量池在方法區

jdk1.7:從某個版本開始去除永久代,常量池1.7放入堆中

jdk1.8及之後:無永久代,常量池1.8在元空間。

java堆(heap)是虛擬機所管理的內存中最大的一塊,java堆是被所有線程共享的一塊內存區域,在虛擬機啓動時創建,幾乎所有的對象實例和數組都在堆上分配。

java堆是垃圾收集器管理的內存區域,從回收的角度看,由於現代垃圾收集器大部分都是基於分代收集理論設計的,所以java堆可以分爲新生代和老年代,新生代又可以分爲Eden空間、From Survivor空間和To Survivor空間,無論怎樣劃分,都是爲了更好的進行垃圾回收。

java堆可以被實現成固定大小的,也可以是擴展的(通過-Xmx和-Xms設定)。如果在java堆中沒有內存完成實例分配,並且堆無法在擴展時,java虛擬機將會拋出OutOfMemory異常。

在堆中加載實例對象的順序

當老年代空間滿時,將會拋出OutOfMemory異常。

java堆溢出

不斷創建對象又不釋放,當對象到達一定數量時,無堆空間時將會產生堆溢出

內存泄漏:GC Roots到對象之間有可達路徑卻無法回收(存在對象引用,卻沒有釋放)

內存溢出:內存溢出是指應用系統中存在無法回收的內存或使用內存過多,最終使得程序運行要用到的內存大於能提供的最大內存。在Java虛擬機中,GC Roots到對象之間無可達路徑,可以被收集,但對象還存活着,此時可以根據物理機內存適當的調大虛擬機參數-Xms、-Xmx,分析代碼是否對象生命週期過長。對象是否持有狀態時間過長。

參考文獻

[1]周志明.深入理解java虛擬機

[2]https://www.bilibili.com/video/BV13J411n72m?from=search&seid=7875422419866373500

看了這篇文章,你是否「博學」

往期推薦

Lombok天天用,卻不知道它的原理是什麼?萬惡的NPE如何避免,幾種你必須知道的方案!!!面試官:消息隊列這些我必問!什麼是集羣?什麼又是負載均衡?你說得清楚嗎?一文搞定分佈式系統ID生成方案這是我看過關於 volatile 最好的文章

點個「在看」,是對我最大的鼓勵!

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