JVM是一種用於計算設備的規範,它是一個虛構出來的計算機,是經過軟件模擬物理機器執行程序的執行器.
JVM屏蔽了與具體操做系統平臺相關的信息,使Java程序只需生成在JVM上運行的字節碼,就能夠在多種平臺上不加修改地運行.
JVM在執行字節碼時,實際上最終仍是把字節碼解釋成具體平臺上的機器指令執行.java
Java字節碼是一種運行於Java和機器語言的中間語言,Java字節碼也是部署Java程序的最小單元.
JVM自己就是用於執行Java字節碼的執行器,因此'.java'源碼文件要先編譯爲'.class'二進制字節碼.編程
ps. javap -c/-verbose 能夠將'.class'已可閱讀方式輸出
數組
生成的'.class'文件由如下幾部分組成 :
緩存
- 結構信息 : 包括class文件格式版本號及各部分的數量與大小的信息.
- 元數據 : 對應於Java源碼中聲明與常量的信息.包含類/繼承的超類/實現的接口的聲明信息、域與方法聲明信息和常量池.
- 方法信息 : 對應Java源碼中語句和表達式對應的信息.包含字節碼、異常處理器表、求值棧與局部變量區大小、求值棧的類型記錄、調試符號信息.
Java字節碼中有4中表示調用方法的操做碼 :
安全
- invokeinterface: 調用接口方法
- invokespecial: 調用初始化方法、私有方法、或父類中定義的方法
- invokestatic: 調用靜態方法
- invokevirtual: 調用實例方法
注意! 類加載的幾個階段是按順序開始,而不是按順序進行或完成.
由於這些階段一般都是互相交叉地混合進行的,一般在一個階段執行的過程當中調用或激活另外一個階段.bash
中文名稱 | 實現語言 | 做用 |
---|---|---|
根加載器 | C++ | 在運行JVM時建立,用於加載JavaAPIs,包括Object類,不是ClassLoader子類 |
擴展加載器 | Java | 用於加載除基本JavaAPIs之外擴展類,也用於加載各類安全擴展功能 |
系統加載器 | Java | 加載應用程序相關的類與用戶指定的ClassPath裏的類 |
用戶自定義加載器 | Java | 應用程序根據自身須要自定義的ClassLoader,如Tomcat、JBoss會根據J2EE規範自行實現ClassLoader |
ps. 加載過程當中會先檢查類是否被已加載,檢查順序是自底向上(見上圖), 只要某個Classloader已加載就視爲已加載此類,保證此類只全部 ClassLoader加載一次. 而加載的順序是自頂向下,也就是由上層來逐層嘗試加載此類(見上圖).
2.2.1. 驗證(Verifying) :網絡
驗證類是否符合Java規範和JVM規範(編譯階段的語法語義分析不一樣).
大部分TCK的測試用例都用於檢測對於給定的錯誤的類文件是否能獲得相應的驗證錯誤信息.數據結構
TCK(Technology Compatibility Kit),由Oracle提供的測試工具.多線程
TCK經過執行大量的測試用例(包括大量經過不一樣方式生成的錯誤類文件)來驗證JVM規範.
只有經過TCK測試的JVM才能被稱做是JVM.架構
相似TCK,還有一個JCP(http://jcp.org),用於驗證新的Java技術規範.
對於一個JCP,必須具備詳細的文檔,相關的實現以及提交給JSR的TCK測試.
若是用戶想像JSR同樣使用新的Java技術,那他必須先從RI提供者那裏獲得許可,或者本身直接實現它並對之進行TCK測試.
ps. 通常狀況由javac編譯的class文件是不會有問題的, 但可能有人的class文件是經過其餘方式編譯出來的, 這就有可能不符合JVM的編譯規則,就須要過濾掉這部分不合法文件
2.2.2. 準備(Preparing) :
根據內存需求準備相應的數據結構,並分別描述出類中定義的字段、方法以及實現的接口信息.
被final修飾的靜態變量,會直接賦值爲用戶的定義值.
爲類的靜態變量分配內存,並設置類變量的初始值爲默認值(不初始化靜態代碼塊).
ps. '內存分配'僅包括類的靜態變量,不包括實例變量,實例變量會在對象實例化時隨對象一塊分配在Java堆中
基本數據類型與referece的默認值,以下 :
數據類型 | 默認零值 |
---|---|
int | 0 |
long | 0L |
short | (short)0 |
char | '\u0000' |
byte | (byte)0 |
boolean | false |
float | 0.0f |
double | 0.0d |
reference | null |
注意!
- 就基本數據類型來講,對於類變量(static)和全局變量,若是不顯式地對其賦值而直接使用,則系統會爲其賦予默認的零值,而對於局部變量來講,在使用前必須顯式地爲其賦值,不然編譯時不經過.
- 對於同時被static和final修飾的常量,必須在聲明的時候就爲其顯式地賦值,不然編譯時不經過,而被final修飾的常量則既能夠在聲明時顯式地爲其賦值,也能夠在類初始化時顯式地爲其賦值.總之,在使用前必須爲其顯式地賦值,系統不會爲其賦予默認零值.
- 對於引用數據類型reference來講,如數組引用、對象引用等,若是沒有對其進行顯式地賦值而直接使用,系統都會爲其賦予默認的零值,即null.
- 若是在數組初始化時沒有對數組中的各元素賦值,那麼其中的元素將根據對應的數據類型而被賦予默認的零值.
2.2.3. 解析(Resolving) :
將常量池中的全部符號引用(字面量描述)轉爲直接引用(對象和實例的地址指針、實例變量和方法的偏移量).
能夠認爲一些靜態綁定的會被解析,動態綁定則只會在運行時進行解析.靜態綁定包括一些final方法(不能夠重寫)、static方法(只會屬於當前類)、構造器(不會被重寫).
這是類加載的最後階段.爲類的變量初始化合適的值.
若是執行的是靜態變量,那麼就會使用用戶指定的值覆蓋以前在準備階段設置的初始值.
若是執行的是static代碼塊,那麼在初始化階段,JVM就會執行static代碼塊中定義的全部操做.
注意!
- JVM必須確保一個類在初始化的過程當中,若是是多線程須要同時初始化它,僅僅只能容許其中一個線程對其執行初始化操做,其他線程必須等待,只有在活動線程執行完對類的初始化操做以後,纔會通知正在等待的其餘線程.
- 非靜態類在實例化類,在Java堆中建立對象的時候,纔會進行初始化,
即在類被Java程序"第一次主動使用"的時候,纔會觸發初始化操做(若是尚未加載,則會順勢觸發類的加載過程).
Heap劃分爲兩大塊 :
# 其分爲如下幾塊區域 # Eden Space : 任何新進入運行時數據區域的實例都會存放在此 # S0 Survivor Space : 存在時間較長,通過垃圾回收沒有被清除的實例,就從Eden搬到了S0 # S1 Survivor Space : 存在時間更長的實例,就從S0搬到了S1
全部新建立的Object都將會存儲在新生代中.晉升到老年代有如下幾種狀況 :
- 每經歷一次垃圾回收,對象的年齡加1(首次進Survivor區後初始年齡爲1),當增長至必定程度(默認爲15)時,晉升爲老年代.
- 當一次Minor GC後,對象不夠Survivor區徹底容納,會直接晉升爲老年代.
- 當新生代的相同年齡的對象超過Survivor區的50%時,年齡大於或等於其相同年齡的對象,會直接晉升爲老年代.
# Tenured : 主要存放應用程序中生命週期長的內存對象
老年代的對象比較穩定,因此Major GC不會頻繁執行.觸發Major GC(Full GC)有如下狀況 :
- 當有新生代的對象晉升入老年代,致使空間不夠用時才觸發Major GC.
- 當沒法找到足夠大的連續空間分配給新建立的較大對象時也會提早觸發一次Major GC進行垃圾回收騰出空間.
- 發生Minor GC時,JVM會檢測以前每次晉升到老年代的平均大小是否大於老年代的剩餘空間大小,
若是大於,則進行一次Major GC,
若是小於,則查看HandlePromotionFailure設置是否容許擔保失敗,
若是容許,那隻會進行一次Minor GC,
若是不容許,則改成進行一次Major GC.
Major GC的耗時比較長,須要先掃描再回收,且爲了減小內存碎片致使的內存損耗,通常都須要進行合併整理方便下次直接分配.
老年代也會存在內存容量不過的狀況,也會拋出OutOfMemoryError異常.
ps. JDK1.8,方法區(HotSpot的永久代(Permanent Generation)),已替換爲元空間(Metaspace),使用的是直接內存,受本機可用內存的限制,而且永遠不會獲得java.lang.OutOfMemoryError(可限制大小).
ps. JDK1.7及以後版本的JVM已經將運行時常量池從方法區中移了出來,在Java Heap中開闢了一塊區域存放運行時常量池
Class文件中有類的版本、字段、方法、接口等描述信息外,還有常量池信息(見上圖,用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後存放到常量池中).
當常量池沒法再申請到內存時是會拋出OutOfMemoryError異常.
ps. 運行時常量池中的內容主要是從各個類型的class文件的常量池中獲取
!注意!
運行時常量是相對於常量來講的,它具有一個重要特徵是 : 動態性
值相同的動態常量與咱們一般說的常量只是來源不一樣,可是都是儲存在池內同一塊內存區域.
Java並不要求常量必定只能在編譯期產生,運行期間也可能產生新的常量,這些常量被放在運行時常量池中.
這裏所說的常量包括:基本類型包裝類(包裝類無論理浮點型,整形只會管理-128到127)和String類(也能夠經過String.intern()方法能夠強制將String放入常量池).
常量池是爲了不頻繁的建立和銷燬對象而影響系統性能,其實現了對象的共享.
例如字符串常量池,在編譯階段就把全部的字符串文字放到一個常量池中.節省內存空間:常量池中全部相同的字符串常量被合併,只佔用一個空間.
節省運行時間:比較字符串時,==比equals()快.對於兩個引用變量,只用==判斷引用是否相等,也就能夠判斷實際值是否相等.
每一個線程都會有一個程序計數器,各線程之間計數器互不影響,獨立儲存,咱們稱這類內存區域爲"線程私有"的內存.
程序計數器是一塊較小的內存空間.當字節碼解釋器工做時,經過改變這個計數器的值來選取下一條須要執行的字節碼指令.
程序計數器主要有兩個做用 :
- 字節碼解釋器經過改變程序計數器來依次讀取指令,從而實現代碼的流程控制,如:順序執行、分支、選擇、循環、跳轉、異常處理、線程恢復等基礎功能.
- 在多線程的狀況下,程序計數器用於記錄當前線程執行的位置,從而當線程被切換回來的時候可以知道該線程上次運行到哪兒了.
ps. 程序計數器是惟一一個不會出現OutOfMemoryError的內存區域,它的生命週期隨着線程的啓動而建立,隨着線程的結束而死亡.
ps. 若下一步執行的指令爲Native的話,則PC寄存器中不存儲任何信息.
爲非Java編寫的本地代程定義的棧空間.也就是說它基本上是用於經過JNI(Java Native Interface)方式調用和執行的C/C++代碼,根據具體狀況,C棧或C++棧將會被建立.
此區域還用於存儲每一個native方法調用的狀態.
本地方法棧與虛擬機棧所發揮的做用很是類似,區別是:
- 虛擬機棧爲虛擬機執行Java方法服務,而本地方法棧則爲虛擬機使用到的Native方法服務.
- 本地方法被執行的時候,在本地方法棧也會建立一個棧幀,用於存放該本地方法的局部變量表、操做數棧、動態連接、出口信息.
- 方法執行完畢後相應的棧幀也會出棧並釋放內存空間,也會出現StackOverFlowError和OutOfMemoryError兩種異常.
ps. 本地方法棧在HotSpot JVM中與虛擬機棧合二爲一
虛擬機棧是線程私有的,不能被任何其餘線程引用,並跟隨線程的啓動而建立.其中存儲的數據無素稱爲棧幀(Stack Frame).
虛擬機棧會擁有多個棧幀(Stack Frame).JVM會把棧楨壓入虛擬機棧或從中彈出一個棧幀.
若是有任何異常拋出,如printStackTrace()方法輸出的棧跟蹤信息的每一行表示一個棧幀.
注意. 1. 若是線程中的計算須要比容許的更大的虛擬機棧,則JVM會拋出一個StackOverflowError. 2. 若是能夠動態擴展虛擬機棧,而且嘗試進行擴展但內存不足以實現擴展,或者能夠內存不足覺得新線程建立初始虛擬機棧,則JVM拋出一個OutOfMemoryError.
棧幀用於存儲數據和部分結果、動態連接、返回地址、調度異常以及屬於當前運行方法的運行時常量池的引用等信息.
本地變量數組和操做數棧的大小在編譯時就已肯定,因此屬在運行時屬於方法的棧幀大小是固定的.
因爲除了推送和彈出幀以外,永遠不會直接操做虛擬機棧.虛擬機棧的內存不須要是連續的.
ps.不管是'return語句'仍是'拋出異常',無論哪一種返回方式都會致使棧幀被彈出.
每一個棧幀包含一個稱爲局部變量的變量數組,用於存放方法參數和方法內定義的局部變量.
幀的局部變量數組的長度在編譯時肯定,並以類或接口的二進制表示形式提供,同時提供與幀相關的方法的代碼.
JVM使用局部變量在方法調用上傳遞參數.在類方法調用中,任何參數都從局部變量0開始的連續局部變量中傳遞.
在實例方法調用中,局部變量0老是用於傳遞對調用實例方法的對象的引用.
隨後,任何參數都在從局部變量1開始的連續局部變量中傳遞,以及其後的就是真正的方法的本地變量.
局部變量表的容量以變量槽(Variable Slot)爲最小單位 :
一個Slot能夠存放一個32位之內(boolean、byte、char、short、int、float、reference和returnAddress)的數據類型,reference類型表示一個對象實例的引用,returnAddress已經不多見了,能夠忽略.
對於64位的數據類型(Java語言中明確的64位數據類型只有long和double),JVM會以高位對齊的方式爲其分配兩個連續的Slot空間.
ps. 在概念模型中,一個活動線程中兩個棧幀是相互獨立的.但大多數虛擬機實現都會作一些優化處理, 即上圖,讓下一個棧幀的部分操做數棧與上一個棧幀的部分局部變量表重疊在一塊兒,這樣的好處是方法調用時能夠共享一部分數據,而無須進行額外的參數複製傳遞.
每一個棧幀包含一個後進先出(LIFO)堆棧,稱爲其操做數棧,幀的操做數棧的最大深度在編譯時已肯定,並與用於與幀相關的方法的代碼一塊兒提供.
當一個方法執行開始時,這個方法的操做數棧是空的,在方法執行過程當中,會有各類字節碼指令往操做數棧中寫入和提取內容,也就是出棧/入棧操做.
JVM提供指令以將局部變量或字段中的常量或值加載到操做數棧上.
其餘JVM指令從操做數棧中獲取操做數,對它們進行操做,並將結果推回操做數棧.
操做數棧還用於準備要傳遞給方法和接收方法結果的參數.
每一個棧幀都包含一個指向運行時常量池中該棧幀所屬方法的引用,持有這個引用是爲了支持方法調用過程當中的動態鏈接.
字節碼中方法調用指令是以常量池中的指向方法的符號引用爲參數的,動態連接將這些符號方法引用轉換爲具體的方法引用.
ps. 注意!有一部分符號引用會在類加載階段或第一次使用的時候轉化爲直接引用,這種轉化稱爲靜態解析,另一部分在每次的運行期間轉化爲直接引用,這部分稱爲動態鏈接.
當一個方法被執行後,有兩種方式退出這個方法:
正常方法調用完成(Normal Method Invocation Completion)
若是調用不會直接從Java虛擬機或執行顯式throw語句引起異常,則方法調用會正常完成.
若是當前方法的調用正常完成,則能夠將值返回給調用方法.
當被調用的方法執行其中一個返回指令時,就會發生這種狀況,返回指令的選擇必須適合於返回值的類型(若是有的話).
這種退出方法的方式是,執行引擎遇到任意一個方法返回的字節碼指令.
忽然方法調用完成(Abrupt Method Invocation Completion)
在方法執行過程當中遇到了異常,且這個異常沒有在方法體內獲得處理(即本方法異常處理表中沒有匹配的異常處理器),則方法調用會忽然完成,就致使方法退出.
這種退出方式不會給上層調用者產生任何返回值.
不管採用何種退出方式,在方法退出後,都須要返回到方法被調用的位置,程序才能繼續執行,
方法返回時可能須要在棧幀中保存一些信息,用來幫助恢復它的上層方法的執行狀態.
通常來講,
方法正常退出時,調用者的程序計數器的值能夠做爲返回地址,棧幀中極可能會保存這個計數器值.
而方法異常退出 時,返回地址是經過異常處理器表來肯定的,棧幀中通常不會保存這部分信息.
ps. 方法退出的過程實際上等同於把當前棧幀出棧,所以退出時可能執行的操做有 : 恢復上層方法的局部變量表和操做數棧,把返回值(若是有的話)壓入調用者棧幀的操做數棧中,調整程序計數器的值以指向方法調用指令後面的一條指令等.
ps. JDK 1.8以後,方法區(Oracle Hotspot JVM)的永久代被完全移除了,取而代之是元空間,元空間使用的是直接內存
Metaspace的組成 :
- Klass Metaspace:
Klass Metaspace就是用來存klass的,klass是咱們熟知的class文件在JVM裏的運行時數據結構.
咱們看到的相似A.class實際上是存在Heap裏的,是java.lang.Class的一個對象實例.
這塊內存是緊接着Heap的,和以前的永久代同樣,這塊內存大小可經過-XX:CompressedClassSpaceSize參數來控制,
這個參數默認是1G,可是這塊內存也能夠沒有,假如沒有開啓壓縮指針就不會有這塊內存,這種狀況下klass都會存在NoKlass Metaspace裏,
另外若是咱們把-Xmx設置大於32G的話,其實也是沒有這塊內存的,由於會這麼大內存會關閉壓縮指針開關.
還有就是這塊內存最多隻會存在一塊.- NoKlass Metaspace:
NoKlass Metaspace專門來存klass相關的其餘的內容,好比Method,ConstantPool等.
這塊內存是由多塊內存組合起來的,因此能夠認爲是不連續的內存塊組成的.
這塊內存是必須的,雖然叫作NoKlass Metaspace,可是也其實能夠存klass的內容,在第一點已經提到了.
ps. Klass Metaspace和NoKlass Mestaspace都是全部classloader共享的,因此類加載器們要分配內存, 可是每一個類加載器都有一個SpaceManager,來管理屬於這個類加載的內存小塊. 若是Klass Metaspace用完了,那就會OutOfMemoryError,不過通常狀況下不會, NoKlass Mestaspace是由一塊塊內存慢慢組合起來的,在沒有達到限制條件的狀況下,會不斷加長這條鏈,讓它能夠持續工做.
Metaspace的特色 :
- 類及相關的元數據的生命週期與類加載器的一致.
- 類的元數據放入Native Memory,字符串池和類的靜態變量放入Java Heap中.
- 每一個加載器有專門的存儲空間.
- 只進行線性分配.
- 不會單獨回收某個類.
- 省掉了GC掃描及壓縮的時間(永久代會爲GC帶來沒必要要的複雜度,而且回收效率偏低).
- 元空間裏的對象的位置是固定的.
- 若是GC發現某個類加載器再也不存活了,會把相關的空間整個回收掉.
JVM經過類加載器把字節碼載入運行時數據區,由執行引擎執行.
執行引擎以指令爲單位讀入Java字節碼,就像CPU一個接一個的執行機器命令同樣.
每一個字節碼命令包含一字節的操做碼和可選的操做數.執行引擎讀取一個指令並執行相應的操做數,而後去讀取並執行下一條指令.
讀取、解釋並逐一執行每一條字節碼指令.
由於解釋器逐一解釋和執行指令,解釋器解釋字節碼的速度更快,但對解釋結果的執行速度較慢.全部的解釋性語言都有相似的缺點.
解釋器的缺點是,當屢次調用一種方法時,每次都須要新的解釋.
即時編譯器的引入用來彌補解釋器的不足.執行引擎先以解釋器的方式運行,而後在合適的時機,即時編譯器把整修字節碼編譯成本地代碼.
首先,即時編譯器先把字節碼經過中間代碼生成器(Itermediate Representation Generator)轉爲一種中間形式的表達式.
而後,代碼優化器(Code Optimizer)負責優化上面生成的代碼.
最後,目標代碼生成器(Target Code Generator)負責生成機器代碼或本地代碼.
期間,Profiler會負責查找熱點代碼,即該方法是否被屢次調用.
ps. Oracle Hotspot VM使用的即時編譯器稱爲Hotspot編譯器. 之因此稱爲Hotspot是由於Hotspot Compiler會根據分析找到具備更高編譯優先級的熱點代碼,而後所這些熱點代碼轉爲本地代碼.而且經過對本地代碼的緩存,編譯後的代碼能具備更快的執行速度. 若是一個被編譯過的方法再也不被頻繁調用,也即再也不是熱點代碼,Hotspot VM會把這些本地代碼從緩存中刪除並對其再次使用解釋器模式執行. Hotspot VM有Server VM和Client VM以後,它們所使用的即時編譯器也有所不一樣.
GC是後臺的守護進程. 有多種且針對不一樣區域的GC收集器(如.Serial收集器、CMS(Concurrent Mark Sweep)收集器、G1(GarbageFirst)收集器等等).