淺談JVM及原理
1、什麽是JVM ?
JVM, 中文名是Java虛擬機, 正如它的名字, 是一個虛擬機器,來模擬通用的物理機。 JVM是一個標準,一套規範, 規定了.class文件在其內部運行的相關標準和規範。 及其相關的內部構成。 比如:所有的JVM都是基於棧結構的運行方式。那麽不符合這種要求的,不算是JVM, 如Android中所使用的Dalvik 虛擬機就不能稱作是JAVA 虛擬機, 因為它是基於寄存器(最新的Android系統據說已經放棄了Dalvik VM, 而是使用ART)。
JVM相關的產品有很多, 通常最有名的莫過於現在Oracle公司所有的HotSpot 虛擬機。因此, 這裏討論的都是HotSpot虛擬機, 如果沒有特別說明。
2、類加載?
類加載, 是通過JVM的類加載器從JVM外部以二進制字節流的方式加載到JVM中。但JVM本身有至少三種類加載器:BootStrap(根類加載器,C++實現, 加載位於jre/lib/rt.jar)、Extension(擴展類加載器, 主要用於加載jre/lib/ext/下的jar)、System(加載classpath環境變量所指定的class);當然還有,自定義的類加載器(用於實現自己的類加載器, 如Tomcat中就實現多個類加載器,用來管理不同的jar)。
如果, 我有一個HelloWorld的類需要加載, 首先類加載器會去從最底層的類加載器去驗證這個類是否被加載, 如果沒有, 則委托給上一次的類加載器驗證是否被加載, 如果到BootStrap類加載器都沒有發現HelloWorld類被加載, 那麽類加載器將執行加載任務, 如果根類加載器沒有加載, 則委托給下一級的Extension類加載器去嘗試加載,直到這個類被加載成功。 參考下圖:
需要註意的是:如果一個類被不同的類加載器加載, 那麽就是兩個不同的類。
3、類加載的具體過程?
被java編譯器(不僅限於, 還有其他任何的可以編輯成為.class的編譯器)編譯過的.class文件(可能是以jar、war、jsp等形式), 經過類加載器加載 、 驗證、準備、解析、初始化之後, 才可以被使用。基本的過程如下:
-
加載: 首先,通過一個類的全類名來獲取此類的二進制字節流。其次,將類中所代表的靜態存儲結構轉換為運行時數據結構, 最後,生成一個代表加載的類的java.lang.Class對象, 作為方法區這個類的所有數據的訪問入口。加載完成之後, 虛擬機外部的二進制靜態數據結構就轉換成了虛擬機所需要的結構存儲在方法區中(至於如何轉換, 則由具體虛擬機自己定義實現), 而所生成的Class對象, 則存放在方法區中, 用來作為程序訪問方法區中數據的外部接口。
-
驗證:其目的就是保證加載進來的.class文件不會危害到虛擬機本身, 且內容符合當前虛擬機規範要求。主要驗證的內容大致有:文件格式、元數據驗證、字節碼驗證、符號引用驗證。其中文件格式驗證, 主要確保符合class文件格式規範(如文本後綴為.class的文件將驗證不通過), 以及主次版本號, 驗證是否當前JVM可以處理等。元數據驗證,主要驗證編譯後的字節碼描述信息是否符合java語法規範。字節碼驗證, 其最為復雜, 主要通過控制流和數據流確定語義是否合法、符合邏輯。符號引用驗證,可以看做是除自身以外(常量池中各種引用符號)的信息匹配校驗,如通過持有的引用能否找到對應的實例。
-
準備:正式為類變量分配內存,並設置類變量的初始值。這些變量都會在方法區中進行分配。
-
解析:將常量池內的符號引用替換為直接引用的過程。主要針對類或接口、字段、類方法、接口方法、方法類型、方法句柄等。
-
初始化:加載的最後階段, 程序真正運行的開始。
4、java運行時數據區 ?
既然類以及加載到JVM中, 那麽數據如何真正的運行?如下圖:
類加載進來, JVM是通過上圖所示的區域來運行和管理這些加載進來的CLASS。即程序運行的是時候, 由上面邏輯單元來運行程序, 包括:方法區、堆、本地方法棧、棧、程序計數器(PC)五大部分組成(有些VM說常量池也是其中的一個單元, 但是HotSpot VM中的常量池是方法區中的一部分)。(註意線程共享)
-
程序計數器 (PC):可以看做是當前線程執行字節碼的行號指示器。字節碼解釋器工作的時候就是通過這個計數器的值來選取下一條需要執行的字節碼指令, 分支, 循環、跳轉、異常處理、線程恢復等基礎功能依賴計數器完成。
-
虛擬機棧:和計數器一樣, 也是線程私有的,生命周期同線程一致。每個方法在執行時,都會創建一個棧幀,用於存儲局部變量表、操作數棧、動態鏈接、方法出口等信息。方法調入則入棧, 方法執行完則出站。局部變量表存儲各種基本類型數據(java的8種,其中long,double占用2個局部變量控件,其余數據占用1個)、對象引用(reference類型)。局部變量表所需的內存空間在編譯期間完成分配,當進入一個方法時, 這個方法需要在幀中分配多大的局部變量空間是完全確定的。在方法運行期間是不會改變局部變量表的大小的。
-
本地方法棧:此棧和JVM棧作用非常類似, 不同在於本地方法棧為虛擬機使用到的Native方法服務, 而JVM棧則是為Java執行的方法服務。Sun HotSpot虛擬機, 直接把本地方法棧和虛擬機棧合二為一。本地方法棧也會拋出StackOverFlowError和OutOfMemoryError異常。
-
Java堆:是JVM管理內存中最大的一塊。被所有線程共享一塊區域。堆是GC垃圾收集器管理的主要區域。從內存回收角度看, java堆被分為新生代、老年代, 再細致一點有其他的劃分。這些目的主要就是更快的分配和回收內存。
-
方法區:和java堆相同, 線程共享區域, 用來存儲已被虛擬機加載的類信息, 常量、靜態變量、即時編譯器編譯後的代碼等數據。有人稱作此方法區為“永久帶”, 本質上不等價,只是HotSpot VM將GC分代收集擴展到了方法區,這樣HotSpot的垃圾收集器管理方法區和管理java堆一樣(優點:不用專門為方法區寫一套垃圾收集器, 缺點:容易導致內存溢出)。官方現在擁也有放棄永久帶並改為采用Native Memory來實現方法區的計劃,目前已經發布的JDK7中的HotSpot中, 已經將原本放在方法區中的字符串常量池移出了。
-
運行時常量池:是方法區的一部分。Class文件中除了有類的版本、字段、方法、接口等描述外,還有一項就是常量池, 用於存放編譯期間生成的各種字面量和符號引用 ,這部分內容在類加載後進入方法區的運行時常量池中存放。
5、垃圾收集?
在java運行時區域中, 程序計數器、虛擬機棧、本地方法棧3個區域隨線程而生,隨程而滅。因為這幾個區域
的內存分配和回收都是具有確定性,這幾個區域不需要過多考慮回收的問題。因為方法結束之後或線程結束之後,
內存自然就跟著回收了(這不是絕對的, 因為如果當棧內存中的引用很消耗內存的時候, 需要手動將引用置為null,
以便垃圾收集器回收大對象)。而java堆和方法區不一樣,一個接口中的多個實現類需要的內存可能不一樣, 一個
方法中的多個分支需要的內存也可能不一樣,我們只有在程序處於運行期間時,才知道會創建哪些對象, 垃圾收集
器關註的就是這部分內存。其也是動態的。
垃圾收集器的區域如下圖:
垃圾收集本是有一套非常復雜的算法, 如果在方法區中(HotSpot VM中的永久帶)進行垃圾收集, 那麽其性價比極
底的,因為垃圾回收主要收集永久帶中的兩部分內容:廢棄的常量和無用的類。回收永久帶中的常量和方法區非常相
似。但是在堆中, 尤其是在新生代中,常規應用進行一次垃圾收集, 一般可以回收70%——95%的空間。而永久帶
的垃圾收集要遠地與此。
如上圖所示, 每一個黑框中都是一個垃圾收集器, 對應特定的垃圾收集算法, 來挺高整體的垃圾收集效率。
淺談JVM及原理