JVM是一個虛構出來的計算機,有本身的處理器,堆棧,寄存器以及相應的指令系統等。JVM是JRE的一部分,經過在實際的計算機上仿真模擬各類計算機功能,這樣就能使Java在跨平臺上運行。html
JVM的內部體系結構分爲三個部分,分別爲類裝載器子系統,運行時數據區和執行引擎。java
每一個Java虛擬機都有一個類加載器,負責查找並加載程序中的類,接口,並給其肯定惟一的名字。Java虛擬機有兩種類裝載器:系統類裝載器和用戶自定義類裝載器,系統類裝載器是JVM實現的一部分,用戶自定義類裝載器是Java程序的一部分,其必須是類裝載器ClassLoader類的子類。面試
類裝載器子系統涉及Java虛擬機的其它組成部分和來自java.lang庫的類。ClassLoader類定義的方法爲程序提供了訪問類裝載器機制的接口。對於每一個被裝載的類型,Java虛擬機都會給它建立一個java.lang.Class類的實例來表明該類型。和其它對象同樣,用戶自定義的類裝載器以及Class類的實例放在內存的堆區,裝載的類型信息位於方法區。算法
類裝載器子系統除了要查找定位導入二進制class文件外,還須要負責驗證被導入的類的正確性,爲類的類變量分配並初始化內存,以及解析符號引用。順序是:
裝載(查找並裝載類型的二進制數據)——>鏈接(驗證:確保被導入類型的正確性,準備:爲類變量分配內存,並將其初始化爲默認值,解析:把類型中的符號引用轉換爲直接引用)——>初始化(將類變量初始化爲正確的初始值)
+----2017-12-16-----+bootstrap
1. 裝載:裝載是指將編譯後的Java類文件(.class文件)中的二進制數據讀入內存,並將其放在運行時數據區的方法區內,而後在堆區建立一個java.lang.Class對象,用其來封裝類在方法區的數據結構.即加載後最後獲得的是Class對象,該對象是單實例的,即不管這個類建立了多少個對象,他的Class對象是惟一的.經過Class.forName(類的全路徑), 實例對象.class, 實例對象getClass() 這三種能夠加載並獲取到該類的Class對象. 類裝載時類中的靜態代碼會被執行,例如:Class.forName()加載JDBC驅動 2. 鏈接:靜態變量的第一次賦值----默認值 3. 初始化:靜態變量第二次賦值----真正的初始值 類的初始化發生在Java程序對類的首次**主動使用**中,主動使用有(建立類的實例,訪問操做類或接口的靜態變量,調用類的靜態方法,反射如:Class.forName(類全路徑),初始化此類的子類,Java虛擬機啓動時被代表爲啓動類的類:java Test),除以上外其餘對類的被動使用是不會致使類的初始化.
指令集:Java方法的字節碼流由Java虛擬機的指令序列構成。每條指令包含:一個單字節的操做碼(表示須要執行的操做),0或多個操做數(操做數向Java虛擬機提供執行操做碼的額外信息,使指令使用的值可能來自當前常量池中的項,當前幀的局部變量中的值或者當前操做數棧頂端的值)。數組
運行中的Java程序的每個線程都是一個獨立的虛擬機執行引擎的實例。從線程生命週期的開始到結束,它要麼在執行字節碼,要麼在執行本地方法。數據結構
主要的執行技術有:解釋,及時編譯,自適應優化,芯片級直接執行。自適應優化吸收解釋和及時編譯的優勢,採起兩種結合的方式。
自適應優化——開始對全部的代碼都採起解釋執行的方式並監視代碼的執行狀況,而後對那些常常調用的方法啓動一個後臺線程,將其及時編譯爲本地代碼進行調用,並進行仔細優化。當該方法再也不頻繁的被調用,則取消編譯過的代碼,將其歸爲解釋執行。app
當虛擬機裝載某個類型時,它使用類裝載器定位載入相應的.class文件到虛擬機中,接着虛擬機提取其中的類型信息,並將這些信息存儲到方法區。當開發人員在程序中經過Class對象的getName(),isInterface()等方法來獲取信息時,這些數據都會來源於方法區,同時方法區也是全局共享的,在必定條件下它也會被GC掉(虛擬機容許經過用戶自定義的類裝載器來動態擴展Java程序,此時方法區也能夠被垃圾回收器收集),當方法區須要使用的內存超過最大容許時,會拋出OutOfMemory。jvm
方法區存放內容以下:函數
- 已經被虛擬機所加載的類信息(類名稱,類類型(接口仍是類),修飾符,類的直接超類名稱),
- 類中的靜態(類)變量,
- 運行常量池runtime constant pool (類中定義的final類型的常量,一個有序集合,包括直接常量(string,integer,floating 常量)和對其餘類型,字段,方法的符號引用)——class文件除了有類信息外,還有一項是常量池(constant pool table),常量池用於存放編譯期生成的各類字面量和符號的引用,這部份內容將在類加載後進入方法區的運行時常量池中存儲。——運行時常量池相對於class文件常量池的一個重要特徵是具有動態性:即除了能夠存儲class文件常量池中的內容外,運行期間也可能將新的常量放入到池中,好比String類的intern()方法,,參考: 深刻理解java虛擬機(三):String.intern()-字符串常量池
- 類中的方法信息(方法名,返回類型,參數數量和類型,修飾符),
- 字段信息(字段名,類型,修飾符),
- 指向ClassLoader類的引用(每一個類型被裝載時,虛擬機必須跟蹤肯定它是由系統類裝載器仍是由用戶自定義裝載器裝載的),
- 指向Class類的引用(對於每隔一個被裝載的類型,虛擬機都爲其相應的建立了一個java.lang.Class類實例)
----------
堆是JVM用來存儲對象實例以及數組值(數組在Java虛擬機中是一個真正的對象)的區域,能夠認爲Java中全部經過new建立的對象的內存都在堆中分配,堆中的對象所佔的內存是須要等待GC進行回收的(JVM沒有釋放內存的指令,須要將釋放內存的任務交給垃圾收集器處理)。堆是JVM中全部線程共享的。
每當啓動一個線程時,Java虛擬機就會爲他分配一個Java棧。Java棧由許多棧幀組成,一個棧幀包含一個對應的Java方法調用的狀態。當線程調用一個Java方法時,虛擬機壓入一個新的棧幀到Java棧中,當該方法返回時,這個棧幀就會從Java棧中彈出。
棧幀:由局部變量區,操做數棧和幀數據區組成。當虛擬機調用一個Java方法時,它從對應類的類型信息中獲得此方法的局部變量區和操做數棧大小,並根據此來分配幀的內存,而後壓入棧中。
局部變量區
局部變量區被組織爲以字長爲單位,從0開始計數的數組。字節碼指令經過從0開始的索引使用其中的數據。類型爲int, float, reference和returnAddress的值在數組中佔據一項,而類型爲byte, short和char的值在存入數組前都被轉換爲int值,也佔據一項。但類型爲long和double的值在數組中卻佔據連續的兩項。以下圖:
操做數棧
與局部變量區同樣,操做數棧被組織成一個以字長爲單位的數組,其經過標準的棧操做訪問。幀數據區
Java棧幀須要幀數據區來支持常量池的解析——(每當虛擬機要執行一個須要操做常量池數據的指令時,就會經過幀數據區中指向常量池的指針來訪問常量池),正常方法返回——(幀數據區還要幫助虛擬機處理Java方法的正常結束或異常停止。若是經過return正常結束,虛擬機必須恢復發起調用的方法的棧幀,包括設置程序計數器指向發起調用方法的下一個指令;若是方法有返回值,虛擬機須要將它壓入到發起調用的方法的操做數棧)以及異常派發機制——(爲了處理Java方法執行期間的異常退出狀況,幀數據區還保存一個對此方法異常表的引用)。
每個線程都有它本身的PC寄存器,也是在該線程啓動時建立的。PC寄存器的內容老是指向下一條即將被執行的指令的地址,這裏的地址能夠是一個本地地址,也能夠是在方法區中相對於該方法的起始指令的偏移量。若是線程執行Java方法,則PC寄存器保存的是下一條執行指令的地址。若線程執行的是本地方法,那麼此時PC寄存器的值是"undefined"。
當線程調用Java方法時,虛擬機會建立一個新的棧幀並將其壓入到對應線程的Java棧。當線程調用的是本地方法時,虛擬機會保持Java棧不變,再也不向Java棧中壓入新的棧幀,虛擬機只是簡單的動態鏈接並調用指定的本地方法。依賴於本地方法的實現,如某個JVM實現的本地方法接口使用C鏈接模型,則本地方法棧就是C棧,能夠說某線程在調用本地方法時,就進入了一個不受JVM限制的領域,也就是JVM能夠利用本地方法來動態擴展自己。
GC經過肯定對象是否被活動對象引用來肯定是否收集回收該對象。
使用System.gc()直接請求Java的垃圾回收。
在jvm垃圾回收以前調用的方法。之全部要使用finalize(),是存在着垃圾回收器不能處理的狀況:1) 在本地方法native method調用中,可能因爲在分配內存的時候可能採用了相似C語言的作法,而非Java的new作法,好比本地方法調用了C++的malloc()來分配內存而沒有調用free()來釋放掉內存,這時候就可能形成內存泄露。這時就能夠在finalize()方法中用本地方法調用free()來釋放掉這些特殊的內存空間。 2)又或者是打開了文件資源,這些資源不屬於垃圾回收器能回收的範圍,則須要在finalize()中調用對應的本地方法來回收文件資源。
基於tracing算法的垃圾回收也稱爲標記和清除(mark-sweep)垃圾收集器。
標記-清除算法分爲兩個階段:標記階段和清除階段。標記階段的任務是標記出全部的須要被回收的對象,清除階段就是回收被標記的對象所佔用的內存空間。
此算法缺陷就是很容易產生內存碎片,在產生大量的內存碎片後就可能沒法對新的大對象所須要的內存空間進行分配。
爲了解決標記-清除算法的缺陷,coping算法是將內存按內存容量劃分爲大小相等的兩塊,每次對新對象的內存分配只使用其中的一塊。當這一塊內存用完的時候,就將在這塊內存上還存活下來的對象複製到另以空閒塊內存上面,而後把那塊已經使用的內存空間所有清理。
此算法雖然不會產生內存碎片,可是每次只能使用一半的內存空間,下降了內存實際使用率。並且當存活的對象還不少的時候,須要將它們所有複製到另外一塊內存上,這也使效率下降。
爲了解決compying算法的缺陷而充分的利用內存空間,提出了mark-compact 算法,即標記-壓縮。該算法的標記階段和mark-sweep同樣將全部須要被回收的對象進行標記。可是標記完成後,它不是直接清理可回收對象,而是將存活的對象都向一端移動,而後清理掉存活對象邊界之外的內存空間。這樣即不會產生內碎片,也充分利用了內存空間。
分代收集算法是目前大部分jvm的垃圾回收器所採用的算法。它的核心思想是根據對象存活的生命週期來劃分不一樣的區域,對每一個區域進行不用的垃圾回收策略。通常狀況將堆區分爲老年代(tenured generation)和新生代(young generation) ,老年代的特色是每次垃圾回收時只會有少許的對象須要被回收,而新生代的特色是每次垃圾回收時都會有大量的對象須要被回收掉。
目前大部分垃圾收集器對於新生代都採起Copying算法,由於新生代中每次垃圾回收都要回收大部分對象,也就是說須要複製的操做次數較少,可是實際中並非按照1:1的比例來劃分新生代的空間的,通常來講是將新生代劃分爲一塊較大的Eden空間和兩塊較小的Survivor空間,每次使用Eden空間和其中的一塊Survivor空間,當進行回收時,將Eden和Survivor中還存活的對象複製到另外一塊Survivor空間中,而後清理掉Eden和剛纔使用過的Survivor空間。
而因爲老年代的特色是每次回收都只回收少許對象,通常使用的是Mark-Compact算法。
參考來自
深刻理解Java虛擬機體系結構
什麼是JVM?
面試準備之JVM的組成、垃圾回收機制
深刻理解Java虛擬機
深刻理解JVM--JVM垃圾回收機制
分享一波阿里雲代金券快速上雲
from usthe.com