JVM

本文引用了部分網絡材料,僅爲學習,如果冒犯請見諒。

1.1.1  概念

JVM:Java Virtual Mechinal(JAVA虛擬機)。JVMJRE的一部分它是一個虛構出來的計算機本身就是一個計算機體系結構。是通過在實際的計算機上仿真模擬各種計算機功能來實現的。JVM有自己完善的硬件架構,如處理器、堆棧、寄存器等,還具有相應的指令系統。JVM 的主要工作是解釋自己的指令集(即字節碼)並映射到本地的 CPU 的指令集或 OS 的系統調用。Java語言是跨平臺運行的,其實就是不同的操作系統,使用不同的JVM映射規則,讓其與操作系統無關,完成了跨平臺性。JVM 對上層的 Java 源文件是不關心的,它關注的只是由源文件生成的類文件( class file)。類文件的組成包括 JVM 指令集,符號表以及一些補助信息。JVM本身是一個規範,所以可以有多種實現,除了Hotspot外,還有諸如OracleJRockitIBMJ9也都是非常有名的JVM

1.1.2  體系結構

JVM的體系結構如下圖所示。


可以看出,JVM主要由類加載器子系統運行時數據區(內存空間)執行引擎以及與本地方法接口等組成。其中運行時數據區又由方法區、堆、Java棧、PC寄存器、本地方法棧組成。從上圖中還可以看出,在內存空間中,方法區和堆是所有Java線程共享的,而Java棧、本地方法棧、PC寄存器則由每個線程私有。

l  ClassLoader 是負責加載class文件 , class文件在文件開頭有特定的文件標示 , 並且ClassLoader只負責class文件的加載 , 至於它是否可以運行,則由ExecutionEngine決定。

l  Native Interface 是負責調用本地接口的。他的作用是調用不同語言的接口給JAVA用 , 他會在Native Method Stack中記錄對應的本地方法 , 然後調用該方法時就通過Execution Engine加載對應的本地lib。原本多於用一些專業領域, 如JAVA驅動 , 地圖製作引擎等 , 現在關於這種本地方法接口的調用已經被類似於Socket通信 , WebService等方式取代。

l  Execution Engine 是執行引擎 , 也叫Interpreter。Class文件被加載後 , 會把指令和數據信息放入內存中,ExecutionEngine則負責把這些命令解釋給操作系統。

l  Runtime Data Area 則是存放數據的, 分爲五部分:Stack,Heap,Method Area,PC Register,Native Method Stack。

Java語言支持通過JNI(JavaNative Interface)來實現本地方法的調用。但是需要注意到,如果你在Java程序用調用了本地方法,那麼你的程序就很可能不再具有跨平臺性,即本地方法會破壞平臺無關性。

JavaHotSpot Client VM和Java HotSpot Server VM是JDK關於JVM的兩種不同的實現,前者可以減少啓動時間和內存佔用,而後者則提供更加優秀的程序運行速度

 

1.1.3  運行過程

1.1.4  Class Loader

簡單的說,類加載器的作用就是解析.class文件,並在jvm內存的“方法區”中爲其分配內存空間,並形成與該類相關的數據結構(靜態變量、成員變量、成員方法、構造函數)。

1.1.4.1  類加載方式

Java基礎類和擴展類都是預先加載的,而用戶程序類是按需加載的。

1 按加載時間區分

按加載按照加載時機,是否自動加載分爲兩種:預先加載和按需加載。

l  預先加載的類是JVM啓動之後,應用程序運行之前。至少包含rt.jar中的所有類。

l  按需加載則是在程序運行過程中,JVM遇到一個還未被裝載的類,這時由ClassLoader把該類載入內存。

2  按加載方式區分

類加載按照方式來分,也是兩種:隱式加載和顯式加載。

l  隱式加載是通過new的方式,在類初始化時由JVM根據相應的Class Loader將類載入。

l  顯式加載則是程序員在代碼中顯式利用某個ClassLoader將類載入。有兩種方式:

[1].  通過Class.forName()方法動態加載

[2].  通過ClassLoader.loadClass()方法動態加載

 

1.1.4.2  分類

Java 的class loader分爲4類:Bootstrap ClassLoader、Extension ClassLoader、App ClassLoader及User Defined ClassLoader 。

這四類加載器存在父子關係。BootstrapClassLoader是ExtensionClassLoader的parent,Extension ClassLoader是App ClassLoader的parent。但是這並不是繼承關係,只是語義上的定義。基本上,每一個ClassLoader實現,都有一個Parent ClassLoader。詳細內容如下:

l  啓動類加載器(BootStrap ClassLoader):負責加載rt.jar文件中所有的Java類,即Java的核心類(以java.*開頭的包)都是由該ClassLoader加載。在Sun JDK中,這個類加載器是由C++實現的,並且在Java語言中無法獲得它的引用。該類是用特定於操作系統的本地代碼實現的,屬於JAVA虛擬機的內核,Bootstrap類不用專門的類裝載器去進行裝載。加載路徑爲:sun.boot.class.path

l  擴展類加載器(Extension Class Loader):負責加載一些擴展功能的jar包(以javax.*開頭的包以及ext下的包)。Java語言編寫的java類。

l  系統類加載器(System Class Loader):負責加載啓動參數中指定的Classpath中的jar包及目錄。通常我們自己寫的Java類也是由該ClassLoader加載,即當使用java命令去啓動執行一個類時,JAVA虛擬機使用AppClassLoader加載這個類。在Sun JDK中,系統類加載器的名字叫AppClassLoader。它是Java語言編寫的java類。

l  用戶自定義類加載器(User Defined Class Loader):由用戶自定義類的加載規則,可以手動控制加載過程中的步驟。

1.1.4.3  工作原理

類加載分爲裝載、鏈接、初始化三步。如下圖所示:


在加載階段主要用到的是JVM內存中的“方法區”

方法區是可供各條線程共享的運行時內存區域。存儲了每一個類的結構信息,例如運行時常量池(Runtime Constant Pool)、字段和方法數據、構造函數和普通方法的字節碼內容、還包括一些在類、實例、接口初始化時用到的特殊方法。

如果把方法的代碼看作它的“靜態”部分,而把一次方法調用需要記錄的臨時數據看做它的“動態”部分,那麼每個方法的代碼是隻有一份的,存儲於JVM的方法區中;每次某方法被調用,則在該調用所在的線程的的Java棧上新分配一個棧幀,用於存放臨時數據,在方法返回時棧幀自動撤銷。

1.1.4.3.1  裝載

裝載是將指定的java字節碼(.class文件)以二進制的方式加載至JVM內存中,然後將二進制數據流按照字節碼規範解析成jvm內部的運行時的數據結構。java只對字節碼進行了規範,並沒有對內部運行時數據結構進行規定,不同的jvm實現可以採用不同的數據結構。當類被加載以後,在JVM內部就以“類的全限定名+ClassLoader實例ID”來標明類。在內存中ClassLoader實例和類的實例都位於堆中它們的類信息都位於方法區

當一個類的二進制解析完畢後,jvm最終會在堆上生成一個java.lang.Class類型的實例對象,該對象是方法區內這些數據的訪問入口,通過這個對象可以訪問到該類在方法區的內容。

雙親委派模型:裝載過程採用了一種被稱爲“雙親委派模型(Parent Delegation Model”的方式,當一個ClassLoader要加載類時,它會先請求它的父ClassLoader加載類,而它的雙親ClassLoader會繼續把加載請求提交再上一級的ClassLoader,直到啓動類加載器。只有其雙親ClassLoader無法加載指定的類時,它纔會自己加載類。

雙親委派模型是JVM的第一道安全防線,它保證了類的安全加載,這裏同時依賴了類加載器隔離的原理:不同類加載器加載的類之間是無法直接交互的,即使是同一個類,被不同的ClassLoader加載,它們也無法感知到彼此的存在。這樣即使有惡意的類冒充自己在覈心包(例如java.lang)下,由於它無法被啓動類加載器加載,也造成不了危害。

由此也可見,如果用戶自定義了類加載器,那就必須自己保障類加載過程中的安全。

全盤委託機制:當一個classloader加載一個Class的時候,這個Class所依賴的和引用的所有 Class也由這個classloader負責載入,除非是顯式的使用另外一個classloader載入。

流程參見下圖。


1.1.4.3.2  鏈接

鏈接的任務是把二進制的類型信息合併到JVM運行時狀態中去。鏈接分爲以下三步:

1)       驗證:校驗.class文件的正確性,保證加載的字節碼符合java語言的規範,並且不會給虛擬機帶來危害。比如驗證這個類是不是符合字節碼的格式、變量與方法是不是有重複、數據類型是不是有效、繼承與實現是否合乎標準等等。按照驗證的內容不同又可以細分爲4個階段:文件格式驗證(這一步會與裝載階段交叉進行),元數據驗證,字節碼驗證,符號引用驗證(這個階段的驗證往往會與解析階段交叉進行)。

2)       準備:爲類的靜態變量分配內存,並設置jvm默認的初始值。對於非靜態的變量,則不會爲它們分配內存。

在jvm中各類型的初始值如下:

int,byte,char,long,float,double默認初始值爲0

boolean 爲false(在jvm內部用int表示boolean,因此初始值爲0)

reference類型爲null

對於final static基本類型或者String類型,則直接採用常量值(這實際上是在編譯階段就已經處理好了)。

3)       解析(可選):主要是把類的常量池中的符號引用解析爲直接引用,這一步可以在用到相應的引用時再解析。

l  解析過程主要針對於常量池中的

CONSTANT_Class_info,CONSTANT_Fieldref_info,CONSTANT_Methodref_info及CONSTANT_InterfaceMethodref_info四種常量。

l  jvm規範並沒有規定解析階段發生的時間,只是規定了在執行anewarray,checkcast,getfield,getstatic,instanceof,invokeinterface,invokespecial,invokespecial,invokestatic,invokevirtual,multinewaary,new,putfield,putstatic這13個指令應用於符號指令時,先對它們進行解析,獲取它們的直接引用。

l   jvm對於每個加載的類都會有在內部創建一個運行時常量池(參考上面圖示),在解析之前是以字符串的方式將符號引用保存在運行時常量池中,在程序運行過程中當需要使用某個符號引用時,就會促發解析的過程,解析過程就是通過符號引用查找對應的類實體,然後用直接引用替換符號引用。由於符號引用已經被替換成直接引用,因此後面再次訪問時,無需再次解析,直接返回直接引用。

1.1.4.3.3  初始化(僅在滿足條件時才進行此步驟

初始化階段是根據用戶程序中的初始化語句爲類的靜態變量賦予正確的初始值。這裏初始化執行邏輯最終會體現在類構造器方法<clinit>()方中。該方法由編譯器在編譯階段自動生成,它封裝了兩部分內容:靜態變量的初始化語句和靜態語句塊。

JVM規範嚴格定義了何時需要對類進行初始化:

a、通過new關鍵字、反射、clone、反序列化機制實例化對象時。

b、調用類的靜態方法時。

c、使用類的靜態字段或對其賦值時。

d、通過反射調用類的方法時。

e、初始化該類的子類時(初始化子類前其父類必須已經被初始化)。

f、JVM啓動時被標記爲啓動類的類(簡單理解爲具有main方法的類)

之後,執行引擎將JAVA的字節碼轉換爲host system的二進制碼,然後交給操作系統執行,也就是從main方法開始執行。

1.1.5  Run time area(內存結構)

運行數據區(runtime data area)是jvm管理的內存空間。編譯後的程序都被加載到這裏,之後纔開始運行。其結構圖如下所示:


結合垃圾回收機制,將堆和虛擬機棧進行細化:

 

 

表1‑1 Runtime data area

序號

名詞

解釋

1.         

PROGEAM COUNTER REGISTER

(程序計數器)

每一個用戶線程對應一個程序計數器,用來指示當前線程所執行字節碼的行號(.class文件中的位置)。由程序計數器給文字碼解釋器提供下一條要執行的字節碼的的位置。根據jvm規範,在這個區域中不會拋出OutOfMemoryError的內存異常。

2.         

JAVA STACK

java虛擬機棧)

線程私有(線程安全)分爲三部分:局部變量區、操作數棧、幀數據區,可能連續也可能不連續.

最典型的應用是方法調用。Java棧由棧幀組成,一個幀對應一個方法調用。調用方法時壓入棧幀,方法返回時彈出棧幀並拋棄。Java棧的主要任務是存儲方法參數、局部變量、中間運算結果,並且提供部分其它模塊工作需要的數據。前面已經提到Java棧是線程私有的,這就保證了線程安全性,使得程序員無需考慮棧同步訪問的問題,只有線程本身可以訪問它自己的局部變量區

3.         

HEAP

(堆)

線程共享

所有的對象實例以及數組都要在堆上分配

回收器主要管理的對象

4.         

MEATHOD AREA

(方法區)

線程共享的內存區域

非堆主要區域

存儲類信息、常量、靜態變量,即編譯器編譯後的代碼

5.         

NATIVE METHOD STACK

(本地方法棧)

爲虛擬機使用到的Native 方法服務

幾點說明:

方法區和堆是線程共享的,所有的運行在jvm上的程序都能訪問這兩個區域。堆,方法區和虛擬機的生命週期一樣,隨着虛擬機的啓動而存在,而棧和程序計數器依賴用戶線程的啓動和結束而建立和銷燬,即每個線程都有自己的棧、程序計數器和本地方法棧


JVM是基於棧執行的,每個線程會jvm stack中建立一個每個棧又包含了若干個棧幀(每個方法的執行都會創建一個棧,每個棧幀包含了局部變量、操作數棧、動態連接、方法的返回地址信息等每一個方法被調用直至執行完成的過程,就對應着一個棧幀在當前線程所在棧中從入棧到出棧的過程。

 

除了pc計數器區,其他區都有可能產生oom,棧區還有可能stackoverflow

 

1.1.6  執行引擎(excution engine)

 

執行引擎是JVM執行Java字節碼的核心,執行方式主要分爲解釋執行、編譯執行、自適應優化執行、硬件芯片執行方式。

JVM的指令集是基於棧而非寄存器的,這樣做的好處在於可以使指令儘可能緊湊,便於快速地在網絡上傳輸(別忘了Java最初就是爲網絡設計的),同時也很容易適應通用寄存器較少的平臺,並且有利於代碼優化,由於Java棧和PC寄存器是線程私有的,線程之間無法互相干涉彼此的棧。每個線程擁有獨立的JVM執行引擎實例

JVM指令由單字節操作碼和若干操作數組成。對於需要操作數的指令,通常是先把操作數壓入操作數棧,即使是對局部變量賦值,也會先入棧再賦值。注意這裏是“通常”情況,之後會講到由於優化導致的例外。

程序的執行可以直接解釋爲是對方法的遞歸調用,通過一連串的方法鏈來最終得出執行結果,亦即是說虛擬機對程序的執行,根本上是對方法的調用和執行。

 棧幀(Stack Frame)是用於支持虛擬機進行方法調用和方法執行的數據結構,是虛擬機運行時數據區中的虛擬機棧的棧元素。棧幀存儲了方法的局部變量表(最小單位爲變量槽VariableSlot)、操作數棧、動態連接和方法返回地址等信息,每一個方法從調用開始到執行完成的過程,就對應着一個棧幀在虛擬機棧裏面從入棧到出棧的過程。棧幀的內容在編譯時就已經完成確定,不受程序運行期變量數據的影響,僅取決於具體的虛擬機實現。
發佈了18 篇原創文章 · 獲贊 1 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章