JVM工作原理和工作流程簡(jiǎn)述
JAVA之所以跨平臺(tái),是因?yàn)橛蠮VM這么一個(gè)編譯和運(yùn)行機(jī)器,它令對(duì)于系統(tǒng)的操作對(duì)于用戶而言是黑盒的,使得開發(fā)人員更快速和更注重軟件功能的實(shí)現(xiàn)。然而,也因?yàn)閖vm是黑盒,所以內(nèi)部和底層具有不確定性,如果用狀態(tài)機(jī)來表示jvm,那么jvm就是一種現(xiàn)役復(fù)制不確定的狀態(tài)機(jī),因?yàn)樗臓顟B(tài)和表現(xiàn)跟系統(tǒng)、底層、硬件等等都有關(guān)系,從而狀態(tài)是不確定,如果在分布式應(yīng)用中,jvm一直以來兼容性都不是很好,這就是主要原因。盡管如此,就單一的系統(tǒng)而言,弄清楚jvm運(yùn)行的來龍去脈,對(duì)于系統(tǒng)的運(yùn)行至關(guān)重要。
理解jvm的運(yùn)行原理具有以下幾點(diǎn)充分作用:
1、針對(duì)系統(tǒng)進(jìn)行內(nèi)存和垃圾回收監(jiān)控
2、解決因內(nèi)存溢出和泄露的問題
3、對(duì)系統(tǒng)進(jìn)行優(yōu)化
4、提升jvm和系統(tǒng)性能
jvm的運(yùn)行原理有主要有三方面,其實(shí)這也是jvm的主要工作:
1、內(nèi)存管理
2、執(zhí)行流程
3、垃圾回收
在開始之前,有一些知識(shí)需要知道,廣義來講,jvm并不是指sun的hotspot,而是一個(gè)規(guī)范,因此不同廠商會(huì)根據(jù)規(guī)范實(shí)現(xiàn)不同的jvm,因此這些jvm的表現(xiàn)都不是一致甚至相差甚遠(yuǎn)。在jvm規(guī)范中,通常我們所能接觸的就是命令行參數(shù)了。
命令行參數(shù)
命令行參數(shù)分為三種,標(biāo)準(zhǔn)、非標(biāo)準(zhǔn)、非穩(wěn)定
標(biāo)準(zhǔn)命令行參數(shù)會(huì)在jvm規(guī)范中明確列出,強(qiáng)制實(shí)現(xiàn)的選項(xiàng),并且具有版本控制廢棄的管理通知。非標(biāo)準(zhǔn)的命令行參數(shù)不是規(guī)范強(qiáng)制并且可能沒有對(duì)應(yīng)的通知,非穩(wěn)定參數(shù)是特定調(diào)校的選項(xiàng),同時(shí)也是非標(biāo)準(zhǔn)的。標(biāo)準(zhǔn)的選項(xiàng)可通過help命令查看,非標(biāo)準(zhǔn)的選項(xiàng)通過-X為前綴訪問,非穩(wěn)定的前綴是-XX,通常對(duì)于布爾類型的選項(xiàng),用+或-來設(shè)置true或者false,如-XX:+UseTLAB開啟線程內(nèi)存緩沖分配。
內(nèi)存劃分
jvm是具有內(nèi)存自動(dòng)分配和管理的架構(gòu),而內(nèi)存管理自動(dòng)化是解放勞動(dòng)力的重要工具,可以對(duì)比C/C++,開發(fā)人員不需要管理內(nèi)存,開發(fā)效率會(huì)比較高。
在jvm中,使用的內(nèi)存分為兩類,線程共享內(nèi)存和線程私有內(nèi)存。
結(jié)合我們平時(shí)的代碼可以看出,線程共享的內(nèi)容包括方法、實(shí)例對(duì)象、常量,分別對(duì)應(yīng)共享內(nèi)存中的方法區(qū)、堆區(qū)、常量池。
堆區(qū)
堆區(qū)通常是共享內(nèi)存中最大的一塊,因此它也是GC重點(diǎn)關(guān)注區(qū)域。堆區(qū)可能是連續(xù)的也可能是不連續(xù)的,以及堆區(qū)的大小都會(huì)對(duì)GC造成相應(yīng)的影響。-Xms和-Xmx設(shè)置堆的最小和最大值,如果堆內(nèi)存大小超過最大值,則拋出OutOfMemoryError異常。
方法區(qū)
方法區(qū)存儲(chǔ)的是方法、類的結(jié)構(gòu)信息,而常量池也包含在內(nèi),除了我們代碼中所看到的靜態(tài)常量,這些常量還包括一些字節(jié)碼內(nèi)容和類初始化所需的特殊內(nèi)容。通常情況下,jvm不會(huì)對(duì)方法區(qū)GC直到方法區(qū)大小不夠,即使GC也只是針對(duì)常量池和類型,所以也被稱為永久區(qū)Permanent Generation,除了可以設(shè)置大小以外,還可以設(shè)置是否進(jìn)行GC,如果超過大小,拋出OutOfMemoryError異常。
常量池
這里說的常量池是運(yùn)行時(shí)的,通常是字節(jié)碼中的類的版本、描述,以及常量池表,這個(gè)表是一種符號(hào)表,在運(yùn)行的時(shí)候?qū)⑦@些符號(hào)的引用變?yōu)橹苯臃?hào)。由此可以看出,加載類會(huì)使用常量池和方法區(qū),如果類過多或常量過多,也會(huì)拋出OutOfMemoryError異常。
線程私有內(nèi)存區(qū)
線程私有內(nèi)存是被某一獨(dú)立線程獨(dú)占的,包括PC寄存器、java棧、本地方法棧
PC寄存器
這個(gè)寄存器是jvm內(nèi)部的,而非物理寄存器,因此也可以看出,jvm的指令執(zhí)行是基于棧架構(gòu)的,所有的操作都是經(jīng)過入棧出棧完成,為了確保線程安全,它被設(shè)定為線程私有的。通常,棧中存儲(chǔ)字節(jié)碼指令地址,如果調(diào)用的是本地方法,即native方法,則是空值。會(huì)不會(huì)拋出OutOfMemoryError異常,jvm目前沒有明確規(guī)定。
java棧
java棧的顆粒度比PC寄存器大,存儲(chǔ)方法的局部變量、操作計(jì)數(shù)、方法返回\方法出口等信息。局部變量除了我們代碼所接觸的類型,還包括一種叫做returnAddress返回地址類型,也是一種jvm規(guī)范的原始類型,但是開發(fā)人員并不能使用,實(shí)際上這種類型標(biāo)識(shí)一條字節(jié)碼指令的操作嗎。java棧也會(huì)OutOfMemoryError異常,不過他也是可以動(dòng)態(tài)擴(kuò)展的。
本地方法棧
用于支持本地方法調(diào)用時(shí)使用,但是jvm沒有強(qiáng)制實(shí)現(xiàn),和java棧類似。
執(zhí)行流程
我們的代碼在IDE中或者通過CMD來執(zhí)行即可看到執(zhí)行結(jié)果,而實(shí)際上每次執(zhí)行都會(huì)啟動(dòng)和關(guān)閉jvm,這個(gè)過程是相當(dāng)復(fù)雜的,下面羅列一下主要步驟。
對(duì)于sun的hotspot,launcher負(fù)責(zé)維護(hù)jvm的生命周期,包括啟動(dòng)和結(jié)束關(guān)閉。就是我們?cè)趈ava目錄下看到的java.exe和javaw.exe,一個(gè)有控制臺(tái)輸出,另一個(gè)沒有,用于執(zhí)行GUI程序。
jvm的啟動(dòng)初始化
1、解析命令行參數(shù),設(shè)置內(nèi)存大小和JIT編譯器,并且加載系統(tǒng)環(huán)境變量。
2、查找主類,并且調(diào)用本地方法JNI_CreateJavaVM創(chuàng)建jvm主線程。
3、當(dāng)jvm初始化完成,就會(huì)加載主類,如果加載成功,則調(diào)用本地方法傳入?yún)?shù),然后開始執(zhí)行java的程序了。
其中調(diào)用本地方法JNI_CreateJavaVM創(chuàng)建jvm主線程,是jvm的啟動(dòng)過程,實(shí)際上啟動(dòng)器并非直接調(diào)用該本地方法,而是先用main()函數(shù)創(chuàng)建主線程,然后通過主線程調(diào)用javamain()函數(shù)調(diào)用該JNI_CreateJavaVM方法創(chuàng)建子線程來完成初始化并執(zhí)行java程序。因?yàn)閯?chuàng)建的主線程是操作系統(tǒng)分配的初始線程,為了更好的定制線程,通過在該線程上創(chuàng)建再初始線程來初始化jvm。
進(jìn)一步細(xì)化JNI_CreateJavaVM函數(shù)的執(zhí)行內(nèi)容,主要流程如下:
1、檢查是否線程安全,也就是是否只有一個(gè)線程調(diào)用此方法,一個(gè)線程只能創(chuàng)建一個(gè)jvm實(shí)例。
2、初始化各個(gè)模塊,如日志、計(jì)數(shù)器、內(nèi)存頁等。
3、加載核心庫并初始化線程庫
4、初始化全局?jǐn)?shù)據(jù),這步完成后就可以創(chuàng)建java子線程了
5、初始化類加載器、解析器、編譯器、GC等模塊。其中重要一點(diǎn)就是初始化universe類型,這種類型是java種一切類型的類型,是一種數(shù)據(jù)結(jié)構(gòu),所有java的存儲(chǔ)對(duì)象都用該類型類存儲(chǔ)。
6、加載并初始化基礎(chǔ)類庫,如Lang、System、reflect等包。
7、返回給調(diào)用者。
通過上面的步驟,可以發(fā)現(xiàn)基礎(chǔ)類庫是在初始化階段完成加載的,這跟開發(fā)人員編寫的類庫加載順序是不同的。
jvm的關(guān)閉
當(dāng)java程序結(jié)束,jvm會(huì)先檢查有無未處理的異常以及清理這些異常,然后調(diào)用本地方法斷開主線程跟本地方法接口的連接,如果可以斷開,說明已經(jīng)沒有線程在運(yùn)行了,則可以安全的關(guān)閉jvm。
和JNI_CreateJavaVM方法對(duì)應(yīng)的是DestroyJavaVM方法,當(dāng)jvm在啟動(dòng)和運(yùn)行時(shí)發(fā)生錯(cuò)誤,根據(jù)嚴(yán)重程度會(huì)調(diào)用該方法關(guān)閉jvm,而在理想狀態(tài)下,即正常運(yùn)行直到退出,也是調(diào)用DestroyJavaVM方法關(guān)閉并銷毀jvm。停止jvm按照以下主要步驟進(jìn)行:
1、守護(hù)線程一致等待,直到只有一個(gè)非守護(hù)線程執(zhí)行。
2、停止監(jiān)控、計(jì)數(shù)器等線程。
3、移除當(dāng)前線程,釋放保護(hù)頁。
4、釋放所有資源,返回到調(diào)用者。
我們可以看出,當(dāng)需要關(guān)閉jvm時(shí),如果jvm中仍有線程在運(yùn)行,是無法強(qiáng)制關(guān)閉的,這就是為什么我們很多代碼的運(yùn)行出現(xiàn)異常后,重復(fù)的調(diào)試導(dǎo)致有多個(gè)后臺(tái)jvm在運(yùn)行卻不能自動(dòng)結(jié)束而要手動(dòng)關(guān)閉。
類加載機(jī)制
在前面說到,開發(fā)人員使用的類和基礎(chǔ)類庫并非同一時(shí)間加載的,這是有原因的。類的加載由類加載器來完成,包括加載、連接、初始化三個(gè)階段。完成加載后就可以通過new來創(chuàng)建類的實(shí)例對(duì)象了。類的加載可以理解為根據(jù)類的字節(jié)碼文件全路徑名讀取后轉(zhuǎn)換為與目標(biāo)類型一致的Class類型,并且是可以動(dòng)態(tài)加載的。
加載類由類加載器完成,加載器分為兩種,一種是Bootstrap Classloader引導(dǎo)加載器,另一種是User-defined Classloader用戶自定義加載器,用戶自定義加載器默認(rèn)又分為ExtClassloader和AppClassloader。
引導(dǎo)加載器是C++編寫的,負(fù)責(zé)完成lib目錄里的類加載,也就是前面所說的基礎(chǔ)類庫,而ExtClassloader和AppClassloader是java編寫的,分別負(fù)責(zé)加載lib/ext目錄和ClassPath系統(tǒng)路徑中的類型。他們都是Classloader的子類,我們也可以通過繼承父類來實(shí)現(xiàn)自己的類加載器。
父類委托模式
通過查閱類關(guān)系樹可以發(fā)現(xiàn),AppClassloader是ExtClassloader的子類,而ExtClassloader則是Classloader的子類,java規(guī)范要求自定義的類加載器都派生與父類,并且在進(jìn)行類加載的時(shí)候,都要委托給直接上級(jí)父類執(zhí)行加載,這就是父類委托模式(parents delegation model),國內(nèi)很多翻譯為雙親委托模式,但是你會(huì)發(fā)現(xiàn)是多親模式,所以我認(rèn)為父類委托更為合適。
父類委托模式在執(zhí)行時(shí),子類始終會(huì)委托父類加載,一級(jí)一級(jí)的向上請(qǐng)求,知道最后唯一的超類來進(jìn)行加載,如果父類無法加載,再一級(jí)一級(jí)的退回到子類進(jìn)行加載,這樣就不會(huì)重復(fù)加載相同的類了。
為什么要使用父類委托模式?因?yàn)轭惖募虞d必須是一次性不可重復(fù)的,試想一下,如果基礎(chǔ)類庫中的類可以重復(fù)加另一個(gè)類來替換原來的類,那是多么嚴(yán)重的安全隱患,為了避免這一點(diǎn),基礎(chǔ)類庫都是由C++編寫的啟動(dòng)加載器來加載,但是為了兼顧擴(kuò)展性,所以除了基礎(chǔ)類庫,其他的類都可以通過用戶加載器來加載,那么為了避免但不強(qiáng)制要求避免重復(fù)加載的情況發(fā)生,java規(guī)范就采取并建議我們按照父類委托的方式實(shí)現(xiàn)類加載器。
類的加載過程
前面說到,類先經(jīng)過類加載器將字節(jié)碼文件轉(zhuǎn)換為Class對(duì)象,但是這個(gè)時(shí)候并不能使用它,此時(shí)的類結(jié)構(gòu)信息存儲(chǔ)在方法區(qū)內(nèi),還需要對(duì)其進(jìn)行驗(yàn)證,結(jié)構(gòu)信息是否有效合法,一旦通過驗(yàn)證,就會(huì)為類中的靜態(tài)變量分配內(nèi)存空間并初始化值,這些準(zhǔn)備工作完成后,還需將類結(jié)構(gòu)中的符號(hào)和常量表的符號(hào)進(jìn)行解析轉(zhuǎn)為直接引用,這時(shí)候的類才具有執(zhí)行能力。最后的工作就是初始化了,也就是我們代碼中在new一個(gè)對(duì)象之前會(huì)執(zhí)行的static代碼塊。
垃圾回收機(jī)制
jvm的垃圾回收包括內(nèi)存動(dòng)態(tài)分配和內(nèi)存回收兩大塊。內(nèi)存的分配和垃圾回收是息息相關(guān)的,內(nèi)存分配的方式一定程度上決定采取何種垃圾收集器和收集算法。
前面說到,堆內(nèi)存可以是連續(xù)也可以是不連續(xù)的,也是GC的重點(diǎn)區(qū)域,但正由于這種分布的不確定性,該GC帶來很大麻煩。首先針對(duì)連續(xù)的情況。
指針碰撞
通過前面講述的jvm啟動(dòng)過程,我們知道創(chuàng)建對(duì)象就需要在堆內(nèi)存中劃分出一部分來存儲(chǔ)對(duì)象,如果此時(shí)的內(nèi)存是規(guī)整的,那么將空閑的和已使用的各放置一邊,兩部分的邊界處用一個(gè)指針標(biāo)記,當(dāng)新增對(duì)象內(nèi)存分配,就將指針偏移相應(yīng)的位置,下一次分配內(nèi)存只需要知道最后指針偏移的位置開始分配內(nèi)存并更新指針偏移量即可,這種方式就是指針碰撞(bump the pointer)。
空閑列表
然而,需要面臨的一個(gè)問題首先不是規(guī)整問題,而是線程安全,如果對(duì)指針的操作加鎖,必然會(huì)降低性能。并且如果堆不是連續(xù)的,指針碰撞就變得很棘手,此時(shí)還有一種解決辦法,就是通過一張表記錄下所有空閑的內(nèi)存,每當(dāng)分配內(nèi)存就更新表上的記錄,這種方式就是空閑列表(free list)。
不管呢種方式,都必須解決線程安全,對(duì)于指針碰撞,為了滿足規(guī)整的先決條件,這就要求GC收集器具有壓縮規(guī)整功能,如serial、par等收集器,而采用mark-sweep的cms這種收集器則不支持規(guī)整,因?yàn)樗褪峭ㄟ^空閑列表方式來整理的內(nèi)存的。分配內(nèi)存就需要對(duì)內(nèi)存指針進(jìn)行操作,如何確保指針的使用是線程安全的?一種做法是用過CAS原子操作來實(shí)現(xiàn),也就是所謂的失敗重試保證更新原子性。還有一種做法就是TLAB(本地線程緩沖),即在堆內(nèi)存中事先劃分一塊線程獨(dú)占的私有內(nèi)存,這樣線程就可以互不干涉的創(chuàng)建對(duì)象了,如果TLAB不夠用,再已加鎖的方式分配TLAB,并且對(duì)象的初始化還可以提前進(jìn)行。
分代劃分收集機(jī)制
目前大部分的GC都是采用分代收集算法的,換而言之,也就是內(nèi)存是分代劃分的。這當(dāng)中的設(shè)計(jì)有很多復(fù)雜和嚴(yán)格的要求,首先對(duì)算法絕對(duì)精確,不能造成誤刪和誤讀,還要保證沒用的對(duì)象及時(shí)回收,以及如何處理產(chǎn)生的碎片和系統(tǒng)停頓開銷等。涉及的指標(biāo)和算法,就在另一篇中單獨(dú)闡述了。
待續(xù)......
到此這篇關(guān)于關(guān)于JVM工作原理簡(jiǎn)述的文章就介紹到這了,更多相關(guān)JVM工作原理簡(jiǎn)述內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java實(shí)現(xiàn)修改PDF文件MD5值且保持內(nèi)容不變
在某些場(chǎng)景中,我們可能需要改變PDF文件的MD5值,而又不希望改變文件的可視內(nèi)容,本文詳細(xì)介紹了如何實(shí)現(xiàn)這一目標(biāo),并提供了具體的Java實(shí)現(xiàn)示例,需要的可以參考下2023-10-10springboot實(shí)現(xiàn)定時(shí)任務(wù)@Scheduled方式
這篇文章主要介紹了springboot實(shí)現(xiàn)定時(shí)任務(wù)@Scheduled方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-07-07Spring Boot集成Redis實(shí)現(xiàn)緩存機(jī)制(從零開始學(xué)Spring Boot)
這篇文章主要介紹了Spring Boot集成Redis實(shí)現(xiàn)緩存機(jī)制(從零開始學(xué)Spring Boot),需要的朋友可以參考下2017-04-04AJAX中Get請(qǐng)求報(bào)錯(cuò)404的原因以及解決辦法
剛學(xué)習(xí)一門技術(shù)時(shí)總會(huì)踩一些坑,下面這篇文章主要給大家介紹了關(guān)于AJAX中Get請(qǐng)求報(bào)錯(cuò)404的原因及解決辦法的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下2023-03-03Java中JDBC連接池的基本原理及實(shí)現(xiàn)方式
本文詳細(xì)講解了Java中JDBC連接池的基本原理及實(shí)現(xiàn)方式,對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-12-12Spring+SpringMVC+JDBC實(shí)現(xiàn)登錄的示例(附源碼)
這篇文章主要介紹了Spring+SpringMVC+JDBC實(shí)現(xiàn)登錄的示例代碼,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2019-05-05