溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

如何進行JVM體系結構分析

發(fā)布時間:2022-01-14 09:53:34 來源:億速云 閱讀:96 作者:柒染 欄目:大數據

小編今天帶大家了解如何進行JVM體系結構分析,文中知識點介紹的非常詳細。覺得有幫助的朋友可以跟著小編一起瀏覽文章的內容,希望能夠幫助更多想解決這個問題的朋友找到問題的答案,下面跟著小編一起深入學習“如何進行JVM體系結構分析”的知識吧。

虛擬機

何為虛擬機呢?虛擬機是模擬執(zhí)行某種指令集體系結構(ISA)的軟件,是對操作系統和硬件的一種抽象。其軟件模型如下圖所示: 
如何進行JVM體系結構分析

計算機系統的這種抽象類似于面向對象編程(OOP)中的針對接口編程泛型(或者是依賴倒轉原則),通過一層抽象提取底層實現中共性的部分,底層實現這個抽象并完成自己個性的部分。也就是說通過一個抽象層次來隔離底層的不同實現。虛擬機規(guī)范定義了這個虛擬機要完成的功能(也就是接口),底層的操作系統和硬件利用自己提供的功能來實現虛擬機需要完成的功能(實現)。通過運行在虛擬機之上,Java才具有很好跨平臺特性。 
如何進行JVM體系結構分析

Java虛擬機

Java虛擬機(JVM)是由Java虛擬機規(guī)范定義的,其上運行的是字節(jié)碼指令集。這種字節(jié)碼指令集包含一個字節(jié)的操作碼(opcode),零至多個操作數(oprand),虛擬機規(guī)范明確定義了每種字節(jié)碼指令完成的功能是什么以及需要多少個操作數。Java虛擬機上運行的class文件,這個文件中包含字節(jié)碼指令流以及類定義的信息,所以Java虛擬機規(guī)范還定義了class文件的格式(精確到每個字節(jié))。所以實現Java虛擬機的兩個要素是字節(jié)碼指令集和class文件格式,Java虛擬機的實現者只要以正確方式讀取class文件中的每一條字節(jié)碼指令,并按照要求實現字節(jié)碼指令的功能就可以實現JVM。 
目前常用的商用JVM主要有:Sun HotSpot,BEA JRocket以及IBM J9。其中由于BEA和Sun已經被Oracle收購,所以Oracle擁有當今世界上最流行的兩個JVM,并有傳言說Oracle將在Java8時將兩個虛擬機合并,各取所需,取長補短,打造一個更加精湛的JVM。HotSpot會以解釋+即時編譯執(zhí)行代碼,HotSpot在解釋執(zhí)行字節(jié)碼的時候,會探測熱點(hotspot)代碼,然后將這部分代碼編譯為本地代碼,之后將直接運行本地代碼,而不是解釋,這樣會有效提高虛擬機性能。JRocket主要是定位于服務器應用,所以不關注虛擬機的啟動速度,它會將所有代碼即時編譯為本地代碼執(zhí)行,JRocket的垃圾收集器具有很高的收集效率。J9定位與HotSpot類似,專注于桌面應用和服務器應用,主要是針對IBM的各種Java產品。

Java語言與Java虛擬機

我們知道Java源代碼,即.java文件,通過javac編譯為.class文件。.class文件可以運行在JVM上,JVM底層會通過字節(jié)碼解釋器或者即時編譯器(JIT Compiler)執(zhí)行.class文件中的字節(jié)碼指令。JVM是運行在操作系統之上的,操作系統又通過指令集調用底層硬件服務執(zhí)行其上的各種軟件。 
如何進行JVM體系結構分析

可以看到Java是運行在JVM之上的。但是Java語言和JVM沒有必然的聯系。Java語言并不是只能運行在JVM之上,只要實現了相應的編譯器Java語言就可以運行在任何平臺之上(比如J++),也可以被編譯為本地代碼直接運行在操作系統之上,比如,Linux上的GCJ(GNU Compiler for Java)就可以把Java語言編譯為本地代碼直接執(zhí)行。同樣的,JVM上也不是只能執(zhí)行Java語言,只要實現了適當的編譯器,將其他語言編譯為JVM上的字節(jié)碼,就可以在JVM上運行。比如,JRuby,Jython以及Groovy等其他JVM語言,都會通過相應的編譯器或是解釋器轉化為.class,然后再JVM上運行。由于JVM并不關心.class文件是由Java、JRuby、Jython等轉化而來,只要這個文件結構正確并能通過class文件校驗。因此,由于.class文件屏蔽了Java、JRuby等上層語言的差異,所以Java、Groovy等可以相互調用。

JVM生命周期

當啟動一個Java程序時,一個虛擬機實例就誕生了,當程序關閉退出時,這個虛擬機實例隨之消亡。JVM實例通過main()方法來運行一個Java程序。而這個main()方法必須是共有的(public)、靜態(tài)的(static)、返回void,并且接收一個字符串數組為參數。Java程序初始類中的main()方法,將作為改程序初始線程的起點,任何其他線程都是由這個初試線程啟動的。 
JVM內部有兩種線程:守護線程與非守護線程。守護線程通常是由虛擬機自己使用的,比如垃圾回收線程。當該程序所有的非守護線程都終止時,JVM實例將自動退出。

Java虛擬機體系結構

JVM由類加載器子系統,運行時數據區(qū),執(zhí)行引擎以及本地方法接口組成。

如何進行JVM體系結構分析

類加載器子系統

類加載器子系統主要用于定位類定義的二進制信息,然后將這些信息解析并加載至虛擬機,轉化為虛擬機內部的類型信息的數據結構。類加載器子系統還承擔著安全性的責任,并且是JVM的動態(tài)鏈接和動態(tài)加載的基礎。將二進制信息=>類型信息的數據結構,中間需要經過很多步驟。首先類加載器是JVM安全沙箱的第一道防線,能夠防止非信任類破壞虛擬機。每一個被加載的class文件需要經過四次校驗才能被加載。校驗通過后,類加載器的命名空間和運行時包的特性能夠防止非信任類偽裝成信任類來破壞虛擬機。類加載器在方法區(qū)構造具有這個類的信息的數據結構后,會在堆上創(chuàng)建一個Class對象作為訪問這個數據結構的接口。同時,類加載還需要初始化類的靜態(tài)數據,也就是調用類的方法。以上就是一個類的加載、鏈接及初始化的過程。

運行時數據區(qū)

運行時數據區(qū)是JVM運行時的內存空間的組織,邏輯上又劃分為多個區(qū),這些區(qū)的生命周期和它是否線程共享有關,它們分別是:

用于存放對象或數組實例,也就是運行期間new出來的對象。堆的生命周期與JVM相同,并且在線程之間共享訪問。由于多線程并發(fā)訪問,所以需要考慮線程安全的問題,有兩種方法。第一種是,加鎖進行互斥訪問。第二種是線程本地分配緩沖(Thread Local Allocate Buffer, TLAB),在線程創(chuàng)建時預先給每個線程分配一塊區(qū)域,這塊區(qū)域是線程私有的,對其他線程是不可見,也就不會被共享。JVM規(guī)范規(guī)定在申請不到足夠的內存時,堆會拋出OutOfMemoryException。

方法區(qū)

存放類型信息和運行時常量池(Runtime Constant Pool)。每個被類加載器加載的類都會在方法區(qū)中形成一個與子對應的類型信息的數據結構,包括:這個類的類名、直接超類、實現的接口列表、字段列表、方法列表等。運行時常量池是class文件中的常量池列表(Constant Pool List)在運行時的一種體現,其中存儲各種基本數據類型及String類型的常量以及其他類、方法、字段的符號引用。方法區(qū)的生命周期與JVM相同,被多個線程共享,所以要考慮并發(fā)訪問的安全性的問題。JVM規(guī)范規(guī)定在需要的內存得不到滿足的情況下,方法區(qū)會拋出OutOfMemoryException。

PC(Program Counter)

線程私有的,生命周期與線程相同,是對CPU中PC的一種模擬。如果線程正在執(zhí)行的是Java方法,則該線程的PC中存放的下一條字節(jié)碼指令的地址。在進行Java方法的調用和返回時,需要更新PC以保存當前方法(Current Method)正在執(zhí)行的字節(jié)碼指令的地址。PC是JVM規(guī)范中唯一沒有規(guī)定會拋出異常的存儲區(qū)。

JVM棧

線程私有,生命周期與線程相同,是對傳統語言(比如C)中的方法調用棧的一種模擬。JVM棧中存放棧幀(Frame)用于進行方法調用和返回、存儲局部變量以及計算的中間結果。JVM規(guī)范規(guī)定棧可以拋出兩種異常:(1)StackOverflowException,在棧的深度大于某個規(guī)定值的情況下拋出。(2)OutOfMemoryException,在為新棧幀分配內存或者是為線程分配棧的內存時,申請不到足夠的內存的情況下拋出。 
JVM棧中存放的是棧幀,每個棧幀對應著一次方法調用。每一時刻,JVM線程只能執(zhí)行一個方法(Current Method),該方法的棧幀是JVM棧的棧頂的元素(叫做當前棧幀,Current Frame),當調用一個方法時,會初始化一個棧幀壓入JVM棧;當方法調用返回或者拋出異常沒有被處理的情況下,JVM棧會彈出該方法對應的棧幀。每一個棧幀中存放局部變量表(Local Variable Table)、操作數棧(Oprand Stack)以及其他棧幀信息。棧幀的大小在編譯時就確定了,編譯器會把局部變量表和操作數棧的大小記錄在class文件中method_info的屬性表中。局部變量表類似于數組存放局部變量和方法參數。由于JVM采用的是基于棧的指令集體系結構,而不是基于寄存器,所以JVM上的所有計算都是在操作數棧上進行的(比如,算術運算、方法調用、內存訪問等)。

本地方法棧

用于支持本地方法調用,拋出的異常與JVM棧相同。

執(zhí)行引擎

執(zhí)行引擎用于執(zhí)行JVM字節(jié)碼指令,主要由兩種實現方式: 
(1)將輸入的字節(jié)碼指令在加載時或執(zhí)行時翻譯成另外一種虛擬機指令; 
(2)將輸入的字節(jié)碼指令在加載時或執(zhí)行時翻譯成宿主主機本地CPU的指令集。這兩種方式對應著字節(jié)碼的解釋執(zhí)行和即時編譯。比如在HotSpot VM中執(zhí)行引擎的實現是一種解釋-編譯的層次結構: 
(1)解釋執(zhí)行:解釋執(zhí)行字節(jié)碼,并以方法為單位收集“熱點(HotSpot)代碼”的信息,將“熱點代碼”執(zhí)行C0編譯。 
(2)C0編譯:將收集的“熱點代碼”編譯成本地代碼,并進行一些簡單的優(yōu)化。繼續(xù)收集運行時信息,將一些頻繁執(zhí)行的本地代碼進行C1編譯。 
(3)C1編譯:將C0階段的本地代碼,進行一些比較激進的優(yōu)化。如果某些優(yōu)化導致本地代碼執(zhí)行失敗,此時JVM會退化到解釋執(zhí)行字節(jié)碼階段。

自動內存管理

自動內存管理用于管理運行時數據區(qū)的分配和釋放。和C和C++相比,Java不需要程序員主動的管理內存(在new出對象后,不需要顯示的delete),這樣JVM就需要承擔內存管理這個任務。內存管理的重點主要是在申請內存(new對象、類加載和初始化、啟動線程時初始化棧等)得不到滿足時,JVM可以自動回收那些不再存活的對象所占用的內存,也就是經常聽到的垃圾收集。在回收過程中還要保證處理內存空間的碎片,以提高空間利用率?;厥者^程主要有兩個關鍵點,標記存活對象和回收內存的算法。

標記存活對象主要有引用計算和根搜索法兩種。 
(1)引用計數,是一種很普遍的方法,在python、lua等一些腳本語言中都是使用這種算法。每個對象持有一個計數器,標記這個對象被引用的次數。進行垃圾收集時,那些引用計數為0的對象就是“死”對象,需要被收集。引用計數的一個缺點就是它沒有辦法處理循環(huán)引用的情況(A->B, B->A)。 
(2)根搜索,HotSpot虛擬機采用這種算法標記存活對象。把方法區(qū)、JVM棧中的所有的引用組成的集合作為搜索的根,從這個集合開始遍歷直到結束。其中被遍歷到的對象是存活對象;那些沒有被遍歷到的對象需要被垃圾收集。這樣可以有效的避免循環(huán)引用的情況。 
回收內存的算法主要有: 
(1)復制算法,將內存分成兩個部分,每一時刻只是用其中的一個。進行回收時,將所有存活的對象依次復制到另一個部分(依次復制避免了內存碎片的產生),接下來只用這一個部分。復制算法需要在兩個內存區(qū)域來回復制,有一定的復制開銷和空間開銷(每一時刻只使用一個區(qū)域),但是可以很好的解決內存碎片的問題,適用于對象頻繁創(chuàng)建并且生命周期短的情況。 
(2)標記清掃,先進行存活對象標記,回收時將“死”對象占用的內存直接釋放掉,會產生大量的內存碎片。 
(3)標記整理,標記階段與標記清掃算法一樣,回收階段釋放“死”對象的內存后,還需要進行對象的移動使得所有對象依次在內存中排列,避免了內存碎片的產生。標記整理與復制算法相反,適用于對象創(chuàng)建不頻繁,生命周期長得情況。 
(4)按代收集,將內存按照對象生命周期的不同劃分為多個部分,每個部分采用不同的收集算法。目前,大部分商業(yè)虛擬機都是采用這種算法。比如,在HotSpot中,內存被劃分為:新生代(New)、老年代(Old)和永久代(Perm)。新生代采用復制算法,老年代和永久代采用標記整理算法。內存分配、回收的策略是,對象首先在新生代分配,如果新生代內存不滿足要求,則觸發(fā)一次新生代內存的垃圾收集(Young GC,或者是Minor GC)。Young GC會導致部分新生代的對象被移動至老年代,一部分是因為新生代內存不足以放下所有的對象;另一部分是因為這些對象的年齡(每個對象都保存著這個對象被垃圾收集的次數,表示它的年齡。存儲在對象頭的age屬性中)大到足以晉升到老年代。當新生代的對象進入老年代,而老年代的內存不滿足要求時,則會觸發(fā)一次整個新生代和老年代的垃圾收集(Full GC, 或者是Major GC)。 
在JVM中有多個后臺線程用于完成自動內存管理,對于CPU來說這些后臺線程和用戶線程是一樣的,都需要占用系統的資源。在GC線程進行垃圾收集時必須執(zhí)行“Stop the World”這一操作,也就是暫停所有的用戶線程。這就導致對于實時性要求比較高的系統,JVM的垃圾收集可能是一個短板。但是在JDK1.5,Sun提供了CMS(Concurrent Mark and Sweep)垃圾收集器,通過GC線程和用戶線程并發(fā)執(zhí)行減少GC時間,提高了JVM的實時性。在JVM的各種應用中,gc調優(yōu)是一個關鍵的部分,主要目標是減少GC的次數并且降低每次GC的時間。關于這部分內容,后續(xù)的JVM內存管理會詳細討論。

JVM執(zhí)行程序的流程分析

在命令行執(zhí)行”java Main”就會開啟一個JVM實例,我們可以通過jps,jstat等JVM工具觀察JVM的運行狀態(tài),下面以運行com.ntes.money.Main這個類為例來描述一下JVM執(zhí)行一個程序的流程。 
當在命令行執(zhí)行”java -Xmx=12m -Xms=12m -Dname=value com.ntes.money.Main”這個命令時,JVM的執(zhí)行流程是: 
1)加載JVM,主要是加載動態(tài)鏈接庫,windows下是jvm.dll,Linux下是libjvm.so; 
2)設置JVM啟動參數,比如命令中的-Xmx=12m -Xms=12m用于設置堆大小。 
3)初始化JVM。 
4)調用類加載器子系統,加載com.ntes.money.Main。這里給出的是自定義類,根據類加載器雙親委派鏈,最后是由系統默認類加載器(Classpath類加載器)進行加載。首先,根據全路徑類型轉化為文件路徑com/ntes/money/Main.class,然后讀取Main.class中的二進制信息、解析、加載,在方法區(qū)中形成Main類對應的數據結構。這里可能拋出ClassNotFoundException,有兩種原因。一是文件路徑com/ntes/money/Main.class不存在;二是com/ntes/money/Main.class文件路徑存在,但是Main.class文件中存儲的不是Main類的信息,比如是Main1,Main2等其他類的信息。這種情況下,會拋出NoClassDefFoundError,然后導致ClassNotFoundException。 
5)在方法區(qū)com.ntes.money.Main類對應的數據結構中,根據方法描述符及訪問標志,查找main方法。這里的描述符,包括了方法的方法名、參數、返回值,也就是public static void main(String[])。如果找不到對應的main方法,會拋出NoSuchMethodError: main異常。 
6)通過本地方法(JNI)執(zhí)行main方法。

感謝大家的閱讀,以上就是“如何進行JVM體系結構分析”的全部內容了,學會的朋友趕緊操作起來吧。相信億速云小編一定會給大家?guī)砀鼉?yōu)質的文章。謝謝大家對億速云網站的支持!

向AI問一下細節(jié)

免責聲明:本站發(fā)布的內容(圖片、視頻和文字)以原創(chuàng)、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

jvm
AI