JVM是可運行Java代碼的虛擬計算機 ,包括一套字節碼指令集、一組寄存器、棧、堆、存儲方法域和垃圾回收。java
JVM是運行在操做系統之上的,它與硬件沒有直接的交互。程序員
運行過程:算法
① Java源文件—->編譯器—->字節碼文件數組
② 字節碼文件—->JVM—->機器碼緩存
每一種平臺的解釋器是不一樣的,可是實現的虛擬機是相同的,這也就是Java爲何可以跨平臺的緣由了 。安全
當一個程序從開始運行,這時虛擬機就開始實例化了,多個程序啓動就會存在多個虛擬機實例。數據結構
程序退出或者關閉,則虛擬機實例消亡,多個虛擬機實例之間數據不能共享。多線程
JVM生命週期:函數
JVM在Java程序開始執行的時候,它才運行,程序結束的時它就中止。spa
JVM中的線程分爲兩種:守護線程和普通線程
守護線程是JVM本身使用的線程,好比垃圾回收(GC)就是一個守護線程。
普通線程通常是Java程序的線程,只要JVM中有普通線程在執行,那麼JVM就不會中止。
權限足夠的話,能夠調用exit()方法終止程序。
在字節碼被類加載器執行以前對文件進行校驗,保證class文件內容有正確的內部結構。
class文件校驗器分紅四部分:
1.文件格式驗證: 校驗class文件的結構的合法性
2.元數據驗證: 掃描發生在方法區中,主要對於,語義,詞法和語法的分析,也就是檢查這個類是否可以順利的編譯
3.字節碼驗證: 字節碼的校驗過程校驗的就是字節碼流的合法過程,也就是校驗操做數+操做碼的合法性(字節碼流=操做碼+操做數)。
4.符號引用驗證: 解析符號引用和直接引用時進行的,此次校驗確認被引用的類,字段以及方法確實存在
Class文件由Java編譯器生成,咱們建立的.Java文件在通過編譯器後,會變成.Class的文件,而後被類加載系統加載後,在JVM上運行。
類的加載指的是將類的.class文件中的二進制數據讀入到內存中,一般是建立一個字節數組讀入.class文件,將其放在運行時數據區的方法區內,而後在堆建立Class對象,用來封裝類在方法區內的數據結構,而且向Java程序員提供了訪問方法區內的數據結構的接口。
JVM將類的加載分爲3個步驟:一、裝載(Load)二、連接(Link)三、初始化(Initialize)
一、經過一個類的全限定名來獲取其定義的二進制字節流。
二、將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構。
三、在Java堆中生成一個表明這個類的java.lang.Class對象,做爲對方法區中這些數據的訪問入口。
一、驗證:確保被加載的類的正確性
驗證是鏈接階段的第一步,這一階段的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。
驗證階段大體會完成4個階段的檢驗動做:
文件格式驗證:驗證字節流是否符合Class文件格式的規範;例如:是否以0xCAFEBABE開頭、主次版本號是否在當前虛擬機的處理範圍以內、常量池中的常量是否有不被支持的類型。
元數據驗證:對字節碼描述的信息進行語義分析(注意:對比javac編譯階段的語義分析),以保證其描述的信息符合Java語言規範的要求;例如:這個類是否有父類,除了java.lang.Object以外。
字節碼驗證:經過數據流和控制流分析,肯定程序語義是合法的、符合邏輯的。
符號引用驗證:確保解析動做能正確執行。
二、準備:爲類的靜態變量分配內存,並將其初始化爲默認值
準備階段是正式爲類變量分配內存並設置類變量初始值的階段(內存分配的僅包括類變量(static),而不包括實例變量,實例變量會在對象實例化時隨着對象一塊分配在Java堆中),這些內存都將在方法區中分配
三、解析:把類中的符號引用轉換爲直接引用
符號引用:就是一組符號來描述目標,能夠是任何字面量。(對類或接口、字段、類方法、接口方法、方法類型、方法句柄和調用限定符 7類符號引用)
直接引用:就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。
初始化,爲類的靜態變量賦予正確的初始值,JVM負責對類進行初始化,主要對類變量進行初始化,
1)若是類存在直接的父類而且這個類尚未被初始化,那麼就先初始化父類;2)若是類中存在初始化語句,就依次執行這些初始化語句。
在Java中對類變量進行初始值設定有兩種方式:
①聲明類變量是指定初始值。
②使用靜態代碼塊爲類變量指定初始值。
一、BootStrap ClassLoader :啓動類加載器,是最頂層的類加載器,負責加載JDK中的核心類庫,如 rt.jar、resources.jar、charsets.jar等。
二、Extension ClassLoader:擴展類加載器,負責加載Java的擴展類庫,默認加載$JAVA_HOME中jre/lib/*.jar 或 -Djava.ext.dirs指定目錄下的jar包。
三、App ClassLoader:系統類加載器,負責加載應用程序classpath目錄下全部jar和class文件。
四、Custom ClassLoader:自定義類加載器,經過java.lang.ClassLoader的子類自定義加載class。
JVM是基於堆棧的虛擬機。JVM爲每一個新建立的線程都分配一個堆棧.也就是說,對於一個Java程序來講,它的運行就是經過對堆棧的操做來完成的。堆棧以幀爲單位保存線程的狀態。JVM對堆棧只進行兩種操做:以幀爲單位的壓棧和出棧操做。
JVM執行class字節碼,線程建立後,都會產生程序計數器(PC)和棧(Stack),程序計數器存放下一條要執行的指令在方法內的偏移量,棧中存放一個個棧幀,每一個棧幀對應着每一個方法的每次調用,而棧幀又是有局部變量區和操做數棧兩部分組成,局部變量區用於存放方法中的局部變量和參數,操做數棧中用於存放方法執行過程當中產生的中間結果。棧的結構以下圖所示:
靜態變量、常量、類信息、運行時常量池存在方法區中。
方法區是被全部線程共享,全部字段和方法字節碼,以及一些特殊方法如構造函數,接口代碼也在此定義。
簡單說,全部定義的方法的信息都保存在該區域,此區域屬於共享區間。
訪問方法區的信息必須確保線程是安全的。若是有兩個線程同時去加載一個類,那麼只能有一個線程被容許去加載這個類,另外一個必須等待。
棧也叫棧內存,主管Java程序的運行,是在線程建立時建立,它的生命期是跟隨線程的生命期,線程結束棧內存也就釋放,對於棧來講不存在垃圾回收問題,只要線程一結束該棧就Over,生命週期和線程一致,是線程私有的。
Java棧中只保存基礎數據類型變量和自定義對象的引用(注意只是對象的引用而不是對象自己,對象是保存在堆區中的)
堆這塊區域是JVM中最大的,應用的對象和數據都是存在這個區域,這塊區域也是線程共享的,也是 GC 主要的回收區。
一個 JVM 實例只存在一個堆類存,堆內存的大小是能夠調節的。類加載器讀取了類文件後,須要把類、方法、常變量放到堆內存中,以方便執行器執行。
堆內存分爲三部分:
新生區:
新生區是類的誕生、成長、消亡的區域,一個類在這裏產生,應用,最後被垃圾回收器收集,結束生命。新生區又分爲兩部分:伊甸區(Eden space)和倖存者區(Survivor pace),全部的類都是在伊甸區被new出來的。倖存區有兩個:0區(Survivor 0 space)和1區(Survivor 1 space)。當伊甸園的空間用完時,程序又須要建立對象,JVM的垃圾回收器將對伊甸園進行垃圾回收(Minor GC),將伊甸園中的剩餘對象移動到倖存0區。若倖存0區也滿了,再對該區進行垃圾回收,而後移動到1區。那若是1去也滿了呢?再移動到養老區。若養老區也滿了,那麼這個時候將產生Major GC(FullGCC),進行養老區的內存清理。若養老區執行Full GC 以後發現依然沒法進行對象的保存,就會產生OOM異常「OutOfMemoryError」。
若是出現java.lang.OutOfMemoryError: Java heap space異常,說明Java虛擬機的堆內存不夠。
緣由有二:
a.Java虛擬機的堆內存設置不夠,能夠經過參數-Xms、-Xmx來調整。
b.代碼中建立了大量大對象,而且長時間不能被垃圾收集器收集(存在被引用)。
養老區:
養老區用於保存重新生區篩選出來的 JAVA 對象,通常池對象都在這個區域活躍。
永久區:
永久存儲區是一個常駐內存區域,用於存放JDK自身所攜帶的 Class,Interface 的元數據,也就是說它存儲的是運行環境必須的類信息,被裝載進此區域的數據是不會被垃圾回收器回收掉的,關閉 JVM 纔會釋放此區域所佔用的內存。
若是出現java.lang.OutOfMemoryError: PermGen space,說明是Java虛擬機對永久代Perm內存設置不夠。
緣由有二:
a. 程序啓動須要加載大量的第三方jar包。例如:在一個Tomcat下部署了太多的應用。
b. 大量動態反射生成的類不斷被加載,最終致使Perm區被佔滿。
本地方法棧是在程序調用或JVM調用本地方法接口(Native)時候啓用,用於存儲本地方法的局部變量表,本地方法的操做數棧等信息。
棧內的數據在超出其做用域後,會被自動釋放掉,它不禁JVM GC管理。
每個線程都包含一個棧區,每一個棧中的數據都是線程私有的,其餘棧不能訪問。
在JVM的概念模型裏,字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令。
分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器來完成。
JVM的多線程是經過線程輪流切換並分配處理器執行時間的方式來實現的,爲了各條線程之間的切換後計數器能恢復到正確的執行位置,因此每條線程都會有一個獨立的程序計數器。
程序計數器僅佔很小的一塊內存空間。
當線程正在執行一個Java方法,程序計數器記錄的是正在執行的JVM字節碼指令的地址。若是正在執行的是一個Natvie(本地方法),那麼這個計數器的值則爲空(Underfined)。
程序計數器這個內存區域是惟一一個在JVM規範中沒有規定任何OutOfMemoryError(內存不足錯誤)的區域。
類裝載器裝載負責裝載編譯後的字節碼,並加載到運行時數據區(Runtime Data Area),而後執行引擎執行會執行這些字節碼
在JVM規範中制定了虛擬機字節碼執行引擎的概念模型。執行引擎必須把字節碼轉換成能夠直接被JVM執行的語言。
字節碼能夠經過如下兩種方式轉換成合適的語言:
1.標記-清除: 這是垃圾收集算法中最基礎的,根據名字就能夠知道,它的思想就是標記哪些要被回收的對象,而後統一回收。這種方法很簡單,可是會有兩個主要問題:1.效率不高,標記和清除的效率都很低;2.會產生大量不連續的內存碎片,致使之後程序在分配較大的對象時,因爲沒有充足的連續內存而提早觸發一次GC動做。
2.複製算法: 爲了解決效率問題,複製算法將可用內存按容量劃分爲相等的兩部分,而後每次只使用其中的一塊,當一塊內存用完時,就將還存活的對象複製到第二塊內存上,而後一次性清楚完第一塊內存,再將第二塊上的對象複製到第一塊。可是這種方式,內存的代價過高,每次基本上都要浪費通常的內存。 因而將該算法進行了改進,內存區域再也不是按照1:1去劃分,而是將內存劃分爲8:1:1三部分,較大那分內存交Eden區,其他是兩塊較小的內存區叫Survior區。每次都會優先使用Eden區,若Eden區滿,就將對象複製到第二塊內存區上,而後清除Eden區,若是此時存活的對象太多,以致於Survivor不夠時,會將這些對象經過分配擔保機制複製到老年代中。(java堆又分爲新生代和老年代)
3. 標記-整理 該算法主要是爲了解決標記-清除,產生大量內存碎片的問題;當對象存活率較高時,也解決了複製算法的效率問題。它的不一樣之處就是在清除對象的時候現將可回收對象移動到一端,而後清除掉端邊界之外的對象,這樣就不會產生內存碎片了。
4.分代收集 如今的虛擬機垃圾收集大多采用這種方式,它根據對象的生存週期,將堆分爲新生代和老年代。在新生代中,因爲對象生存期短,每次回收都會有大量對象死去,那麼這時就採用複製算法。老年代裏的對象存活率較高,沒有額外的空間進行分配擔保,因此可使用標記-整理 或者 標記-清除。