轉載:https://www.jianshu.com/p/ae97b692614e?from=timelinejava
JVM
是一種解釋執行class
文件的規範技術。
算法
我翻譯的中文圖:編程
在JVM中負責裝載
.class
文件(一種8位二進制流文件,各個數據項按順序緊密的從前向後排列, 相鄰的項之間沒有間隙,經編譯器編譯.java
源文件後生成,每一個類(或者接口)都單獨佔有一個class
文件)。數組
當
JVM
使用類裝載器定位class
文件,並將其輸入到內存中時。會提取class
文件的類型信息,並將這些信息存儲到方法區中。同時放入方法區中的還有該類型中的類靜態變量。安全
java.io.FileOutputStream
java.io.OutputStream
public
、abstract
、final
)。java.io.Closeable
, java.io.Flushable
。ClassLoader
類的引用。Class
類的引用。JVM
對每一個裝載的類型都會創建一個方法表,用於存儲該類型對象能夠調用的方法的直接引用,這些方法就包括從超類中繼承來的。而這張表與Java動態綁定
機制的實現是密切相關的。常量池數據結構
常量池指的是在編譯期被肯定,並被保存在已編譯的
.class
文件中的一些數據。除了包含代碼中所定義的各類基本數據類型和對象型(String及數組)的常量值(final,在編譯時肯定,而且編譯器會優化)還包含一些以文本形式出現的符號引用(類信息),好比:多線程
虛擬機必須給每一個被裝載的類型維護一個常量池。常量池就是該類型所用到常量的一個有序集合
,包括直接常量(string、integer等)和其餘類型,字段和方法的符號引用
。jvm
方法區是多線程共享的。也就是當虛擬機實例開始運行程序時,邊運行邊加載進class文件
。不一樣的Class
文件都會提取出不一樣類型信息存放在方法區中。一樣,方法區中再也不須要運行的類型信息會被垃圾回收線程丟棄掉。函數
Java
程序在運行時建立的全部類型對象和數組都存儲在堆中
。JVM
會根據new
指令在堆中開闢一個肯定類型的對象內存空間。可是堆中開闢對象的空間並無任何人工指令能夠回收,而是經過JVM的垃圾回收器負責回收。優化
對於一個運行的Java而言,每個線程都有一個PC寄存器。當線程執行Java程序時,PC寄存器的內容老是下一條將被執行的指令地址。
每啓動一個
線程
,JVM
都會爲它分配一個Java棧
,用於存放方法中的局部變量,操做數以及異常數據等。當線程調用某個方法時,JVM會根據方法區中該方法的字節碼組建一個棧幀
。並將該棧幀壓入Java棧
中,方法執行完畢時,JVM
會彈出該棧幀並釋放掉。
注意:Java棧中的數據是線程私有的,一個線程是沒法訪問另外一個線程的Java棧
的數據。這也就是爲何多線程編程時,兩個相同線程執行同一方法時,對方法內的局部變量是不須要數據同步的緣由。
成員變量有默認值(被final修飾且沒有static的必須顯式賦值),局部變量不會自動賦值。
運行
Java
的每個線程都是一個獨立的虛擬機執行引擎的實例。從線程生命週期的開始到結束,他要麼在執行字節碼,要麼在執行本地方法。一個線程可能經過解釋或者使用芯片級指令直接執行字節碼,或者間接經過JIT
(即時編譯器)執行編譯過的本地代碼。
注意:JVM是進程級別,執行引擎是線程級別。
實際上,
class
文件中方法的字節碼流就是有JVM
的指令序列構成的。每一條指令包含一個單字節的操做碼,後面跟隨0個或多個操做數。
指令由一個操做碼
和零個或多個操做數
組成。
iload_0 // 把存儲在局部變量區中索引爲0的整數壓入操做數棧。 iload_1 // 把存儲在局部變量區中索引爲1的整數壓入操做數棧。 iadd // 從操做數棧中彈出兩個整數相加,在將結果壓入操做數棧。 istore_2 // 從操做數棧中彈出結果
很顯然,上面的指令反覆用到了Java棧
中的某一個方法棧幀
。實際上執行引擎運行Java字節碼
指令不少時候都是在不停的操做Java棧
,也有的時候須要在堆中開闢對象以及運行系統的本地指令等。可是Java棧的操做要比堆中的操做要快的多,所以反覆開闢對象是很是耗時的。這也是爲何Java程序優化的時候,儘可能減小new對象。
示例分析:
//源代碼 Test.java
package edu.hr.jvm; import edu.hr.jvm.bean; public class Test{ public static void main(String[] args){ Act act=new Act(); act.doMathForever(); } } //源代碼 Act.java
package edu.hr.jvm.bean; public class Act{ public void doMathForever(){ int i=0; for(;;){ i+=1; i*=2; } } }
首先OS
會建立一個JVM實例
(進行必要的初始化工做,好比:初始啓動類裝載器
,初始運行時內存數據區
等。
而後經過自定義類裝載器加載Test.class
。並提取Test.class字節碼
中的信息存放在方法區 中(具體的信息在上面已經講過)。上圖展現了方法區中的Test類信息,其中在常量池中有一個符號引用「Act」
(類的全限定名,注意:這個引用目前尚未真正的類信息的內存地址)。
接着JVM
開始從Test
類的main
字節碼處開始解釋執行。在運行以前,會在Java棧中組建一個main方法的棧幀 ,如上圖Java棧
所示。JVM須要運行任何方法前,經過在Java棧中壓入一個幀棧。在這個幀棧的內存區域中進行計算。
如今能夠開始執行main
方法的第一條指令 —— JVM
須要爲常量池的第一項的類(符號引用Act
)分配內存空間。可是Act
類此時尚未加載進JVM
(由於常量池目前只有一個「Act」
的符號引用)。
JVM
加載進Act.class
,並提取Act類信息
放入方法區中。而後以一個直接指向方法區Act類信息
的直接引用(在棧中)換開始在常量池中的符號引用「Act」
,這個過程就是常量池解析
。之後就能夠直接訪問Act
的類信息了。
此時JVM
能夠根據方法區中的Act類信息
,在堆中開闢一個Act類對象act。
接着開始執行main方法
中的第二條指令調用doMathForever
方法。這個能夠經過堆中act對象
所指的方法表中查找,而後定位到方法區中的Act類信息
中的doMathForever
方法字節碼
。在運行以前,仍然要組建一個doMathForever棧幀壓入Java棧。(注意:JVM
會根據方法區中doMathForever的字節碼
來建立棧幀的局部變量區
和操做數棧
的大小)
接下來JVM
開始解釋運行Act.doMathForever字節碼
的內容了。
編譯:源碼要運行,必須先轉成二進制的機器碼。這是編譯器的任務。
.class
文件。Java
編譯一個類時,若是這個類所依賴的類尚未被編譯,編譯器就會先編譯這個被依賴的類,而後引用,不然直接引用。若是java
編譯器在指定目錄下找不到該類所其依賴的類的.class
文件或者.java
源文件的話,編譯器話報「cant find symbol」
的錯誤。token(類名,成員變量名等等)
以及符號引用(方法引用,成員變量引用等等)
;方法字節碼放的是類中各個方法的字節碼。運行:
java
類運行的過程大概可分爲兩個過程:類的加載,類的執行。須要說明的是:JVM
主要在程序第一次主動使用類的時候,纔會去加載該類。也就是說,JVM
並非在一開始就把一個程序就全部的類都加載到內存中,而是到不得不用的時候才把它加載進來,並且只加載一次。
下面是程序運行的詳細步驟:
//MainApp.java
public class MainApp { public static void main(String[] args) { Animal animal = new Animal("Puppy"); animal.printName(); } } //Animal.java
public class Animal { public String name; public Animal(String name) { this.name = name; } public void printName() { System.out.println("Animal ["+name+"]"); } }
java
程序獲得MainApp.class
文件後,在命令行上敲java AppMain
。系統就會啓動一個jvm進程
,jvm進程
從classpath
路徑中找到一個名爲AppMain.class
的二進制文件,將MainApp
的類信息加載到運行時數據區的方法區內,這個過程叫作MainApp類的加載
。JVM
找到AppMain
的主函數入口,開始執行main函數
。main
函數的第一條命令是Animal animal = new Animal("Puppy");
就是讓JVM
建立一個Animal
對象,可是這時候方法區中沒有Animal
類的信息,因此JVM
立刻加載Animal
類,把Animal
類的類型信息放到方法區中。Animal
類以後,Java
虛擬機作的第一件事情就是在堆區中爲一個新的Animal
實例分配內存,而後調用構造函數初始化Animal實例
,這個Animal實例
持有着指向方法區的Animal類的類型信息
(其中包含有方法表,java動態綁定
的底層實現)的引用。animal.printName()
的時候,JVM
根據animal引用
找到Animal
對象,而後根據Animal對象
持有的引用定位到方法區中Animal類
的類型信息的方法表,得到printName()
函數的字節碼的地址。printName()
函數的字節碼
(能夠把字節碼理解爲一條條的指令)。特別說明:java類中全部public和protected的實例方法都採用動態綁定機制,全部私有方法
、靜態方法
、構造器
及初始化方法<clinit>
都是採用靜態綁定機制
。而使用動態綁定機制的時候會用到方法表,靜態綁定時並不會用到。
經過前面的兩個例子的分析,應該理解了很多了吧。
JVM
主要包含三大核心部分:類加載器,運行時數據區和執行引擎。
虛擬機將描述類的數據從class文件加載到內存,並對數據進行校驗,準備,解析和初始化,最終就會造成能夠被虛擬機使用的java類型,這就是一個虛擬機的類加載機制。java在類中的類是動態加載的,只有在運行期間使用到該類的時候,纔會將該類加載到內存中,java依賴於運行期動態加載和動態連接來實現類的動態使用。
一個類的生命週期:
加載,驗證,準備,初始化和卸載在開始的順序上是固定的,可是能夠交叉進行。
在Java中,對於類有且僅有四種狀況會對類進行「初始化」。
new
關鍵字實例化對象的時候,讀取或設置一個類的靜態字段時候(除final
修飾的static
外),調用類的靜態方法時候,都只會初始化該靜態字段或者靜態方法所定義的類。reflect包
對類進行反射調用的時候,若是類沒有進行初始化,則先要初始化該類。main方法的主類
。注意:
加載
加載階段主要完成三件事,即經過一個類的全限定名來獲取定義此類的二進制字節流,將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構,在
Java堆
中生成一個表明此類的Class對象
,做爲訪問方法區這些數據的入口。這個加載過程主要就是靠類加載器實現的,這個過程能夠由用戶自定義類的加載過程。
驗證
這個階段目的在於確保才class
文件的字節流中包含信息符合當前虛擬機要求,不會危害虛擬機自身安全。
主要包括四種驗證:
文件格式驗證
:基於字節流驗證,驗證字節流是否符合Class文件格式的規範
,而且能被當前虛擬機處理。元數據驗證
:基於方法區的存儲結構驗證,對字節碼描述信息進行語義驗證。字節碼驗證
:基於方法區的存儲結構驗證,進行數據流和控制流的驗證。符號引用驗證
:基於方法區的存儲結構驗證,發生在解析中,是否能夠將符號引用成功解析爲直接引用。準備
僅僅爲類變量(即static修飾的字段變量)分配內存而且設置該類變量的初始值即零值,這裏不包含用final修飾的static,由於final
在編譯的時候就會分配了(編譯器的優化),同時這裏也不會爲實例變量分配初始化。類變量會分配在方法區中,而實例變量是會隨着對象一塊兒分配到Java
堆中。
解析
解析主要就是將常量池中的符號引用替換爲直接引用的過程。符號引用
就是一組符號來描述目標,能夠是任何字面量,而直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄。有類或接口的解析,字段解析,類方法解析,接口方法解析。
初始化
初始化階段依舊是初始化類變量和其餘資源,這裏將執行用戶的static字段和靜態語句塊的賦值操做。這個過程就是執行類構造器< clinit >方法的過程。
< clinit >方法是由編譯器收集類中全部類變量的賦值動做和靜態語句塊的語句生成的,類構造器< clinit >方法與實例構造器< init >方法不一樣,這裏面不用顯示的調用父類的< clinit >方法,父類的< clinit >方法會自動先執行於子類的< clinit >方法。即父類定義的靜態語句塊和靜態字段都要優先子類的變量賦值操做。
類加載器的分類
啓動類加載器(Bootstrap ClassLoader)
:主要負責加載<JAVA_HOME>\lib目錄
中的'.'
或是-Xbootclasspath
參數指定的路徑中的,而且能夠被虛擬機識別(僅僅按照文件名識別的)的類庫到虛擬機內存中。它加載的是System.getProperty("sun.boot.class.path")
所指定的路徑
或jar
。擴展類加載器(Extension ClassLoader)
:主要負責加載<JAVA_HOME>\lib\ext
目錄中的,或者被java.ext.dirs
系統變量所指定的路徑中的全部類庫。它加載的是System.getProperty("java.ext.dirs")
所指定的路徑或jar
。應用程序類加載器(Application ClassLoader)
:也叫系統類加載器,主要負責加載ClassPath路徑
上的類庫,若是應用程序沒有自定義本身類加載器,則這個就是默認的類加載器。它加載的是System.getProperty("java.class.path")
所指定的路徑
或jar
。類加載器的特色
Application Loader
(系統類加載器)開始加載指定的類。Bootstrap Loader
(啓動類加載器)是最頂級的類加載器了,其父加載器爲null
。類加載器的雙親委派模型
類加載器雙親委派模型的工做過程是:若是一個類加載器收到一個類加載的請求,它首先將這個請求委派給父類加載器去完成,每個層次類加載器都是如此,則全部的類加載請求都會傳送到頂層的啓動類加載器,只有父加載器沒法完成這個加載請求(即它的搜索範圍中沒有找到所要的類),子類才嘗試加載。
使用雙親委派模型主要是兩個緣由:
Java核心API
,則就會帶來安全隱患。下面是一個類加載器雙親委派模型,這裏各個類加載器並非繼承關係,它們利用組合實現的父類與子類關係。
類加載的幾種方式
JVM
初始化加載,加載含有main
的主類。Class.forName("Hello")
方法動態加載類,默認會執行初始化塊,這是由於Class.forName("Hello")
其實就是Class.forName("Hello",true,CALLCLASS.getClassLoader())
,第二個參數就是類加載過程當中的鏈接操做。若是指定了ClassLoader
,則不會執行初始化塊。ClassLoader.loadClass("Hello")
方法動態加載類,不會執行初始化塊,由於loadClass
方法有兩個參數,用戶只是用第一個參數,第二個參數默認爲false
,即不對該類進行解析則就不會初始化。類加載實例
當在命令行下執行:java HelloWorld(HelloWorld是含有main方法的類的Class文件)
,JVM會將HelloWorld.class
加載到內存中,並在堆中造成一個Class的對象HelloWorld.class
。
基本的加載流程以下:
jre目錄
,尋找jvm.dll
,並初始化JVM
;Bootstrap Loader
(啓動類加載器);Bootstrap Loader
,該加載器會加載它指定路徑下的Java核心API
,而且再自動加載Extended Loader
(標準擴展類加載器),Extended Loader
會加載指定路徑下的擴展JavaAPI
,並將其父Loader
設爲BootstrapLoader
。Bootstrap Loader
也會同時自動加載AppClass Loader
(系統類加載器),並將其父Loader
設爲ExtendedLoader
。AppClass Loader
加載CLASSPATH
目錄下定義的類,HelloWorld類
。在Java
應用開發過程當中,可能會須要建立應用本身的類加載器。典型的場景包括實現特定的Java字節代碼
查找方式、對字節代碼進行加密/解密以及實現同名Java類的隔離等。建立本身的類加載器並非一件複雜的事情,只須要繼承自java.lang.ClassLoader
類並覆寫對應的方法便可。 java.lang.ClassLoader
中提供的方法有很多,下面介紹幾個建立類加載器時須要考慮的:
defineClass()
:這個方法用來完成從Java字節碼
的字節數組到java.lang.Class
的轉換。這個方法是不能被覆寫的,通常是用原生代碼來實現的。findLoadedClass()
:這個方法用來根據名稱查找已經加載過的Java類。一個類加載器不會重複加載同一名稱的類。findClass()
:這個方法用來根據名稱查找並加載Java類
。loadClass()
:這個方法用來根據名稱加載Java類
。resolveClass()
:這個方法用來連接一個Java類
。這裏比較 容易混淆的是findClass()
方法和loadClass()
方法的做用。前面提到過,在Java類
的連接過程當中,會須要對Java類
進行解析,而解析可能會致使當前Java類
所引用的其它Java類
被加載。在這個時候,JVM
就是經過調用當前類的定義類加載器的loadClass()
方法來加載其它類的。findClass()方法
則是應用建立的類加載器的擴展點。應用本身的類加載器應該覆寫findClass()方法
來添加自定義的類加載邏輯。 loadClass()方法
的默認實現會負責調用findClass()方法
。
前面提到,類加載器的代理模式默認使用的是父類優先的策略。這個策略的實現是封裝在loadClass()方法
中的。若是但願修改此策略,就須要覆寫loadClass()方法
。
下面的代碼給出了自定義的類加載的常見實現模式:
public class MyClassLoader extends ClassLoader { protected Class<?> findClass(String name) throws ClassNotFoundException { byte[] b = null; //查找或生成Java類的字節代碼 return defineClass(name, b, 0, b.length); } }
分代收集
新生代
(Young Generation)
Eden
空間(Eden space,任何實例都經過Eden空間進入運行時內存區域)S0 Survivor
空間(S0 Survivor space,存在時間長的實例將會從Eden空間移動到S0 Survivor空間)S1 Survivor
空間 (存在時間更長的實例將會從S0 Survivor空間移動到S1 Survivor空間)
老年代
(Old Generation)實例將從S1提高到Tenured(終身代)
永久代
(Permanent Generation)包含類、方法等細節的元信息
永久代空間在Java SE8
特性中已經被移除。
年輕代:使用標記複製清理算法
,解決內存碎片問題。由於在年輕代會有大量的內存須要回收,GC
比較頻繁。經過這種方式來處理內存碎片化,而後在老年代中經過標記清理算法
來回收內存,由於在老年代須要被回收的內存比較少,提升效率。
Eden 區:當一個實例被建立了,首先會被存儲在堆內存年輕代的 Eden
區中。
Survivor 區(S0 和 S1):做爲年輕代 GC(Minor GC)
週期的一部分,存活的對象(仍然被引用的)從 Eden
區被移動到 Survivor
區的 S0
中。相似的,垃圾回收器會掃描 S0
而後將存活的實例移動到 S1
中。總會有一個空的survivor區
。
老年代: 老年代(Old or tenured generation
)是堆內存中的第二塊邏輯區。當垃圾回收器執行 Minor GC
週期時(對象年齡計數器),在 S1 Survivor
區中的存活實例將會被晉升到老年代,而未被引用的對象被標記爲回收。老年代是實例生命週期的最後階段。Major GC
掃描老年代的垃圾回收過程。若是實例再也不被引用,那麼它們會被標記爲回收,不然它們會繼續留在老年代中。
內存碎片:一旦實例從堆內存中被刪除,其位置就會變空而且可用於將來實例的分配。這些空出的空間將會使整個內存區域碎片化。爲了實例的快速分配,須要進行碎片整理。基於垃圾回收器的不一樣選擇,回收的內存區域要麼被不停地被整理,要麼在一個單獨的GC進程中完成。
Java語言規範沒有明確地說明JVM使用哪一種垃圾回收算法,可是任何一種垃圾收集算法通常要作2件基本的事情:
- 發現無用信息對象
- 回收被無用對象佔用的內存空間,使該空間可被程序再次使用。
GC Roots
根集就是正在執行的Java程序能夠訪問的引用變量的集合
(包括局部變量、參數、類變量)
GC Roots的對象包括
**可達性算法分析 **
經過一系列稱爲」GC Roots」的對象做爲起點,從這些節點開始向下搜索,搜索全部走過的路徑稱爲引用鏈,當一個對象到GC Roots沒有任何引用鏈相連時(從GC Roots到此對象不可達),則證實此對象是不可用的,應該被回收。
根搜索算法:計算可達性,如圖:
引用計數法
引用計數法是惟一沒有使用根集(GC Roots)的垃圾回收的法,該算法使用引用計數器來區分存活對象和再也不使用的對象。堆中的每一個對象對應一個引用計數器。當每一次建立一個對象並賦給一個變量時,引用計數器置爲1。當對象被賦給任意變量時,引用計數器每次加1,當對象出了做用域後(該對象丟棄再也不使用),引用計數器減1,一旦引用計數器爲0,對象就知足了垃圾收集的條件。
惟一沒有使用根可達性算法
的垃圾回收算法。
缺陷:不能解決循環引用的回收。
tracing算法(tracing collector)
tracing算法
是爲了解決引用計數法的問題
而提出,它使用了根集(GC Roots)
概念。垃圾收集器從根集開始掃描,識別出哪些對象可達,哪些對象不可達,並用某種方式標記可達對象,例如對每一個可達對象設置一個或多個位。在掃描識別過程當中,基於tracing算法
的垃圾收集也稱爲標記和清除(mark-and-sweep)垃圾收集器
。
compacting算法(Compacting Collector)
爲了
解決堆碎片問題
,在清除的過程當中,算法將全部的對象移到堆的一端,堆的另外一端就變成了一個相鄰的空閒內存區,收集器會對它移動的全部對象的全部引用進行更新,使得這些引用在新的位置能識別原來的對象。在基於Compacting算法的收集器的實現中,通常增長句柄和句柄表。
copying算法(Coping Collector)
該算法的提出是爲了克服
句柄的開銷和解決堆碎片
的垃圾回收。它開始時把堆分紅 一個對象面和多個空閒面,程序從對象面爲對象分配空間,當對象滿了,基於coping算法的垃圾收集就從根集中掃描活動對象,並將每一個活動對象複製到空閒面(使得活動對象所佔的內存之間沒有空閒洞),這樣空閒面變成了對象面,原來的對象面變成了空閒面,程序會在新的對象面中分配內存。
generation算法(Generational Collector) :如今的java內存分區
stop-and-copy垃圾收集器
的一個缺陷是收集器必須複製全部的活動對象,這增長了程序等待時間,這是coping算法低效
的緣由。在程序設計中有這樣的規律:多數對象存在的時間比較短,少數的存在時間比較長。所以,generation算法將堆分紅兩個或多個,每一個子堆做爲對象的一代 (generation)。因爲多數對象存在的時間比較短,隨着程序丟棄不使用的對象,垃圾收集器將從最年輕的子堆中收集這些對象。在分代式的垃圾收集器運行後,上次運行存活下來的對象移到下一最高代的子堆中,因爲老一代的子堆不會常常被回收,於是節省了時間。
adaptive算法(Adaptive Collector)
在特定的狀況下,一些垃圾收集算法會優於其它算法。基於Adaptive算法的垃圾收集器就是監控當前堆的使用狀況,並將選擇適當算法的垃圾收集器