JVM虛擬機基礎
JVM虛擬機結構
jvm的整體結構大致如下:
- 類加載器:類加載器用來加載Java類到JVM虛擬機中,源代碼程序.java文件在經過編譯器編譯之后就被轉換成字節代碼.class文件,類加載器負責讀取字節代碼,並轉換成java.lang.Class類的一個實例。
- 運行時數據區
- 元數據區:JDK1.8開始的說法,之前稱為方法區Method-Area,存儲已被虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼等數據。
- 堆區:所有線程共享的一塊內存區域,虛擬機啟動時被創建用來存放對象實例。
- JVM棧:可以參考了解棧的數據結構,存放Java方法執行的內存模型,在Java開發中,一個功能實現需要多個子程序方法配合,程序執行時跳往子程序前,會將下個指令的地址存到堆棧中,直到子程序執行完后再將地址取出,退回到原來的程序中。
- 本地方法棧:本地方法棧和虛擬機棧的功能類似,為JVM調用native方法時服務。
- 程序計數器:相對較小的一塊內存空間,作用可以理解是當前線程所執行的字節碼的行號指示器。
- 執行引擎:Java虛擬機最核心的組成部分,輸入的是字節碼,處理過程是字節碼解析,輸出執行結果
生命周期
這里說的JVM生命周期,指JVM執行Java程序時的周期:
- 啟動初始化:啟動時通過引導類加載器創建初始類完成;
- 程序執行:從main方法開始,執行Java程序,直到程序執行完結束;
- 虛擬機退出:程序正常執行結束,或者發生異常、錯誤等而造成終止,也可以調用exit退出方法;
HotSpot虛擬機
HotSpot是Java體系下使用最多的虛擬機,它結合了最新的內存模型,垃圾收集器和自適應優化器,為使用許多先進技術的Java應用程序提供了最佳性能。
JVM類加載機制
類加載簡介
類的加載機制是指把編譯后的.class類文件的二進制數據讀取到內存中,並為之創建一個java.lang.Class對象,用來封裝類在元數據空間的數據結構。
類在JVM中的生命周期為:加載,連接,初始化,使用,卸載。不過這里只重點描述加載,連接,初始化這三個過程
加載過程
基於一張圖看類加載子系統的細節流程:
1.加載階段
過程描述:加載階段需要完成以下三個過程:
- 通過類的全限定名來獲取其定義的二進制字節流;
- 將字節流所代表的靜態存儲結構轉化為雲數據空間的運行時數據結構;
- 在堆Heap中生成一個代表這個類的java.lang.Class對象,作為對元數據空間中這些數據的訪問入口;
類加載器:
- 引導類加載器:Bootstrap-ClassLoader基於C/C++實現,負責加載Java的核心類庫JAVA_HOME\jre\lib\rt.jar,該加載器不繼承自ClassLoader抽象類,並且只加載包名為java、javax、sun等開頭類,一次保證對核心源碼的保護。
- 擴展類加載器:Extension-ClassLoader,基於Java語言,由sun.misc.Launcher$ExtClassLoader實現,派生於ClassLoader抽象類,從java.ext.dirs系統變量指定的路徑中的加載類庫,或者JDK安裝目錄jre\lib\ext目錄下加載。
- 系統類加載器:Application-ClassLoader,基於Java語言,由sun.misc.Launcher$ExtClassLoader實現,它負責加載環境變量ClassPath指定的類庫,如果在應用程序中沒有自定義類加載器,一般情況下作為程序中默認的類加載器。
2.連接階段:
驗證:目的在於確保Class文件的字節流中包含的信息符合當前虛擬機的要求,保證加載類的正確性,不會危害虛擬機自身的安全,主要包括四種檢驗動作:
- 文件格式驗證:驗證字節流是否符合Class文件格式的規范;
- 元數據驗證:確保其描述的信息符合Java語言規范的要求;
- 字節碼驗證:確定程序語義是符合邏輯的;
- 符號引用驗證:確保解析動作能正確執行。
准備:為類的靜態變量分配內存,並初始化為默認值,這時候進行內存分配的僅包括類變量(static)修飾,不包括(final-static)修飾的,這里也不會為實例變量分配初始化,實例變量會隨着對象一塊分配到Java堆中。
解析:將常量池中的符號引用轉換為直接引用的過程,直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。解析主要針對類或接口、字段、類方法、接口方法、方法類型等,解析的動作實際是會隨着JVM在執行完初始化之后再執行的。
3.初始化階段
執行類構造器clinit()方法的過程,該方法不需要自定義,是javac編譯器自動收集類中的所有類變量的賦值動作和靜態代碼塊中的語句合並而來,Jvm要保證clinit()方法在多線程訪問下的安全性。
機制策略
1.雙親委派模式
類加載器收到了類加載的請求時,不會自己先去嘗試加載這個類,而是把請求委托給父加載器去執行;
如果父加載器還存在父類加載器,則依次向上委托,因此類加載請求最終都應該被傳遞到頂層的啟動類加載器中;
如果父類加載器可以完成類加載請求,就直接成功返回,只有當父加載器在無法完成該加載,子加載器才會嘗試自己去加載該類;
2.沙箱安全機制
假設自定義一個類名為String且所在包為java.lang,在使用引導類加載器加載時會先加載JDK中的String類,因為這個類本來是屬於jdk的,后面再次出現String類就會報錯,以此保證源代碼不被惡意篡改,這就是沙箱安全機制
JVM運行時數據區
-
內存結構
內存是計算機的重要部件之一,它是外存與CPU進行溝通的橋梁,計算機中所有程序的運行都在內存中進行,內存性能的強弱影響計算機整體發揮的水平。JVM的內存結構規定Java程序在執行時內存的申請、划分、使用、回收的管理策略,通說來說JVM的內存管理指運行時數據區這一大塊的管理。
-
線程運行
JVM中一個應用是可以有多個線程並行執行,線程被一對一映射為服務所在操作系統線程,調度在可用的CPU上執行,啟動時會創建一個操作系統線程;當該線程終止時,這個操作系統線程也會被回收。
在虛擬機啟動運行時,會創建多個線程,數據區中有的模塊是線程共享的,有的是線程私有的:
線程共享:元數據區、堆Heap;
線程私有:虛擬機棧、本地方法棧、程序計數器;
單個CPU在特定時刻只能執行一個線程,所以多線程通過幾塊空間的使用,然后不斷的爭搶CPU的執行時間段。
元數據空間
基本描述:方法元空間(方法區)在JVM啟動的時候被創建,是被各個線程共享的內存空間,用於存放類和方法的元數據以及常量池,比如Class和Method。在實際的開發中,經常因為加載的類太多,進而導致內存溢出問題,這樣可以對元空間的大小進行擴展。
與堆的關系:
元空間存放加載的類信息,當類被實例化時,堆中存儲實例化的對象信息,並且通過對象類型數據的指針找到類。
堆空間
基本描述:JVM啟動時創建堆區,是內存管理的核心區,通常情況下也是最大的內存空間,是被所有線程共享的,幾乎所有的對象實例都要在堆中分配內存,所以這里也是垃圾回收的重點空間。
堆棧關系
棧是JVM運行時的單位,堆是存儲單位,當棧中方法結束,相關對象失去所有引用后,不會馬上被移除堆空間,要等到垃圾收集器運行的時候。
虛擬機棧
虛擬機棧(Java棧)在每個線程創建時都會生成一個虛擬機棧,棧的內部是一個個棧幀單元,對應Java方法的調用,其生命周期和線程周期保持一致。用來存儲方法的局部遍歷,部分執行結果,方法的調用和返回。
棧幀是方法執行的數據集,維持執行過程中的各種數據信息,執行的方法依次入棧,棧頂存放當前要執行的方法,執行結束后出棧,對於棧沒有垃圾回收問題。
程序計數器
基本描述:JVM中程序計數寄存器用來存儲下一條將要執行指令的地址,執行引擎獲取到指令后進行執行,是線程私有的。它可以看作是當前線程所執行的字節碼的行號指示器。
前后關系:線程在獲取CPU的時間段內執行代碼,但是線程隨時可能沒有執行完就被掛起,等到線程A再次獲取CPU執行時,CPU 得知道執行到線程A的哪一個指令,程序計數器會存儲該動作。
本地方法棧
本地方法棧與虛擬機棧所起到的作用是類似的,虛擬機棧為虛擬機執行Java方法,本地方法棧管理虛擬機使用到的 本地方法,在虛擬機規范中對本地方法棧中方法使用的語言、使用方式與數據結構並沒有強制規定,因此具體的虛擬機可以自由實現它。HotSpot虛擬機直接就把本地方法棧和虛擬機棧合二為一。
執行引擎和垃圾回收
執行引擎
應用程序經過編譯,轉換為字節碼文件,字節碼加載到內存空間並不能直接在操作系統上執行,執行引擎作為Java虛擬機核心的組成部分,作用就是將字節碼指令解釋/編譯為對應系統平台上的本地機器指令。
解釋器:虛擬機啟動時會根據預定義對字節碼采用逐行解釋的方式執行,將每條字節碼文件中的內容解釋為對應系統平台的本地機器指令執行;
JIT編譯器:虛擬機將源代碼編譯成本地機器平台相關的機器語言,並且尋找熱點高頻執行的代碼將其放入元空間中,即元空間中存放的JIT緩存代碼;
垃圾回收:對於沒有任何引用的對象標記為垃圾,會被回收釋放內存空間。
垃圾對象標記
1. 引用計數法
每個對象保存一個整型引用計數器,用來記錄對象被引用的次數,當該對象被一個對象引用時,計數器加1,當失去一個引用時,計數器減1;引用計數算法就是通過判斷對象的引用數量來決定對象是否可以被當做垃圾對象回收掉。
雖然引用計數法效率高,但是當兩個對象互相引用時會導致這兩個對象一直不會被回收,這是一個致命的缺陷。所以JVM並沒有采用該標記算法。
2. 垃圾對象標記
可達性分析算法是基於對象到根對象的引用鏈是否可達來判斷對象是否可以被回收;
運行程序把所有的引用關系鏈看作一張圖,通過GC-Roots根對象對象集合作為起始點,從每個根節點向下不斷搜索被根對象集合所連接的對象是否可達,搜索路徑稱為引用鏈(Reference-Chain),如果對象到GC-Roots沒有任何引用鏈存在,則說明此對象是不可用的,虛擬機棧中引用的對象如下:
- 元空間中類靜態屬性引用的對象;
- 元空間中常量引用的對象;
- 本地方法棧中Native方法引用的對象;
相對於引用計數法算法,可達性分析算法則避免了循環引用導致的問題,同樣具備執行高效的特點,也是JVM采用的標記算法。
垃圾回收機制
1.標記清除算法
標記-清除算法分為標記和清除兩個階段:
- 標記階段:從根對象集合進行掃描,對存活的對象對象標記;
- 清除階段:再次掃描發現未被標記的對象並進行回收
該算法效率不高,進行垃圾回收需要暫停應用程序,同時會產生大量內存碎片,后續程序運行過程中分配內存占用較大的對象時,會有連續內存不夠情況,容易觸發再一次垃圾收集動作。
2.標記整理算法
標記整理算法的標記過程類似標記清除算法
- 第一階段:標記出垃圾對象;
- 第二階段:讓所有存活的對象都向內存區一端移動;
- 第三階段:直接清理掉邊界端以外的內存,類似於磁盤整理的過程;
該垃圾回收算法效率不高,對象移動過程需要暫停應用程序,適用於對象存活率高的場景(老年代)。
3.復制算法
復制算法將內存按容量划分為大小相等的兩塊,每次只使用其中的一塊,當使用的這塊的內存用完,就將還存活着的對象復制到另外一塊空閑內存上,然后使用過的內存空間一次清理。
該算法實現簡單,運行效率高,但是內存空間嚴重浪費,適用於對象存活率低的場景,比如新生代。
4.分代收集算法
當前市場上幾乎所有的虛擬機都采用該回收算法,分代收集算法根據年輕代和老年代的各自特點采用不同的算法機制,不同內存區域中對象生命周期也不同,因此對堆內存不同區域采用不同的回收策略可以提高垃圾回收執行效率。通常情況新生代對象存活率低,回收頻繁,就采用復制算法;老年代存對象生命周期長,活率高,就用標記清除算法或者標記整理算法。
Java堆內存一般可以分為新生代、老年代和永久代三個模塊,如下圖所示:
新生代:通常情況下,新創建的對象實例首先都是放在新生代空間中,所以追求快速的回收掉垃圾對象,一般情況下,新生代內存按照8:1:1的比例分為一個eden區和兩個survivor(survivor0,survivor1)區,對象實例大部分在Eden區中生成;
垃圾回收時先把eden區存活對象復制到S0區,然后清空eden區,當S0區也滿時,再將eden區和S0區存活對象復制到S1區,然后清空eden和S0區,之后交換S0區和S1區的角色,當S1區無法存放eden區和S0區的存活對象時,就將存活對象直接存移到老年代區,當老年代區也滿了,觸發一次FullGC,即新生代、老年代都進行回收。
老年代:老年代區存放一些生命周期較長的對象,對象實例在新生代中經歷了多次垃圾回收仍然存活的對象,會被移動到老年代區中。