瞭解JVM的工做機制可以更好的幫助咱們理解java語言自己,規避各類可能的錯誤。因此,今天趁此機會好好複習一下。來看看JVM是怎麼工做的。java
1、啥是JVMsql
JVM能夠理解爲用來運行java程序的一種運行時引擎(run-time engine)。沒錯,main方法就是由jvm調用做爲入口方法的。JVM是JRE的重要組成部分。bootstrap
JVM爲java程序提供了一次編寫,處處運行的基礎。api
當咱們編譯一個java文件時,就由編譯器生成一個對應的class文件,當咱們運行class文件時,JVM會對class文件進行一系列的調度工做。這就是JVM的職責所在。因此,想要弄清楚JVM的工做原理,就要弄清java程序在運行過程當中每一步都發生了啥子。下圖是JVM的一張架構圖,預覽一下~架構
2、JVM的職責oracle
JVM的職責主要包括三部分app
裝載(Loading)jvm
連接(Linking)ui
初始化(Intialization)spa
2.1 裝載
類裝載器(Class Loader)讀取class文件,生成對應的二進制數據並將其存儲在方法區中(method Area)。
對於每一個class文件,JVM都會存儲以下信息:
① 裝載類的徹底限定名(Fully qualified name)和他們的直接父類
② class文件是否關聯於另外一個類或者接口或者枚舉
③ 修飾符,變量和方法信息等等
裝載class文件後,JVM會在堆區中建立一個Class類型來表明這個文件的類信息。是的,這個Class類型就是咱們日常說的在java.lang包中預約義的那個Class。具體怎麼用這個Class類型,請參考相關文檔。注意,對於每個class文件,只會有一個Class對象被建立,是一對一的關係。
2.2 連接
連接階段主要是進行驗證(Verification),準備(preparation),和解析(resolution)
① 驗證:它主要是確保每個class文件的正確性,如class文件的格式是否符合標準?是否由有效的編譯器生成?若是驗證失敗,咱們將會收到java.lang.VerifyError的運行時異常
② 準備:JVM爲類變量分配存儲空間,而且將他們初始化爲默認值。
③ 解析:主要是用來將符號引用替換爲直接引用。經過搜索方法區來找到對應定位須要引用的實體。
2.3 初始化
在這個階段,全部的靜態變量都會初始化爲咱們在代碼中定義的值。執行的順序是:在單個類中,從上到下,在類層次中,由父類到子類。
接下來就輪到具體的類裝載器了。通常而言,分爲三種類裝載器
① 根類裝載器 (Bootstrap class loader):每個JVM都必須有一個根類裝載器,用來裝載可信任的類,例如它會裝載位於JAVA_HOME/JRE/lib中的java api類。這個路徑也常被稱做根目錄(bootstrap path)。注意,根類裝載器不是由JAVA語言編寫,而是由本地語言(Native languages)例如C和C++編寫而成。
② 擴展裝載器(Extension class loader):它是根裝載器的子裝載器。主要用於裝載位於JAVA_HOME/jre/lib/ext(也叫做擴展路徑:Extension path)的各類類,或者也能夠由系統屬性java.ext.dirs來指定加載路徑。這個裝載器是由JAVA語言編寫,實現類爲sun.misc.Launcher$ExtClassLoader。
③ 系統/應用類裝載器(System/Application class loader): 它是擴展裝載器的子裝載器。主要負責加載位於應用類路徑(application class path)下的類文件。這個是由咱們本身指定。默認狀況下,會使用環境變量的值,該環境變量記錄了java.class.path的值。該裝載器也是由JAVA語言編寫而成,實現類爲sun.misc.Launcher$ExtClassLoader。
須要特別說明的是,JVM採起了一種委託-分層(Delegation-Hierarchy)機制來加載類文件。系統類裝載器將裝載請求委託給擴展類裝載器,而後再交給根類裝載器。若是在根類路徑下面發現了須要的類文件,那麼就直接加載,不然,會將加載請求回傳給擴展類裝載進行查找,若是還找不到就回傳給系統類裝載器。若是連繫統類裝載器都沒法加載,那麼咱們就會獲得一個運行時錯誤:java.lang.ClassNotFoundException。
3、JVM的內存分佈
JVM內存的分佈狀況也是咱們須要重點關注的。主要分爲以下幾類:
① 方法區(Method Area):在方法區中,包括全部類層次的信息如類名,直接父類名,類方法,成員變量,靜態變量等等。每個JVM只能擁有一個方法區,且方法區是一個共享資源,可供其餘資源訪問調用。
② 堆區(Heap Area):全部的對象都存儲在堆區中。每個JVM也只能有一個堆區。它也具備共享性。
③ 棧區 (Stack Area):對於每個線程,JVM都會在此處建立一個運行時棧。每個棧塊又叫作活動記錄/棧幀(activation record/stack frame),它們都記錄了方法調用的狀況,全部屬於該方法的局部變量都存於對應的棧幀中。線程結束後,棧幀會被銷燬。棧區不是共享資源,它由對應的線程獨享。
④ PC寄存器(PC registors): 用於存儲線程的當前的執行指令的地址。很明顯,每一個線程都有屬於本身的PC寄存器。
⑤ 本地方法棧(Native method stacks):對於每一個線程,都會分別建立一個本地方法棧。主要用於存儲本地方法的調用狀況。
4、執行引擎(Execution Engine)
執行引擎用來執行class文件(字節碼)。執行器一行一行的讀取字節碼,結合當前內存區存儲的各類數據和信息,執行相關指令。執行器也能夠分爲三個部分
① 解釋器(Interpreter):用來逐行解釋字節碼並執行。缺點就是若是同一個方法調用屢次,每一次都仍是須要進行解釋,效率低下浪費資源。
② 實時編譯器 (JIT- Just In Time Compiler):主要用來提高解釋器的執行效率。它會將整個的字節碼進行編譯並將其轉化爲本地碼(Native Code)。如此一來,每當解釋器看到對應的方法調用時,均可以直接使用JIT提供的本地碼而不須要再次進行解釋。從而也提高了效率。
③ 垃圾回收器(Garbage Collector):主要用於銷燬沒有引用的對象。
5、常見問題
5.1 上文中出現的「本地方法」究竟是個什麼東西?
這其實和一個關鍵的概念有關:JNI。JNI也叫java本地接口,該接口可以與本地方法(Native Method Libraries)庫進行交互,爲程序的執行提供本地庫(如C/C++)的支持。它可以讓JVM調用C/C++庫或者被C/C++庫調用。後者主要是在硬件特性比較特殊的狀況下使用。
本地方法庫主要是指本地庫(C/C++)的集合,執行器會用到這些庫。
native這個詞其實也是JAVA語言的關鍵詞之一。通常狀況下用的比較少。可是它有以下幾個做用:
It allows you to:
This could be used to:
with the tradeoff of lower portability.
5.2 靜態變量會被垃圾回收器回收嗎?
當類被加載後,靜態變量屬於上文的方法區,很明顯,他不會被回收。可是,除非用來加載這個類的類加載器被回收了,那這個靜態方法也就會被回收。如此,通常狀況下就能夠理解爲never。也請注意,根類裝載器是不會被回收的。
5.3 Class.forName都幹了些啥?
它啊,就是用於加載類啊,而且初始化靜態變量。
主要是有不少人問到在老版本的jdbc爲啥須要Class.forName("oracle.jdbc.driver.OracleDriver")這個東東。
注意啊,這個是老版本的需求,如今已經不用了:
In previous versions of JDBC, to obtain a connection, you first had to initialize your JDBC driver by calling the method
Class.forName
. This methods required an object of typejava.sql.Driver
. Each JDBC driver contains one or more classes that implements the interfacejava.sql.Driver
.
...
Any JDBC 4.0 drivers that are found in your class path are automatically loaded. (However, you must manually load any drivers prior to JDBC 4.0 with the methodClass.forName
.)
說白了用Class.forName就是保證在不用顯式import的狀況下來使用對應的類,也便是說,能夠在classpath下不存在相關類文件時,仍舊能夠構建項目,加載指定類文件。