JVM的結(jié)構(gòu)體系

前文提到j(luò)avac程序是一個Java編譯器。它將文件.java編譯成.class文件,并發(fā)送到j(luò)ava虛擬機。虛擬機執(zhí)行編譯器放在class文件中的字節(jié)碼。
JVM針對每個操作系統(tǒng)開發(fā)其對應(yīng)的解釋器,所以只要其操作系統(tǒng)有對應(yīng)版本的JVM,那么這份Java編譯后的代碼就能夠運行起來,這就是Java能一次編譯,到處運行的原因。
java的運行機制包括類的加載機制,jvm內(nèi)存結(jié)構(gòu),垃圾回收算法,jvm調(diào)優(yōu)
類的加載機制
主要關(guān)注點:
什么是類的加載
類的生命周期
類加載器
雙親委派模型
什么是類的加載
類的加載指的是將類的.class文件中的二進制數(shù)據(jù)讀入到內(nèi)存中,將其放在運行時數(shù)據(jù)區(qū)的方法區(qū)內(nèi),然后在堆區(qū)創(chuàng)建一個java.lang.Class對象,類的加載的最終是位于堆區(qū)中的Class對象。
類的生命周期
類的生命周期包括這幾個部分,加載、連接、初始化、使用和卸載,其中前三部是類的加載的過程,如下圖;

加載,查找并加載類的二進制數(shù)據(jù),在Java堆中也創(chuàng)建一個java.lang.Class類的對象
連接,連接又包含三塊內(nèi)容:驗證、準(zhǔn)備、初始化。1)驗證,文件格式、元數(shù)據(jù)、字節(jié)碼、符號引用驗證;2)準(zhǔn)備,為類的靜態(tài)變量分配內(nèi)存,并將其初始化為默認(rèn)值;3)解析,把類中的符號引用轉(zhuǎn)換為直接引用
初始化,為類的靜態(tài)變量賦予正確的初始值
使用,new出對象程序中使用
卸載,執(zhí)行垃圾回收
幾個小問題?
1、JVM初始化步驟 ? 2、類初始化時機 ?3、哪幾種情況下,Java虛擬機將結(jié)束生命周期?
類加載器

啟動類加載器:Bootstrap ClassLoader,負(fù)責(zé)加載存放在JDK\jre\lib(JDK代表JDK的安裝目錄,下同)下,或被-Xbootclasspath參數(shù)指定的路徑中的,并且能被虛擬機識別的類庫
擴展類加載器:Extension ClassLoader,該加載器由sun.misc.Launcher$ExtClassLoader實現(xiàn),它負(fù)責(zé)加載DK\jre\lib\ext目錄中,或者由java.ext.dirs系統(tǒng)變量指定的路徑中的所有類庫(如javax.*開頭的類),開發(fā)者可以直接使用擴展類加載器。
應(yīng)用程序類加載器:Application ClassLoader,該類加載器由sun.misc.Launcher$AppClassLoader來實現(xiàn),它負(fù)責(zé)加載用戶類路徑(ClassPath)所指定的類,開發(fā)者可以直接使用該類加載器
類加載機制
全盤負(fù)責(zé),當(dāng)一個類加載器負(fù)責(zé)加載某個Class時,該Class所依賴的和引用的其他Class也將由該類加載器負(fù)責(zé)載入,除非顯示使用另外一個類加載器來載入
父類委托,先讓父類加載器試圖加載該類,只有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類
緩存機制,緩存機制將會保證所有加載過的Class都會被緩存,當(dāng)程序中需要使用某個Class時,類加載器先從緩存區(qū)尋找該Class,只有緩存區(qū)不存在,系統(tǒng)才會讀取該類對應(yīng)的二進制數(shù)據(jù),并將其轉(zhuǎn)換成Class對象,存入緩存區(qū)。這就是為什么修改了Class后,必須重啟JVM,程序的修改才會生效
jvm內(nèi)存結(jié)構(gòu)
主要關(guān)注點:
jvm內(nèi)存結(jié)構(gòu)都是什么
對象分配規(guī)則
jvm內(nèi)存結(jié)構(gòu)

方法區(qū)和堆是所有線程共享的內(nèi)存區(qū)域;而java棧、本地方法棧和程序計數(shù)器是運行是線程私有的內(nèi)存區(qū)域。
Java堆(Heap),是Java虛擬機所管理的內(nèi)存中最大的一塊。Java堆是被所有線程共享的一塊內(nèi)存區(qū)域,在虛擬機啟動時創(chuàng)建。此內(nèi)存區(qū)域的唯一目的就是存放對象實例,幾乎所有的對象實例都在這里分配內(nèi)存。
方法區(qū)(Method Area),方法區(qū)(Method Area)與Java堆一樣,是各個線程共享的內(nèi)存區(qū)域,它用于存儲已被虛擬機加載的類信息、常量、靜態(tài)變量、即時編譯器編譯后的代碼等數(shù)據(jù)。
程序計數(shù)器(Program Counter Register),程序計數(shù)器(Program Counter Register)是一塊較小的內(nèi)存空間,它的作用可以看做是當(dāng)前線程所執(zhí)行的字節(jié)碼的行號指示器。
JVM棧(JVM Stacks),與程序計數(shù)器一樣,Java虛擬機棧(Java Virtual Machine Stacks)也是線程私有的,它的生命周期與線程相同。虛擬機棧描述的是Java方法執(zhí)行的內(nèi)存模型:每個方法被執(zhí)行的時候都會同時創(chuàng)建一個棧幀(Stack Frame)用于存儲局部變量表、操作棧、動態(tài)鏈接、方法出口等信息。每一個方法被調(diào)用直至執(zhí)行完成的過程,就對應(yīng)著一個棧幀在虛擬機棧中從入棧到出棧的過程。
本地方法棧(Native Method Stacks),本地方法棧(Native Method Stacks)與虛擬機棧所發(fā)揮的作用是非常相似的,其區(qū)別不過是虛擬機棧為虛擬機執(zhí)行Java方法(也就是字節(jié)碼)服務(wù),而本地方法棧則是為虛擬機使用到的Native方法服務(wù)。
對象分配規(guī)則
對象優(yōu)先分配在Eden區(qū),如果Eden區(qū)沒有足夠的空間時,虛擬機執(zhí)行一次Minor GC。
大對象直接進入老年代(大對象是指需要大量連續(xù)內(nèi)存空間的對象)。這樣做的目的是避免在Eden區(qū)和兩個Survivor區(qū)之間發(fā)生大量的內(nèi)存拷貝(新生代采用復(fù)制算法收集內(nèi)存)。
長期存活的對象進入老年代。虛擬機為每個對象定義了一個年齡計數(shù)器,如果對象經(jīng)過了1次Minor GC那么對象會進入Survivor區(qū),之后每經(jīng)過一次Minor GC那么對象的年齡加1,知道達到閥值對象進入老年區(qū)。
動態(tài)判斷對象的年齡。如果Survivor區(qū)中相同年齡的所有對象大小的總和大于Survivor空間的一半,年齡大于或等于該年齡的對象可以直接進入老年代。
空間分配擔(dān)保。每次進行Minor GC時,JVM會計算Survivor區(qū)移至老年區(qū)的對象的平均大小,如果這個值大于老年區(qū)的剩余值大小則進行一次Full GC,如果小于檢查HandlePromotionFailure設(shè)置,如果true則只進行Monitor GC,如果false則進行Full GC。
如何通過參數(shù)來控制個各個內(nèi)存區(qū)域
GC算法 垃圾回收
主要關(guān)注點:
對象存活判斷
GC算法
垃圾回收器
對象存活判斷
判斷對象是否存活一般有兩種方式:
引用計數(shù):每個對象有一個引用計數(shù)屬性,新增一個引用時計數(shù)加1,引用釋放時計數(shù)減1,計數(shù)為0時可以回收。此方法簡單,無法解決對象相互循環(huán)引用的問題。
可達性分析(Reachability Analysis):從GC Roots開始向下搜索,搜索所走過的路徑稱為引用鏈。當(dāng)一個對象到GC Roots沒有任何引用鏈相連時,則證明此對象是不可用的,不可達對象。
GC算法
GC最基礎(chǔ)的算法有三種:標(biāo)記 -清除算法、復(fù)制算法、標(biāo)記-壓縮算法,我們常用的垃圾回收器一般都采用分代收集算法。
標(biāo)記 -清除算法,“標(biāo)記-清除”(Mark-Sweep)算法,如它的名字一樣,算法分為“標(biāo)記”和“清除”兩個階段:首先標(biāo)記出所有需要回收的對象,在標(biāo)記完成后統(tǒng)一回收掉所有被標(biāo)記的對象。
復(fù)制算法,“復(fù)制”(Copying)的收集算法,它將可用內(nèi)存按容量劃分為大小相等的兩塊,每次只使用其中的一塊。當(dāng)這一塊的內(nèi)存用完了,就將還存活著的對象復(fù)制到另外一塊上面,然后再把已使用過的內(nèi)存空間一次清理掉。
標(biāo)記-壓縮算法,標(biāo)記過程仍然與“標(biāo)記-清除”算法一樣,但后續(xù)步驟不是直接對可回收對象進行清理,而是讓所有存活的對象都向一端移動,然后直接清理掉端邊界以外的內(nèi)存
分代收集算法,“分代收集”(Generational Collection)算法,把Java堆分為新生代和老年代,這樣就可以根據(jù)各個年代的特點采用最適當(dāng)?shù)氖占惴ā?/p>
垃圾回收器
Serial收集器,串行收集器是最古老,最穩(wěn)定以及效率高的收集器,可能會產(chǎn)生較長的停頓,只使用一個線程去回收。
ParNew收集器,ParNew收集器其實就是Serial收集器的多線程版本。
Parallel收集器,Parallel Scavenge收集器類似ParNew收集器,Parallel收集器更關(guān)注系統(tǒng)的吞吐量。
Parallel Old 收集器,Parallel Old是Parallel Scavenge收集器的老年代版本,使用多線程和“標(biāo)記-整理”算法
CMS收集器,CMS(Concurrent Mark Sweep)收集器是一種以獲取最短回收停頓時間為目標(biāo)的收集器。
G1收集器,G1 (Garbage-First)是一款面向服務(wù)器的垃圾收集器,主要針對配備多顆處理器及大容量內(nèi)存的機器. 以極高概率滿足GC停頓時間要求的同時,還具備高吞吐量性能特征
補充:
JVM 常量池
JVM常量池也稱之為運行時常量池,它是方法區(qū)(Method Area)的一部分。用于存放編譯期間生成的各種字面量和符號引用。運行時常量池不要求一定只有在編譯器產(chǎn)生的才能進入,運行期間也可以將新的常量放入池中,這種特性被開發(fā)人員利用比較多的就是String.intern()方法。
由“****用于存放編譯期間生成的各種字面量和符號引用”這句話可見,常量池中存儲的是對象的引用而不是對象的本身。
常量池的好處
常量池是為了避免頻繁的創(chuàng)建和銷毀對象而影響系統(tǒng)性能,它也實現(xiàn)了對象的共享。
例如字符串常量池:在編譯階段就把所有字符串文字放到一個常量池中。
1、節(jié)省內(nèi)存空間:常量池中如果有對應(yīng)的字符串,那么則返回該對象的引用,從而不必再次創(chuàng)建一個新對象。
2、節(jié)省運行時間:比較字符串時,==比equals()快。對于兩個引用變量,==判斷引用是否相等,也就可以判斷實際值是否相等。
雙等號(==)的含義
基本數(shù)據(jù)類型之間使用雙等號,比較的是數(shù)值。
復(fù)合數(shù)據(jù)類型(類)之間使用雙等號,比較的是對象的引用地址是否相等。
八種基本類型的包裝類和常量池
Byte、Short、Integer、Long、Character、Boolean、String這7種包裝類都各自實現(xiàn)了自己的常量池。
//例子:
Integer i1 = 20;
Integer i2 = 20;
System.out.println(i1=i2);//輸出TRUE
Byte、Short、Integer、Long、Character這5種包裝類都默認(rèn)創(chuàng)建了數(shù)值[-128 , 127]的緩存數(shù)據(jù)。當(dāng)對這5個類型的數(shù)據(jù)不在這個區(qū)間內(nèi)的時候,將會去創(chuàng)建新的對象,并且不會將這些新的對象放入常量池中。
//IntegerCache.low = -128
//IntegerCache.high = 127
public static Integer valueOf(int i) {
if (i >= IntegerCache.low && i <= IntegerCache.high)
return IntegerCache.cache[i + (-IntegerCache.low)];
return new Integer(i);
}
//例子
Integer i1 = 200;
Integer i2 = 200;
System.out.println(i1==i2);//返回FALSE
Float 和Double 沒有實現(xiàn)常量池。
String包裝類與常量池
String str1 = "aaa";
當(dāng)以上代碼運行時,JVM會到字符串常量池查找 "aaa" 這個字面量對象是否存在?
存在:則返回該對象的引用給變量 **str1 **。
不存在:則在堆中創(chuàng)建一個相應(yīng)的對象,將創(chuàng)建的對象的引用存放到常量池中,同時將引用返回給變量 **str1 **。
String str1 = "aaa";
String str2 = "aaa";
System.out.println(str1 == str2);//返回TRUE
因為變量**str1 和str2 **都指向同一個對象,所以返回true。
String str3 = new String("aaa");
System.out.println(str1 == str3);//返回FALSE
當(dāng)我們使用了new來構(gòu)造字符串對象的時候,不管字符串常量池中是否有相同內(nèi)容的對象的引用,新的字符串對象都會創(chuàng)建。因為兩個指向的是不同的對象,所以返回FALSE 。
String.intern()方法
對于使用了new 創(chuàng)建的字符串對象,如果想要將這個對象引用到字符串常量池,可以使用intern() 方法。
調(diào)用intern() 方法后,檢查字符串常量池中是否有這個對象的引用,并做如下操作:
存在:直接返回對象引用給變量。
不存在:將這個對象引用加入到常量池,再返回對象引用給變量。
String interns = str3.intern();
System.out.println(interns == str1);//返回TRUE
假定常量池中都沒有以上字面量的對象,以下創(chuàng)建了多少個對象呢?
String str4 = "abc"+"efg";
String str5 = "abcefg";
System.out.println(str4 == str5);//返回TRUE
答案是三個。第一個:"abc" ,第一個:"efg",第三個:"abc"+"efg"("abcefg")
String str5 = "abcefg"; 這句代碼并沒有創(chuàng)建對象,它從常量池中找到了"abcefg" 的引用,所有str4 == str5 返回TRUE,因為它們都指向一個相同的對象。
什么情況下會將字符串對象引用自動加入字符串常量池?
//只有在這兩種情況下會將對象引用自動加入到常量池
String str1 = "aaa";
String str2 = "aa"+"a";
//其他方式下都不會將對象引用自動加入到常量池,如下:
String str3 = new String("aaa");
String str4 = New StringBuilder("aa").append("a").toString();
StringBuilder sb = New StringBuilder();
sb.append("aa");
sb.append("a");
String str5 = sb.toString();