深入理解JVM虚拟机(总结篇)
JVM平臺上還可以運(yùn)行其他語言,運(yùn)行的是Class字節(jié)碼。只要能翻譯成Class的語言就OK了。挺強(qiáng)大的。
- JVM廠商很多
- 垃圾收集器、收集算法
- JVM檢測工具
關(guān)于類的加載:
- ?Java代碼中,類型(interface, class,enum等,有些是在運(yùn)行時候生成的,比如動態(tài)代理)的加載、連接與初始化過程都是在程序運(yùn)行期間完成的。不涉及到對象的概念。同時也是個Runtime階段。
- ?提供了更大的靈活性,增加了更多的可能性。提供了一些擴(kuò)展,靈活擴(kuò)展。
Java虛擬機(jī)與程序的生命周期:
? 在如下幾種情況下,Java虛擬機(jī)將會結(jié)束生命周期:
類的加載、連接與初始化:
加載:查找并加載類的二進(jìn)制數(shù)據(jù)
連接:?
- 驗(yàn)證: 確保被加載類的正確性。Class有格式的。
- 準(zhǔn)備:為類的靜態(tài)變量分配內(nèi)存,并將其初始化為默認(rèn)值??
- 注:
1.類的靜態(tài)變量或類的靜態(tài)方法,通常可以看做全局的,由類去直接調(diào)用。此時還是個類的概念,不存在對象。
2.關(guān)于默認(rèn)值問題:
class Test{
public static int a = 1;
}
中間過程: Test類加載到內(nèi)存的過程中,會給a分配一個內(nèi)存。然后將a初始化為默認(rèn)值0(整型變量) - 解析: 把類中的符號引用轉(zhuǎn)為直接引用。符號的引用也是間接的引用方式。
初始化: 為類的靜態(tài)變量賦予正確的初始值
- class Test{public static int a = 1;} 此時的a才真正成為1了
類的使用與卸載
?使用:?類的方法變量使用等
?卸載: class字節(jié)碼文件,加載到內(nèi)存里面。形成了自己的數(shù)據(jù)結(jié)構(gòu),駐留在內(nèi)存里面。可以銷毀掉。卸載到了就不能進(jìn)行new 對象了。
總體流程:
Java程序?qū)︻惖氖褂梅绞椒譃閮煞N:
所有的Java虛擬機(jī)實(shí)現(xiàn)必須在每個類或接口被Java程序“首次主動使用”時才初始化他們。即初始化只會執(zhí)行一次。
主動使用,七種(非精確劃分,大體劃分):
1.java.lang.invoke.MethodHandle實(shí)例的解析結(jié)果REF_getStatic, REF_putStatic, REF_invokeStatic句柄對應(yīng)的類沒有初始化,則初始化
2.1.7開始提供了對動態(tài)語言的支持。特別的JVM平臺上通過腳本引擎調(diào)用JS代碼(動態(tài)語言)。
注:助記符了解即可
除了以上七種情況,其他使用Java類的方式都被看做是對類的被動使用,都不會導(dǎo)致類的初始化。
類的加載:
?類的加載指的是將類 .class文件中的二進(jìn)制數(shù)據(jù)讀入內(nèi)存中,將其放在運(yùn)行時數(shù)據(jù)區(qū)的方法區(qū)內(nèi),然后在內(nèi)存中創(chuàng)建一個java.lang.Class對象(規(guī)范并說明Class對象位于哪里,HotSpot虛擬機(jī)將其放在了方法區(qū)中,JVM沒有規(guī)范這個)用來封裝類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)。
?引申:一個類不管生成了多少實(shí)例,所有的實(shí)例對應(yīng)只有一份Class對象。 Class對象是面鏡子,能反映到方法區(qū)中的Class文件的內(nèi)容、結(jié)構(gòu)等各種信息。
加載.class文件的方式:
str1 子類調(diào)用了繼承到的父類的str1,子類的靜態(tài)代碼塊沒有執(zhí)行。str1是父類中定義的。MyParent1的主動使用,但是沒有主動使用MyChild1. 總結(jié):看定義的!
str2 可以執(zhí)行,同時初始化子類時候,父類會主動使用。所有的父類都會被初始化!
MyTest1是一個啟動類,主動使用。先加載之。?
總結(jié):?
引申: -XX:+TraceClassLoading,用于追蹤類的加載信息并打印出來。可以看到類的加載情況。
? ? ? ? ? ?打印: 虛擬機(jī)在當(dāng)前啟動情況下所加載的類的信息。
總結(jié)設(shè)置方式:
所有JVM參數(shù)都是: -XX: 開頭??
? 類似于Boolean類型的開關(guān):
? ? ? ? -XX:+<option> 表示開啟option選項
? ? ? ? -XX: - <option>? 表示關(guān)閉option選項
? 賦值:? ?
? ? ?-XX:<option>=<value>, 表示將option選項的值設(shè)置為value??
?關(guān)于常量:
public class MyTest2 {public static void main(String[] args) {System.out.println(MyParent2.str);} }class MyParent2{// final修飾成為常量public static final String str = "hello world";static {System.out.println("MyParent2 ----> run");}}
在編譯階段這個常量被存入到 調(diào)用這個常量的方法所在的類的常量池中。
本例中:
? ?“hello world”是一個常量,會放置到MyTest2類的常量池中。
? ?這里指的時將常量存放到了MyTest2的常量池匯總,之后MyTest2與MyParent2就沒有任何關(guān)系了
? ?甚至,極端一些。我們可以將MyParent3的class文件刪除。(編譯完畢后,把class字節(jié)碼刪除)
總結(jié):
- ?常量編譯階段會存入到調(diào)用這個常量的方法所在的類的常量池中。
- ?本質(zhì)上,調(diào)用類并沒有直接引用到定義常量的類,因此并不會觸發(fā)定義常量類的初始化。
引申反編譯: javap -c? 類的全路徑名字
助記符引申:
- ldc表示將int,float 或 String類型的常量值從常量池中推送至棧頂。
- bipush表示將單字節(jié)(-128 ~ 127)的常量值推送至棧頂
- sipush表示將一個短整型常量值(-32768 ~ 32767)推送至棧頂
- iconst_1 表示將int類型的1推送至棧頂 (iconst_1 ~ iconst_5)
助記符是在rt.jar中相關(guān)類去實(shí)現(xiàn)的。
如果常量的值,在編譯器不能確定下來呢?
public class MyTest3 {public static void main(String[] args) {System.out.println(MyParent3.str);} }class MyParent3 {public static final String str = UUID.randomUUID().toString();static {System.out.println("MyParent3 -- run");} }此時放在MyTest3類的常量池中沒有意義的。
總結(jié):
? 當(dāng)一個常量值并非編譯期間可以確定的,那么其值就不會被放到調(diào)用類的常量池中。這時在程序運(yùn)行時,會導(dǎo)致主動使用這個常量所在的類,顯然會導(dǎo)致這個類被初始化。
new對象實(shí)例情況:
public class MyTest4 {public static void main(String[] args) {MyParent4 myParent4 = new MyParent4();} } class MyParent4{static {System.out.println("MyParent4 --> run");} }對這個類的主動使用。
如果多次new,只會初始化一次。首次主動使用。
數(shù)組情況:
public class MyTest4 {public static void main(String[] args) {MyParent4[] myParent4s = new MyParent4[1];} } class MyParent4{static {System.out.println("MyParent4 --> run");} }不在七種情況范圍內(nèi)。不會初始化!
不是MyParent4的實(shí)例!
到底創(chuàng)建的什么實(shí)例?getClass!,數(shù)組的實(shí)例到底是個啥玩意兒?
public class MyTest4 {public static void main(String[] args) {MyParent4[] myParent4s = new MyParent4[1];//看看是啥Class<? extends MyParent4[]> aClass = myParent4s.getClass();System.out.println(aClass);} } class MyParent4{static {System.out.println("MyParent4 --> run");} }Java虛擬機(jī)在運(yùn)行期,創(chuàng)建出來的類型。是個數(shù)組類型。有點(diǎn)類似動態(tài)代理
數(shù)組類型也是比較特殊的。[Lxxxx
二維數(shù)組也是一樣的特殊
看下父類型:
public class MyTest4 {public static void main(String[] args) {MyParent4[] myParent4s = new MyParent4[1];//看看是啥System.out.println(myParent4s.getClass().getSuperclass());} } class MyParent4{static {System.out.println("MyParent4 --> run");} }父類型其實(shí)是Object
總結(jié):
?對于數(shù)組實(shí)例來說,其類型是由JVM在運(yùn)行期動態(tài)生成的
?動態(tài)生成的類型,其父類就是Object
?對于數(shù)組來說,JavaDoc經(jīng)常將構(gòu)成數(shù)組的元素為Component,實(shí)際上就是將數(shù)組降低一個維度后的類型。
看下原生類型的數(shù)組:
public class MyTest4 {public static void main(String[] args) {int[] ints = new int[3];System.out.println(ints.getClass());System.out.println(ints.getClass().getSuperclass());} } class MyParent4{static {System.out.println("MyParent4 --> run");} }助記符:
? anewarray: 表示創(chuàng)建一個引用類型的(比如類、接口、數(shù)組)數(shù)組,并將其引用值壓如棧頂。
? newarray: 表示創(chuàng)建一個指定的原始類型(如:int,float,char等)的數(shù)組,并將其引用值壓入棧頂。
以上所總結(jié)的是類與類之間的關(guān)系,包括繼承的。下面接口的特點(diǎn):
public class MyTest5 {public static void main(String[] args) {System.out.println(MyChild5.b);} }interface MyParent5 {public static int a = 5; }interface MyChild5 extends MyParent5 {public static int b = 6; }? 接口是沒有靜態(tài)代碼塊的。可以通過手動刪除class文件來證明之。
public class MyTest5 {public static void main(String[] args) {System.out.println(MyChild5.b);} }interface MyParent5 {public static int a = 5; }interface MyChild5 extends MyParent5 {// 只有在運(yùn)行時候才會賦值,會放到MyTest5的常量池里面。如果Class刪除了,運(yùn)行時候就會報錯!public static int b = new Random().nextInt(2); }? 結(jié)論:
- 當(dāng)一個接口在初始化時候,并不要求其父類接口都完成了初始化。
- 只有在真正使用到父類接口的時候(如引用接口中定義的常量時),才會初始化。
- 類,一定要先初始化父類。
分析: 先賦值: 默認(rèn)的0 和 給定的0,然后構(gòu)造方法進(jìn)行++操作。?
如果更改位置:
public class MyTest6 {public static void main(String[] args) {Singleton instance = Singleton.getInstance();System.out.println("counter1-->"+ instance.counter1);System.out.println("counter2-->"+ instance.counter2);} } class Singleton{public static int counter1;private static Singleton singleton = new Singleton();private Singleton(){counter1++;counter2++;System.out.println(counter1);System.out.println(counter2);}public static int counter2 = 0;public static Singleton getInstance(){return singleton;} }?
?解析:
按照從上到下的順序進(jìn)行初始化。
類主動使用時候,先準(zhǔn)備,給類的靜態(tài)變量賦初始值。
此時:?
? ?counter1 初始值 0?
? ?singleton 初始值 null
? ?counter2 初始值 0
接著調(diào)用靜態(tài)方法 getInstance時候,賦初始值。
sigleton 會指向一個實(shí)例,然后執(zhí)行私有構(gòu)造方法。
然后執(zhí)行到 public static int counter2 = 0時候,顯示賦值0了。
總結(jié):
?先準(zhǔn)備
?再初始化: 根據(jù)類里面代碼的順序去執(zhí)行的.真正的賦值(準(zhǔn)備為其提供初始值,要不談不上做++操作)
?
?畫個圖:
?關(guān)于類的實(shí)例化:
? 為對象分配內(nèi)存,即為new對象,在堆上面。
? ?為實(shí)例變量賦默認(rèn)值、為實(shí)例變量賦正確的初始值都跟靜態(tài)變量似的了。賦予默認(rèn)值之后,再去賦予開發(fā)者指定的值。
類的加載:
- ? 類的加載的最終產(chǎn)品是位于內(nèi)充中的Class對象
- ? Class對象封裝了類在方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu),并且向Java程序員提供了訪問方法區(qū)內(nèi)的數(shù)據(jù)結(jié)構(gòu)的接口
Class是反射的入口。像一面鏡子一樣。
有兩種類型的類加載器:
? 1.Java虛擬機(jī)自帶的加載器
- 根類加載器(BootStrap)
- 擴(kuò)展類加載器(Extension)
- 系統(tǒng)(應(yīng)用)類加載器(System)
2.用戶自定義的類加載器
- java.lang.ClassLoader的子類
- 用戶可以定制類的加載方式
類的加載:?
?類加載器并不需要等到某個類被“首次主動使用”時候再加載它
?注:
- ? JVM規(guī)范允許類加載器在預(yù)料某個類將要被使用時就預(yù)先加載它。如果在預(yù)先加載的過程中遇到了.class文件確實(shí)或者存在錯誤,類加載器必須在程序首次主動使用該類時候才報告錯誤(LinkageaError錯誤)
- ? 如果這個類一直沒有被程序主動使用,那么類加載器就不會報告錯誤
類的驗(yàn)證:
? 類被加載后,就進(jìn)入連接階段。連接就是將已經(jīng)讀入到內(nèi)存中的類的二進(jìn)制數(shù)據(jù)合并到虛擬機(jī)的運(yùn)行時的環(huán)境中去。
類的驗(yàn)證的內(nèi)容:
- ? 類文件的結(jié)構(gòu)檢查
- ? 語義檢查
- ? 字節(jié)碼驗(yàn)證
- ? 二進(jìn)制兼容性的驗(yàn)證
在準(zhǔn)備階段:
??
?
?初始化階段:
?
?類的初始化步驟:
- ?假如這個類還沒有被加載和連接,那就先進(jìn)行加載和連接
- ?假如類存在直接父類,并且這個父類還沒有被初始化,那就先初始直接父類
- ?假如類中存在初始化語句,那就依次執(zhí)行這些初始化語句?
只有當(dāng)程序訪問的靜態(tài)變量或靜態(tài)方法確實(shí)在當(dāng)前類或當(dāng)前接口定義時,才可以認(rèn)為是對類或接口的主動使用。
調(diào)用ClassLoader類的loadClass方法加載一個類,并不是對類的主動使用,不會導(dǎo)致類的初始化。
?
除了以上虛擬機(jī)自帶的加載器外,用戶還可以定制自己的類加載器。Java提供了抽象類java.lang.ClassLoader,所有用戶自定義的類加載器都應(yīng)該繼承ClassLoader類
?
引申看下這個例子:
public class MyTest {public static void main(String[] args) {System.out.println(MyChild.b);} }interface MyParent{public static int a = 5; } interface MyChild extends MyParent{public static final int b = 8; }分析:
MyTest類有main函數(shù)。會主動使用,先去加載。
接口和類其實(shí)是不同的,如下:
加載層面:?
如果是類的話,MyChild肯定會被加載。如果是接口的話,不會被加載。
如果把b 修改為 Random(運(yùn)行期才知道的值)。會將Parend 和 Child都加載. 很重要的一點(diǎn)是變量是編譯器的還是運(yùn)行期才能確定的
如果 parent和child都是final,test用到的常量會放入自己的常量池中,則不會對parent和child進(jìn)行加載了。
如果把接口換做class,則存在加載,不加載的話必須是final的!
總結(jié)出了final關(guān)鍵字的區(qū)別小結(jié):
- ? final修飾的變量,決定當(dāng)前類是否加載。(static修飾的,不會這樣)
- ? implement 實(shí)現(xiàn)的接口,不會加載
final修飾后,哪個類去主動調(diào)用就將這個常量放入到自己類的常量池里面。
Remember:
?block 優(yōu)先 構(gòu)造函數(shù)執(zhí)行,每次都執(zhí)行。
證明初始化一個類時候,不會初始化他的接口:
public class MyTest5 {public static void main(String[] args) {System.out.println(MyChild5.b);} }interface MyParent5 {public static Thread thread = new Thread(){{System.out.println("MyParent5 Thread ==========");}}; }interface MyChild5 extends MyParent5 {public static int b = 6; } class C{{System.out.println("hello c{block}");}public C(){System.out.println("hello c(construct)");} }?如果將父子的interface 改成class 則會初始化父類
?當(dāng)一個類被初始化時候,他所實(shí)現(xiàn)的類是不會被初始化的。
繼續(xù)看下面例子:
public class MyTest5 {public static void main(String[] args) {System.out.println(MyChild5.b);} }interface MyGrandPa{public static Thread thread = new Thread(){{System.out.println("MyGrandPa Thread ==========");}}; }interface MyParent5 extends MyGrandPa{public static Thread thread = new Thread(){{System.out.println("MyParent5 Thread ==========");}}; }interface MyChild5 extends MyParent5 {public static int b = 6; } class C{{System.out.println("hello c{block}");}public C(){System.out.println("hello c(construct)");} }總結(jié):
類加載器的雙親委派機(jī)制:
? 在雙親委派機(jī)制中,各個加載器按照父子關(guān)系形成了樹形結(jié)構(gòu),除了根類加載器之外,其余的類加載器都有且只有一個父類加載器。
如果有一個類加載器能夠成功加載Test類,那么這個類加載器被稱為定義類加載器,所有能夠成功返回Class對象引用的類加載器(包括定義類加載器)都被稱為初始化類加載器。(了解即可)
public class MyTest7 {public static void main(String[] args) throws ClassNotFoundException {Class<?> clazz = Class.forName("java.lang.String");System.out.println(clazz.getClassLoader());Class<?> mClazz = Class.forName("com.jvm.t1.M");System.out.println(mClazz.getClassLoader());} } //位于工程的classPath目錄地址下 class M{}如下例子:
package com.jvm.t1;public class MyTest9 {static {System.out.println("MyTest9 static block");}public static void main(String[] args) {System.out.println(Child.b);} }class Parent{static int a = 3;static {System.out.println("parent static block");} }class Child extends Parent{static int b = 4;static {System.out.println("chile static block");} }?便于查看加載過程清晰:
輸出結(jié)果:
看下面的例子:
public class MyTest10 {static {System.out.println("MyTest10 static block");}public static void main(String[] args) {//聲明類型的使用,并不是主動使用Parent2 parent2;System.out.println("-------");parent2 = new Parent2();System.out.println("---------");System.out.println(parent2.a);System.out.println("---------");System.out.println(Child2.b);} } class Parent2{static int a = 3;static {System.out.println("Parent2 static block");} }class Child2 extends Parent2{static int b = 4;static {System.out.println("Child2 static block");} }?使用child時候,parent已經(jīng)被初始化了,只會初始化一次。
總結(jié):
初始化一次就OK了。?
看下面例子:
class Parent3{static int a = 3;static {System.out.println("Parent3 static block");}static void doSomeThing(){System.out.println("do something");} } class Child3 extends Parent3{static {System.out.println("Child3 static block");} }public class MyTest11 {public static void main(String[] args) {//訪問父類的。調(diào)用父類的Parent的(主動使用)System.out.println(Child3.a);//訪問的父類的。調(diào)用父類的Parent的(主動使用)Child3.doSomeThing();} }總結(jié):?
- 雖然名字是Child3 但是沒有對其主動使用。
- 如果使用子類去訪問父類定義的變量、方法,本質(zhì)上都表示對于父類的主動使用!
看下面例子:
class CL{static {System.out.println("static block class CL");} }public class MyTest12 {public static void main(String[] args) throws ClassNotFoundException {//系統(tǒng)類加載器(應(yīng)用類加載器)ClassLoader classLoader = ClassLoader.getSystemClassLoader();//指定加載的類//這個不會導(dǎo)致類的初始Class<?> clazz = classLoader.loadClass("com.jvm.t1.CL");System.out.println(clazz);System.out.println("-------");//類的初始化,反射導(dǎo)致類的初始化clazz = Class.forName("com.jvm.t1.CL");System.out.println(clazz);} }總結(jié):
- 調(diào)用classLoader.loadClass 不是對類的主動使用,不會導(dǎo)致初始化
- 反射是對類的主動使用
關(guān)于雙親委派機(jī)制:?
public class MyTest13 {public static void main(String[] args) {ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();System.out.println(systemClassLoader);while (null != systemClassLoader){systemClassLoader = systemClassLoader.getParent();System.out.println(systemClassLoader);}} }?結(jié)論:
?在HotSpot中,BootStrap ClassLoader使用null表示的.(啟動類加載器)
看下面例子:
public class MyTest14 {public static void main(String[] args) {//獲取上下文的類加載器。線程創(chuàng)建者提供的。(有默認(rèn)值的)ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();System.out.println(contextClassLoader);} }類型是APPClassLoader,加載應(yīng)用的類加載器(系統(tǒng)類加載器)。
看下面的例子:
public class MyTest14 {public static void main(String[] args) throws IOException {//獲取上下文的類加載器。線程創(chuàng)建者提供的。(有默認(rèn)值的)ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();//存在磁盤上的字節(jié)碼(磁盤上的目錄)String resourceName = "com/jvm/t1/MyTest13.class";//給定名字的所有資源(圖片、音頻等)Enumeration<URL> resources = contextClassLoader.getResources(resourceName);while (resources.hasMoreElements()){URL url = resources.nextElement();System.out.println(url);}} } ?獲取ClassLoader的途徑:
?我們自己定義的類,APPClassLoader:
public class MyTest14 {public static void main(String[] args) throws IOException {Class<MyTest14> myTest14Class = MyTest14.class;System.out.println(myTest14Class.getClassLoader());} } public class MyTest14 {public static void main(String[] args) throws IOException {Class<String> stringClass = String.class;System.out.println(stringClass.getClassLoader());} }?String 這個類位于rt.jar
用戶自定義的類加載器都直接或間接的從ClassLoader類繼承下來。
數(shù)組類的Class對象并不是由類加載器創(chuàng)建的,運(yùn)行時由于Java虛擬機(jī)自動創(chuàng)建的。只有數(shù)組如此
public class MyTest15 {public static void main(String[] args) {String[] strings = new String[2];System.out.println(strings.getClass().getClassLoader());System.out.println("--------------");MyTest15[] myTest15s = new MyTest15[12];System.out.println(myTest15s.getClass().getClassLoader());System.out.println("--------------");int[] ins = new int[2];System.out.println(ins.getClass().getClassLoader());} }總結(jié):
- 根據(jù)里面的每個元素的類型定義的!String、MyTest15。?
- 雖然獲取到了數(shù)組的類加載器,但是數(shù)組對應(yīng)的Class對象并不是ClassLoader加載的,是JVM動態(tài)創(chuàng)建的。
- 原生類型,沒有加載器。
?自己定義類加載器,看下面例子:
public class MyTest16 extends ClassLoader {private String classLoaderName = "";private String fileExtension = ".class";public MyTest16(String classLoaderName) {super(); // 將系統(tǒng)類加載器當(dāng)做該類加載器的父類加載器this.classLoaderName = classLoaderName;}public MyTest16(ClassLoader parent, String classLoaderName) {super(parent); //顯示指定該類的加載器的父類加載器this.classLoaderName = classLoaderName;}private byte[] loadClassData(String name) {InputStream is = null;byte[] data = null;ByteArrayOutputStream baos = null;try {//注意win和linuxthis.classLoaderName = this.classLoaderName.replace(".", "/");is = new FileInputStream(new File(name + this.fileExtension));baos = new ByteArrayOutputStream();int ch ;while (-1 != (ch = is.read())) {baos.write(ch);}// 字節(jié)數(shù)組輸出流轉(zhuǎn)換成字節(jié)數(shù)組data = baos.toByteArray();} catch (Exception e) {e.printStackTrace();} finally {try {is.close();baos.close();} catch (Exception e) {e.printStackTrace();}}return data;}@Overrideprotected Class<?> findClass(String className) throws ClassNotFoundException {byte[] data = this.loadClassData(className);//返回Class對象return this.defineClass(className, data, 0 , data.length);}public static void test(ClassLoader classLoader) throws ClassNotFoundException, IllegalAccessException, InstantiationException {//內(nèi)部底層的api已經(jīng)被我們重寫了Class<?> clazz = classLoader.loadClass("com.jvm.t1.MyTest15");Object object = clazz.newInstance();System.out.println(object);}@Overridepublic String toString() {return "[" + this.classLoaderName + "]";}public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {MyTest16 loader1 = new MyTest16("loader1");test(loader1);} }其實(shí)此時我們定義的 findClass是沒有被調(diào)用的!以為雙親委派機(jī)制,讓父類去加載了!
看下面例子:
public class MyTest16 extends ClassLoader {private String classLoaderName = "";private String fileExtension = ".class";private String path;public MyTest16(String classLoaderName) {super(); // 將系統(tǒng)類加載器當(dāng)做該類加載器的父類加載器this.classLoaderName = classLoaderName;}public void setPath(String path){this.path = path;}public MyTest16(ClassLoader parent, String classLoaderName) {super(parent); //顯示指定該類的加載器的父類加載器this.classLoaderName = classLoaderName;}private byte[] loadClassData(String className) {InputStream is = null;byte[] data = null;ByteArrayOutputStream baos = null;className.replace(",","/");try {//注意win和linuxthis.classLoaderName = this.classLoaderName.replace(".", "/");//指定磁盤全路徑is = new FileInputStream(this.path + new File(className + this.fileExtension));baos = new ByteArrayOutputStream();int ch ;while (-1 != (ch = is.read())) {baos.write(ch);}// 字節(jié)數(shù)組輸出流轉(zhuǎn)換成字節(jié)數(shù)組data = baos.toByteArray();} catch (Exception e) {e.printStackTrace();} finally {try {is.close();baos.close();} catch (Exception e) {e.printStackTrace();}}return data;}@Overrideprotected Class<?> findClass(String className) throws ClassNotFoundException {System.out.println("findClass invoked:" + className);System.out.println("class loader name" + this.classLoaderName);byte[] data = this.loadClassData(className);//返回Class對象return this.defineClass(className, data, 0 , data.length);}@Overridepublic String toString() {return "[" + this.classLoaderName + "]";}public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException {// 創(chuàng)建自定義類加載器 名字“l(fā)oader1” 父類加載器是系統(tǒng)類加載器MyTest16 loader1 = new MyTest16("loader1");//此路徑為classPath,故 findClass方法不會被調(diào)用執(zhí)行! 如果換個路徑,不是classPath就會去執(zhí)行了!loader1.setPath("D:\\eclipse_pj\\dianshang\\jvmTest\\out\\production\\jvmTest\\");Class<?> clazz = loader1.loadClass("com.jvm.t1.MyTest15");System.out.println("class:"+ clazz.hashCode());Object object = clazz.newInstance();System.out.println(object);} }?委托給父類,父類去classPath目錄下面找,找到了加載之。
關(guān)于命名空間:
- 每個類加載器都有自己的命名空間,命名空間由該加載器及所有父加載器所加載的類組成
- 同一個命名 空間中,不會出現(xiàn)類的完整名字(包括類的包名)相同的兩個類
- 在不同的命名空間中,有可能會出現(xiàn)類的完整名字(包括類的包名)相同的兩個類?
關(guān)于類的卸載:
- ?當(dāng)MySample類被加載、連接和初始化后,他的聲明周期就開始了。當(dāng)代表MySample類的Class對象不再被引用,即不可觸及時,Class對象就會結(jié)束聲明周期,MySample類在方法區(qū)內(nèi)的數(shù)據(jù)也會被卸載,從而結(jié)束Sample類的生命周期。
- ?一個類何時結(jié)束生命周期,取決于代表他的Class對象何時結(jié)束生命周期。
- 由用戶自定義的類加載器所加載的類是可以被卸載的。??
? 加載? <----> 卸載
?看下面的例子:
public class MySample {MySample(){System.out.println("MySample is loaded by"+ this.getClass().getClassLoader());MyCat myCat = new MyCat();} } public class MyCat {public MyCat() {System.out.println("MyCat is loaded by" + this.getClass().getClassLoader());} } public class MyTest17 {public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException {MyTest16 loader1 = new MyTest16("loader1");//要加載的類Class<?> clazz = loader1.loadClass("com.jvm.t1.MySample");System.out.println("clazz"+ clazz.hashCode());//如果注釋掉改行,那么并不會實(shí)例化MySample對象,即MySample構(gòu)造方法不會被調(diào)用// 因此不會實(shí)例化MyCat對象,即沒有對MyCat進(jìn)行主動使用,這里就不會加載MyCat classObject object = clazz.newInstance();// new instance 沒有任何參數(shù)。調(diào)用無參構(gòu)造方法} }?關(guān)于命名空間的說明:?
情況1.如果 class字節(jié)碼在classPath,返回 true。 執(zhí)行成功。(讀者自行考慮,提示雙親委派)
情況2.如果 class字節(jié)碼只在:"/User/test/" 。返回false。執(zhí)行報錯。
? ? ? ?原因
- ? ?命名空間。 兩個loader不存在父子關(guān)系,是平行的。在jvm中存在兩個命名空間。
- ? ?不同命名空間的類不可見,引用不到就報錯。(子加載器的命名空間包含所有父加載器的命名空間,子可看到父類加載的所有類。)
雙親委派的好處:
知識總結(jié):?
簡單看下:
public class test4 {public static void main(String[] args) {System.out.println(ClassLoader.class.getClassLoader());//擴(kuò)展類System.out.println(Launcher.class.getClassLoader());} } ?可以自己做系統(tǒng)類加載器。略。需要控制臺指令顯示指定
通過改變屬性,提示:
System.getProperty("java.system.class.loader")
引申:
?getSystemClassLoader()
- ? 返回用于委托的系統(tǒng)類加載器,
- ? 創(chuàng)建的ClassLoader默認(rèn)的爸爸(也是用于啟動應(yīng)用的類加載器)。
- ? 創(chuàng)建類加載器,然后設(shè)置為調(diào)用這個方法的線程的上下文類加載器。(Contex Class Loader)。應(yīng)用框架,服務(wù)器大量使用的!
- ? 默認(rèn)的系統(tǒng)類加載器,與此類實(shí)現(xiàn)相關(guān)的實(shí)例。?
- ? java.system.class.loader所指定的類,是被默認(rèn)的系統(tǒng)類加載器加載。必須要定義public的構(gòu)造方法,傳遞自定義類加載器的爸爸。
OpenJDK是JDK開源版本。
解析Class.forName:
?其實(shí):Class.forName("Foo") 等價于 Class.forName("Foo",true, this.getClass().getClassLoader() )?
關(guān)于線程上下文的類加載器: Thread.currentThread().setContextClassLoader(sys)
作用就是改變雙親委派模型在某些場景下不適用的情況。
看下面例子:
public class MyTest24 {public static void main(String[] args) {System.out.println(Thread.currentThread().getContextClassLoader());System.out.println(Thread.class.getClassLoader()); // 路徑位置導(dǎo)致的} }
當(dāng)前類加載器(Current ClassLoader)
每個類都會使用自己的類加載器(即加載自身的類加載器)去加載其它類(指的是所依賴的類):
? 如果ClassX引用了ClassY,那么ClassX的類加載器就會去加載ClassY(前提是ClassY尚未被加載)
線程上下文類加載器:
- ?線程上下文類加載器是從JDK1.2開始引入的,類Thread中的getContextClassLoader? 與 setContextClassLoader(ClassLoader cl) 分別用來獲取和設(shè)置上下文類加載器
- 如果沒有通過setContextClassLoader進(jìn)行設(shè)置的話。線程將繼承其父線程的上下文類加載器。
- Java應(yīng)用運(yùn)行時的初始線程的上下文類加載器是系統(tǒng)類加載器。在線程中運(yùn)行的代碼可以通過該類加載器來加載類與資源。
?線程上下文類加載器的重要性:
?應(yīng)用場景:?
?SPI(Service Provider Interface)
?父ClassLoader可以使用當(dāng)前線程Thread.currentThread().getContexClassLoader() 所指定的ClassLoader加載的類,這就改變了父ClassLoader不能使用子ClassLoader或是其他沒有直接父子關(guān)系的ClassLoader加載的類的情況。
?線程上下文類加載器就是當(dāng)前線程的Current ClassLoader
在雙親委派模型下,類加載是由下至上的,即下層的類加載器會委托上層進(jìn)行加載。但是對于SPI來說,有些接口是Java類核心庫所提供的,而Java核心庫是由啟動類加載器來加載的,而這些接口的實(shí)現(xiàn)卻來自于不同的jar包(廠商提供。
Java的啟動類加載器是不會加? 載其他來源你的Jar包 ,這樣的傳統(tǒng)的雙親委派模型就無法滿足SPI的要求。而通過給當(dāng)前線程設(shè)置上下文類加載器,就可以由設(shè)置的上下文類加載器來實(shí)現(xiàn)對于接口實(shí)現(xiàn)類的加載。
?總結(jié):接口是啟動類加載器加載的, 實(shí)現(xiàn)類應(yīng)用類加載器加載,通過給當(dāng)前的線程設(shè)置上下文類加載器,實(shí)現(xiàn)對于接口實(shí)現(xiàn)類的加載,打破了雙親委派模型現(xiàn)在。(框架開發(fā),底層開發(fā)會用到)
(JDK中沒有對于JDBC的任何實(shí)現(xiàn),除了傳統(tǒng)的接口之外,具體實(shí)現(xiàn)都是由廠商趨勢線的,比如MySQL。)
看下面代碼:
public class MyTest25 implements Runnable {private Thread thread;public MyTest25(){thread = new Thread(this);thread.start();}@Overridepublic void run() {// 獲取到上下文類加載器ClassLoader classLoader = this.thread.getContextClassLoader();this.thread.setContextClassLoader(classLoader);System.out.println("Class:"+classLoader.getClass());System.out.println("Class:"+classLoader.getParent().getClass());}public static void main(String[] args) {MyTest25 myTest25 = new MyTest25();} }沒有設(shè)置,所以線程將繼承父線程的上下文類加載器。
線程上下文類加載器的一般使用模式(獲取 - 使用 - 還原)
注意:如果一個類由A加載器加載,那么這個類的依賴也是由相同的類加載器加載的(如果該依賴之前沒有被加載過的話)
ContextClassLoader的作用就是為了破壞Java的類加載委托機(jī)制??
當(dāng)高層提供了統(tǒng)一的接口讓底層去實(shí)現(xiàn),同時又要在高層加載(或者實(shí)例化)低層的類時候,就必須要通過線程上下文類加載器來幫助高層的ClassLoader找到并加載該類
看下面例子:
public class MyTest26 {public static void main(String[] args) {//設(shè)置下// Thread.currentThread().setContextClassLoader(MyTest26.class.getClassLoader());ServiceLoader<Driver> loader = ServiceLoader.load(Driver.class);Iterator<Driver> iterator = loader.iterator();while (iterator.hasNext()){Driver driver = iterator.next();System.out.println("driver" + driver.getClass() + ", loader" + driver.getClass().getClassLoader() );}System.out.println("當(dāng)前線程上下文類加載器:" + Thread.currentThread().getContextClassLoader());System.out.println("ServiceLoader的類加載器:" + ServiceLoader.class.getClassLoader());} }?
關(guān)于字節(jié)碼:
對于能編譯成class字節(jié)碼的代碼,class的規(guī)范,合法性保證好了就OK了。
對于Idea編譯器,是非常熟悉class字節(jié)碼了,可以隨心所欲的反編譯。
對于java代碼:
public class MyTest1 {private int a = 1;public int getA() {return a;}public void setA(int a) {this.a = a;} }idea看字節(jié)碼:
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) //package com.jvm.t1.t2;public class MyTest1 {private int a = 1;public MyTest1() {}public int getA() {return this.a;}public void setA(int a) {this.a = a;} }通過反編譯指令:
?看到三個方法:其中一個是默認(rèn)的構(gòu)造方法。
詳細(xì)查看字節(jié)碼信息:輸入
javap -c com.jvm.t1.t2.MyTest1
Compiled from "MyTest1.java" public class com.jvm.t1.t2.MyTest1 {//構(gòu)造方法public com.jvm.t1.t2.MyTest1();
//下面都是助記符Code:0: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: aload_05: iconst_16: putfield #2 // Field a:I9: returnpublic int getA();Code:0: aload_01: getfield #2 // Field a:I4: ireturnpublic void setA(int);Code:0: aload_01: iload_12: putfield #2 // Field a:I5: return }
看下面指令:
javap -verbose com.jvm.t1.t2.MyTest1
Classfile /D:/eclipse_pj/dianshang/jvmTest/out/production/jvmTest/com/jvm/t1/t2/MyTest1.classLast modified 2019-10-20; size 473 bytesMD5 checksum c5b1387c6f6c79b14c1b6a5438da3b29Compiled from "MyTest1.java" public class com.jvm.t1.t2.MyTest1minor version: 0major version: 52flags: ACC_PUBLIC, ACC_SUPER// 常量池: 占據(jù)相當(dāng)大的比重 Constant pool:#1 = Methodref #4.#20 // java/lang/Object."<init>":()V#2 = Fieldref #3.#21 // com/jvm/t1/t2/MyTest1.a:I#3 = Class #22 // com/jvm/t1/t2/MyTest1#4 = Class #23 // java/lang/Object#5 = Utf8 a#6 = Utf8 I#7 = Utf8 <init>#8 = Utf8 ()V#9 = Utf8 Code#10 = Utf8 LineNumberTable#11 = Utf8 LocalVariableTable#12 = Utf8 this#13 = Utf8 Lcom/jvm/t1/t2/MyTest1;#14 = Utf8 getA#15 = Utf8 ()I#16 = Utf8 setA#17 = Utf8 (I)V#18 = Utf8 SourceFile#19 = Utf8 MyTest1.java#20 = NameAndType #7:#8 // "<init>":()V#21 = NameAndType #5:#6 // a:I#22 = Utf8 com/jvm/t1/t2/MyTest1#23 = Utf8 java/lang/Object//方法的描述 {public com.jvm.t1.t2.MyTest1();descriptor: ()Vflags: ACC_PUBLICCode:stack=2, locals=1, args_size=10: aload_01: invokespecial #1 // Method java/lang/Object."<init>":()V4: aload_05: iconst_16: putfield #2 // Field a:I9: returnLineNumberTable:line 3: 0line 5: 4LocalVariableTable:Start Length Slot Name Signature0 10 0 this Lcom/jvm/t1/t2/MyTest1;public int getA();descriptor: ()Iflags: ACC_PUBLICCode:stack=1, locals=1, args_size=10: aload_01: getfield #2 // Field a:I4: ireturnLineNumberTable:line 8: 0LocalVariableTable:Start Length Slot Name Signature0 5 0 this Lcom/jvm/t1/t2/MyTest1;public void setA(int);descriptor: (I)Vflags: ACC_PUBLICCode:stack=2, locals=2, args_size=20: aload_01: iload_12: putfield #2 // Field a:I5: returnLineNumberTable:line 12: 0line 13: 5LocalVariableTable:Start Length Slot Name Signature0 6 0 this Lcom/jvm/t1/t2/MyTest1;0 6 1 a I } SourceFile: "MyTest1.java"
使用如上的這個命令分析字節(jié)碼時候,將會分析該字節(jié)碼文件的魔數(shù),版本號,常量池,類信息,類的構(gòu)造方法,類中的方法信息,類變量與成員變量等信息。
備注:
?魔數(shù): 所有的.class字節(jié)碼文件的前4個字節(jié)都是魔數(shù),魔數(shù)值為固定值: 0xCAFEBABE。
?魔數(shù)之后的4個字節(jié)為版本信息,前兩個字節(jié)表示minor version(次版本號),后兩個字節(jié)表示major? version(主版本號)。
常量池(constant pool): 緊接著主板號之后就是常量池入口。一個Java類中定義的很多信息都是由常量池來維護(hù)和描述的。常量池在整個字節(jié)碼文件中占的比重最大,里面的信息會被很多地方引用到。相當(dāng)于把常量集中在一個地方,其他地方用到時候去引用之。通過Index找到常量池中特定的常量。可以將常量池看做是class文件的資源倉庫。比如:Java類總定義的方法與變量信息,都是存儲在常量池中。常量池中主要存儲兩類常量:字面量與符號引用量。
注意:常量池!里面存放的不一定都是常量。也有變量信息。
- 字面量如文本字符串,Java中聲明為final 的常量值等,而符號引用,比如說類和接口的全局限定名,字段的名稱和描述符,方法的名稱和描述符等。
常量池的總體結(jié)構(gòu): Java類所對應(yīng)的常量池主要由常量池數(shù)量與常量池數(shù)組(常量表)這兩部分共同組成。常量池數(shù)量緊跟在主版本后面,占據(jù)2個字節(jié);常量池數(shù)組則緊跟在常量池數(shù)量之后。常量池數(shù)組和一般的數(shù)組不同的是,常量池數(shù)組中不同的元素的類型,結(jié)構(gòu)都是不同的,長度當(dāng)然也就不同;但是每一種元素的數(shù)都是一個u1類型,該字節(jié)是個標(biāo)志位,占據(jù)1個字節(jié)。JVM在解析常量池時候,會根據(jù)這個u1類型來獲取元素的具體類型。值得注意的是:常量池數(shù)組中元素的個數(shù) = 常量池數(shù) - 1 (其中0暫時不使用)。目的是滿足某些常量池索引值的數(shù)據(jù)在特定情況下需要表達(dá) 【不引用任何一個常量池】的含義。根本原因在于,索引為0也是一個常量(保留常量)。只不過它不位于常量表中,這個常量就對應(yīng)null值。所以常量池的索引從1而非從0開始。
如下,從1開始:
常量池中數(shù)據(jù)類型:
?在JVM規(guī)范中,每個變量/字段都有描述信息,描述信息主要的作用是描述字段的數(shù)據(jù)類型、方法的參數(shù)列表(包括數(shù)量、類型與順序)與返回值。根據(jù)描述符規(guī)則,基本數(shù)據(jù)類型和代表無返回值的void類型都
用一個大寫字符來表示,對象類型則使用字符L加對象的全限定名稱來表示。為了壓縮字節(jié)碼文件的體積。對于基本數(shù)據(jù)類型,JVM都只使用一個大寫字母來表示,如下所示:
B ---> byte? ?C --> char? D ---> doube F ---> float? I --> int? J --long S? --> short? Z --> boolean? V --> void
L --->對象類型 ,如:?L java/lang/String
對于數(shù)組類型來說,每一個維度使用一個前置的 [ 來表示,如 int[ ]? 被記錄為 [I , String[][] 被記錄為[[ Ljava/lang/String
用描述符描述方法時,按照先參數(shù)列表,后返回值的順序來描述。參數(shù)列表按照參數(shù)的嚴(yán)格順序放在一組之內(nèi),如方法:
get getName (int id, String name)描述為:
常量池里面存儲的各種 index? 和 信息
Java字節(jié)碼整體結(jié)構(gòu):
?完整Java字節(jié)碼接口例子:
Access_Flag訪問標(biāo)志
訪問標(biāo)志信息包括該Class文件是類還是接口,是否被定義成public,是否是abstract,如果是類,是否被聲明稱final。
?
?字段表集合:
? 字段表用于描述類和接口中聲明的變量。這里的字段包含了類級別變量(靜態(tài)變量)以及實(shí)例變量(非靜態(tài)變量),但是不包括方法內(nèi)部聲明的局部變量。
一個field_info包含的信息:
?
? 方法表:
methods_count: u2
前三個字段和field_info一樣
?方法中每個屬性都是一個attribute_info結(jié)構(gòu)
JVM預(yù)定義了部分attribute,但是編譯器自己也可以實(shí)現(xiàn)自己的attribute寫入class文件里,供運(yùn)行使用
不同的attribute通過attribute_name_index來區(qū)分
?Code結(jié)構(gòu)
Code attribute的作用是保存該方法的結(jié)構(gòu),如所對應(yīng)的字節(jié)碼
- attribute_length 表示attribute所包含的字節(jié)數(shù),不包含attribute_name_index 和 attribute_length字段
- max_stack 表示這個方法運(yùn)行的任何時刻所能達(dá)到的操作數(shù)棧的最大深度
- max_locals表示方法執(zhí)行期間創(chuàng)建的局部變量的數(shù)目,包含用來表示傳入的參數(shù)的局部變量?
- code_length表示該方法所包含的字節(jié)碼的字節(jié)數(shù)以及具體的指令碼
- 具體字節(jié)碼即是該方法被調(diào)用時,虛擬機(jī)所執(zhí)行的字節(jié)碼
- exception_table,這里存放的是處理異常的信息
- 每個exception_table表項由start_pc, end_pc, handler_pc, catch_type 組成
- start_pc和end_pc表示在code數(shù)組中的從start_pc到end_pc處(包含start_pc, 不包含end_pc)的指令拋出的異常會由這個表項來處理。
- handeler_pc 表示處理異常的代碼的開始處,catch_type 表示會被處理的異常類型,它指向常量池里的一個異常類。當(dāng)catch_type為0時,表示處理所有異常。
- 方法中的每個屬性都是一個attribute_info結(jié)構(gòu)
?code attribute的作用是保存該方法的結(jié)構(gòu),如所對應(yīng)的字節(jié)碼
?
?推薦大家使用:??jclasslib 閱讀字節(jié)碼信息
?Java中,每一個方法都是可以訪問this(表示對當(dāng)前對象的引用),
?字節(jié)碼角度,如果方法本身是個非靜態(tài)(實(shí)例)的,this可以作為方法的第一個方法,可以隱式的傳遞進(jìn)來。會使得每個實(shí)例方法都可以訪問this。至少會有個局部變量,這個局部變量就是this。
對于某各類Test,中的靜態(tài)方法 使用了synchronized 關(guān)鍵字,相當(dāng)于給這個Test對應(yīng)的Class對象加鎖了。
關(guān)于this關(guān)鍵字:?
Java編譯器在編譯時候,把對this方法的訪問,轉(zhuǎn)變成了對普通參數(shù)的訪問。在Java中,每一個非靜態(tài)實(shí)例的方法的局部變量中,至少會存在一個指向當(dāng)前對象的局部變量。即:
對于Java類中的每一個實(shí)例方法(非static方法),其中在編譯后所生成的字節(jié)碼當(dāng)中,方法參數(shù)的數(shù)量總會比源代碼匯總方法的參數(shù)多一個(this),它位于方法的第一個參數(shù)位置處;這樣我們就可以在Java實(shí)例方法中使用this訪問當(dāng)前對象的屬性以及其他方法。這個操作是在編譯期間完成的,由javac編譯器,在編譯時候?qū)his的訪問轉(zhuǎn)化為對一個普通實(shí)例方法參數(shù)的訪問,接下來在運(yùn)行期間,由JVM在調(diào)用實(shí)例方法時,自動向?qū)嵗椒▊魅朐搕his參數(shù)。所以,在實(shí)例方法的局部變量表中,至少會有一個指向當(dāng)前對象的局部變量。
關(guān)于異常處理:
?Code結(jié)構(gòu):
?attribute_length表示attribute鎖包含的字節(jié)數(shù),不包含attribute_name_index和attribute_length字段
?max_stack表示這個方法運(yùn)行的任何時刻所能達(dá)到的操作數(shù)棧的最大深度
?max_locals表示方法執(zhí)行期間所創(chuàng)建的局部變量的數(shù)目,包含用來表示傳入的參數(shù)的局部變量
?code_lenght表示該方法所含的字節(jié)碼的字節(jié)數(shù)以及具體的指令碼
?具體字節(jié)碼即是該方法被調(diào)用時,虛擬機(jī)所執(zhí)行的字節(jié)碼
?exception_table, 這里存放的是處理異常的消息
?每個exception_tabel 表項由start_pc, end_pc , handler_pc ,catch_type 組成
?start_pc 和 end_pc 表示在code 數(shù)組中的從start_pc都end_pc處(包含start_pc, 不包含end_pc)的指令拋出的異常會由這個表項來處理
?handler_pc表示處理異常的代碼的開始處。catch_type 表示會被處理的異常類型,它指向常量池里的一個異常類。當(dāng)catch_type為0時,表示處理所有的異常。
Java字節(jié)碼對于異常的處理方式:
?1.? 統(tǒng)一采用異常表的方式來對異常進(jìn)行處理
?2. 老版本中,并不是使用遺產(chǎn)表的方式來對異常進(jìn)行處理的,而是采用特定的指令方式(了解)
?3. 當(dāng)異常處理存在finally語句塊時,現(xiàn)代化的JVM采取的方式將finally語句塊的字節(jié)碼拼接到每一個catch塊后面,換句話說,程序存在多少個catch塊,就會在每一個catch塊后面重復(fù)多少個finally語句塊的字節(jié)碼。
棧幀,是一種用于幫助虛擬機(jī)執(zhí)行方法調(diào)用與方法執(zhí)行的數(shù)據(jù)結(jié)構(gòu)。
棧幀, 本身是一種數(shù)據(jù)結(jié)構(gòu),封裝了風(fēng)閥的局部變量表,動態(tài)鏈接信息,方法的返回地址操作數(shù)棧等信息。
Java中,對于不同的類之間的關(guān)系,編譯期間,地址關(guān)系實(shí)際上是不知道的。什么時候知道?
? 1. 類加載時候
? 2. 真正調(diào)用時候,才知道目標(biāo)方法地址。
基于以上兩點(diǎn),引申出了符號引用和直接引用。
有些符號引用是在類加載階段或是第一次使用時就會轉(zhuǎn)換為直接引用,這種轉(zhuǎn)換叫做靜態(tài)解析;另外一些符號引用則是在每次運(yùn)行期轉(zhuǎn)為直接引用,這種轉(zhuǎn)換叫做動態(tài)鏈接,這體現(xiàn)為Java的多態(tài)性
比如父類因用戶指向子類實(shí)現(xiàn)。
Aninaml a = new Cat();a.run();a = new Fish();a.run編譯時候,a都是Animal.? 字節(jié)碼角度,都是Animal
運(yùn)行時候,每次運(yùn)行期,都會進(jìn)行一次直接引用的轉(zhuǎn)換。
JVM 方法調(diào)用的字節(jié)碼指令:
?1. invokeinterface:調(diào)用接口中的方法,實(shí)際上是在運(yùn)行期決定的,決定到底調(diào)用實(shí)現(xiàn)該接口的那個對象的特定方法(一個接口,n個實(shí)現(xiàn)類)。
?2. invokestatic: 調(diào)用靜態(tài)方法
?3.invokespecial: 調(diào)用自己的私有方法,構(gòu)造方法(<init>) 以及父類的方法
?4. invokevirtual: 調(diào)用虛方法,運(yùn)行期動態(tài)查找的過程。
?5. invokedynamic: 動態(tài)調(diào)用方法。
靜態(tài)解析的四種情況:
? 1. 靜態(tài)方法
? 2.父類方法
? 3. 構(gòu)造方法
?4. 私有方法(公有方法可以被重寫或者復(fù)寫,多態(tài)的可能。私有方法在加載時候就能夠被確定了)
以上四種稱之為: 非虛方法。他們是在類加載階段就可以將符號引用轉(zhuǎn)換為直接引用的。
public class MyTest5 {public void test(GrandPa grandPa){System.out.println("grandPa");}public void test(Father father){System.out.println("father");}public void test(Son son){System.out.println("son");}public static void main(String[] args) {//都是GrandPal類型的GrandPa father = new Father();GrandPa son = new Son();MyTest5 myTest5 = new MyTest5();myTest5.test(father);myTest5.test(son);}}class GrandPa{} class Father extends GrandPa{} class Son extends Father{以上代碼 ,? father的靜態(tài)類型是Grandpa,而father的實(shí)際類型(真正指向的類型)是Father??
變量本身的靜態(tài)類型是不會被改變的,?GrandPa father
結(jié)論:
?變量的靜態(tài)類型是不會發(fā)生變化的,而變量的實(shí)際類型是可以發(fā)生變化的(多態(tài)的一種體現(xiàn))。實(shí)際類型是在運(yùn)行期方可確定。
以上,方法的重載,參數(shù)類型不一樣。方法重載是一種純粹的靜態(tài)行為。
所以,當(dāng)使用myTest5調(diào)用方法的時候, 是根據(jù)類型進(jìn)行匹配。尋找類型是 GrandPa的。編譯器就可以完全確定的。
public class MyTest6 {public static void main(String[] args) {Fruit apple = new Apple();Fruit orange = new Orange();apple.test();orange.test();apple = new Orange();apple.test();} }class Fruit{public void test(){System.out.println("fruit");} } class Apple extends Fruit{@Overridepublic void test() {System.out.println("apple");} } class Orange extends Fruit{@Overridepublic void test() {System.out.println("orange");} }引申:
Java中,new起到了三個作用:
?1. 在堆上開辟空間
?2. 執(zhí)行構(gòu)造方法
?3. 將構(gòu)造方法執(zhí)行后返回的堆上的此引用值返回
方法的動態(tài)分派:
?方法的動態(tài)分派涉及到一個重要概念:方法接收者
?invokevirtual字節(jié)碼指令的多態(tài)查找流程
?方法重載和方法重寫,我們可以得到這個方法重載是靜態(tài)的,是編譯器行為,方法重寫是動態(tài)的,是運(yùn)行期行為。
public class MyTest7 {public static void main(String[] args) {Animal animal = new Animal();Dog dog = new Dog();animal.test("hello");dog.test(new Date( ));} }class Animal{public void test(String str){System.out.println("animal str");}public void test(Date date){System.out.println("animal date");} }class Dog extends Animal{@Overridepublic void test(String str) {System.out.println("dog str");}@Overridepublic void test(Date date) {System.out.println("dog date");} } ?針對于方法調(diào)用動態(tài)分派的過程,虛擬機(jī)會在類的方法區(qū)建立一個虛方法表的數(shù)據(jù)結(jié)構(gòu)(virtual method table,簡稱 vtable)
現(xiàn)代JVM在執(zhí)行Java代碼的時候,通常會將解釋執(zhí)行與編譯執(zhí)行二者結(jié)合起來執(zhí)行。
所謂解釋執(zhí)行:通過解釋器讀取字節(jié)碼,遇到相應(yīng)的指令就去執(zhí)行該指令
所謂編譯執(zhí)行:通過及時編譯器(Just In Time, JIT)將字節(jié)碼轉(zhuǎn)為本地機(jī)器碼來執(zhí)行,現(xiàn)代JVM會根據(jù)代碼熱點(diǎn)來生成相應(yīng)的本地機(jī)器碼。
基于棧的指令集合基于寄存器的指令集之間的關(guān)系:
?1. JVM執(zhí)行指令時所采取的的方式是基于棧的指令集
?2. 基于棧的指令集的主要操作: 入棧、出棧
?3. 基于棧的指令集的優(yōu)勢在于他可以在不同平臺間一直,而基于寄存器的指令集是與硬件架構(gòu)密切關(guān)聯(lián)的,無法做到可移植。
?4.? 基于棧的指令集的缺點(diǎn): 完成相同的操作,執(zhí)行數(shù)量通常要比基于寄存器的指令集數(shù)量多 。基于棧的指令集是在內(nèi)存中操作的,而基于寄存器的指令集是直接由CPU執(zhí)行的,它是在高速緩沖區(qū)中進(jìn)行的,速度要快很多。雖然虛擬機(jī)可以采用一些優(yōu)化手段,但總體? ? ? 來說,基于棧的指令集的執(zhí)行速度要慢一些。
注意:
棧 配合 局部變量表使用,局部變量表的0位置是this?
對應(yīng)動態(tài)代理,主要有一個類(proxy)和一個接口(InvocationHandler)去搞定。
接口:
public interface Subject {void request(); }實(shí)現(xiàn)類:
public class RealSubject implements Subject {@Overridepublic void request() {System.out.println("reslsubjct");} }代理類:
/*** 動態(tài)代理文件*/ public class DynamicSubject implements InvocationHandler {private Object sub;public DynamicSubject(Object obj){this.sub = obj;}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {System.out.println("before calling"+ method);method.invoke(this.sub, args);System.out.println("after calling"+ method);return null;} }測試:
public class Client {public static void main(String[] args) {RealSubject realSubject = new RealSubject();DynamicSubject dynamicSubject = new DynamicSubject(realSubject);Class<?> clazz = realSubject.getClass();//獲取 Class對象是為了,動態(tài)代理需要類加載器。Subject subject = (Subject) Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), dynamicSubject);subject.request();System.out.println(subject.getClass());} }程序運(yùn)行期動態(tài)生成的:
首先創(chuàng)建代理類,然后創(chuàng)建代理類的實(shí)例對象。?
對象分為兩部分內(nèi)容:?
1, 對象本身擁有的那些數(shù)據(jù)(位于堆)
2, 對象所屬的類型(元數(shù)據(jù)信息,MetaData) 所有實(shí)例對應(yīng)一個Class對象。位于方法區(qū)(存儲的一部分對象的類型數(shù)據(jù)信息)
方案一:
對象引用的是一個指向?qū)ο髮?shí)例的指針,另外一個指針指向方法區(qū)中的類型數(shù)據(jù)
方案二:(HotSpot的方案)
對象引用的是對象本身,和一個指向方法區(qū)匯總的類型數(shù)據(jù)指針? (對象實(shí)例數(shù)據(jù)、方法區(qū))
?兩種方案的差別L
堆發(fā)生垃圾回收頻率很高,對于垃圾回收算法來說,有幾種會涉及到對象移動(壓縮):為了保證區(qū)域連續(xù)的地方增大,移動之
方案一:對象一旦移動了,指針值會發(fā)生變化!隨著每次垃圾回收會變化。
方案二:指針不會隨之變化。
?JVM內(nèi)存劃分:
虛擬機(jī)棧
程序計數(shù)器
本地方法棧:主要用于處理本地方法
堆: JVM管理的最大一塊內(nèi)存空間
線程共享的區(qū)域,主要存儲元信息。從JDK1.8開始,徹底廢棄永久代。使用元空間(meta space)
運(yùn)行時常量池(方法區(qū)的一部分): 方法區(qū)的一部分內(nèi)容。編譯后的字節(jié)碼的符號引用等等。加載完后,放入到方法區(qū)的運(yùn)行時常量池。
直接內(nèi)存: Direct Memory。 與Java NIO密切相關(guān),JVM通過堆上的DirectByteBuffer來直接操作內(nèi)存。
現(xiàn)代幾乎所有的垃圾收集器都是采用的分代收集算法,所以堆空間也基于這一點(diǎn)進(jìn)行了相應(yīng)的劃分。
Java對象的創(chuàng)建:
?new
?反射
?克隆
?反序列化
new關(guān)鍵字創(chuàng)建對象的3個步驟:
?1, 在堆內(nèi)存中創(chuàng)建出對象的實(shí)例
?2, 為對象成員變量賦初始值(指的是,實(shí)例變量,區(qū)別靜態(tài)變量)
?3, 將對象的引用返回。?
虛擬機(jī)干的活兒: 檢查指令的參數(shù)new指令創(chuàng)建一個對象,指令參數(shù)是不是能在常量池中定位成一個類的符號引用。查看這個類是不是已經(jīng)加載、鏈接、初始化了。
指針碰撞: 前提是堆中的空間通過一個指針進(jìn)行分割,一側(cè)是已經(jīng)被占用的空間,另一側(cè)是未被占用的空間。
空閑列表:(前提是堆內(nèi)存空間中已被使用與未被使用的空間交織在一起的。這時,虛擬機(jī)就需要通過一個列表來記錄那些空間是可以用的,哪些空間是已被使用的,接下來找出可以容納下新創(chuàng)建對象的且未被使用的空間,在此空間存放該對象,同時還要修改列表的記錄)
一個對象包含三部分布局:
? 1.對象的頭,?
? 2.實(shí)例數(shù)據(jù)(class中定義的成員變量)?
? 3.對齊填充
永久代屬于與堆連接的一個空間,對于永久代處理是比較麻煩的。
元空間,使用的操作系統(tǒng)的本地內(nèi)存。可以不連續(xù)的。元空間里還有元空間虛擬機(jī),管理元空間的內(nèi)存的分配和回收情況。 初始大小21M,隨著對于內(nèi)存占用,會進(jìn)行垃圾回收,甚至內(nèi)存擴(kuò)展,可以擴(kuò)展到內(nèi)存大小的最大值。
存放一個類的元數(shù)據(jù)信息,在框架中,用到運(yùn)行期動態(tài)生成類的手段。動態(tài)創(chuàng)建出來的類,元信息放在元空間。
元空間參數(shù): -XX:MaxMetaspaceSize=200M
在Java虛擬機(jī)(以下簡稱JVM)中,類包含其對應(yīng)的元數(shù)據(jù),比如類的層級信息,方法數(shù)據(jù)和方法信息(如字節(jié)碼,棧和變量大小),運(yùn)行時常量池,已確定的符號引用和虛方法表。
在過去(當(dāng)自定義類加載器使用不普遍的時候,幾乎不動態(tài)搭理),類幾乎是“靜態(tài)的”并且很少被卸載和回收,因此類也可以被看成“永久的”。另外由于類作為JVM實(shí)現(xiàn)的一部分,它們不由程序來創(chuàng)建,因?yàn)樗鼈円脖徽J(rèn)為是“非堆”的內(nèi)存。
在JDK8之前的HotSpot虛擬機(jī)中,類的這些“永久的”數(shù)據(jù)存放在一個叫做永久代的區(qū)域。永久代一段連續(xù)的內(nèi)存空間,我們在JVM啟動之前可以通過設(shè)置-XX:MaxPermSize的值來控制永久代的大小,32位機(jī)器默認(rèn)的永久代的大小為64M,64位的機(jī)器則為85M。永久代的垃圾回收和老年代的垃圾回收是綁定的,一旦其中一個區(qū)域被占滿,這兩個區(qū)都要進(jìn)行垃圾回收。但是有一個明顯的問題,由于我們可以通過?XX:MaxPermSize 設(shè)置永久代的大小,一旦類的元數(shù)據(jù)超過了設(shè)定的大小,程序就會耗盡內(nèi)存,并出現(xiàn)內(nèi)存溢出錯誤(OOM)。
備注:在JDK7之前的HotSpot虛擬機(jī)中,納入字符串常量池的字符串被存儲在永久代中,因此導(dǎo)致了一系列的性能問題和內(nèi)存溢出錯誤。想要了解這些永久代移除這些字符串的信息,請訪問這里查看。
隨著Java8的到來,我們再也見不到永久代了。但是這并不意味著類的元數(shù)據(jù)信息也消失了。這些數(shù)據(jù)被移到了一個與堆不相連的本地內(nèi)存區(qū)域,這個區(qū)域就是我們要提到的元空間。
這項改動是很有必要的,因?yàn)閷τ谰么M(jìn)行調(diào)優(yōu)是很困難的。永久代中的元數(shù)據(jù)可能會隨著每一次Full GC發(fā)生而進(jìn)行移動。并且為永久代設(shè)置空間大小也是很難確定的,因?yàn)檫@其中有很多影響因素,比如類的總數(shù),常量池的大小和方法數(shù)量等。
同時,HotSpot虛擬機(jī)的每種類型的垃圾回收器都需要特殊處理永久代中的元數(shù)據(jù)。將元數(shù)據(jù)從永久代剝離出來,不僅實(shí)現(xiàn)了對元空間的無縫管理,還可以簡化Full GC以及對以后的并發(fā)隔離類元數(shù)據(jù)等方面進(jìn)行優(yōu)化。
移除永久代的影響
由于類的元數(shù)據(jù)分配在本地內(nèi)存中,元空間的最大可分配空間就是系統(tǒng)可用內(nèi)存空間。因此,我們就不會遇到永久代存在時的內(nèi)存溢出錯誤,也不會出現(xiàn)泄漏的數(shù)據(jù)移到交換區(qū)這樣的事情。最終用戶可以為元空間設(shè)置一個可用空間最大值,如果不進(jìn)行設(shè)置,JVM 會自動根據(jù)類的元數(shù)據(jù)大小動態(tài)增加元空間的容量。
注意:永久代的移除并不代表自定義的類加載器泄露問題就解決了。因此,你還必須監(jiān)控你的內(nèi)存消耗情況,因?yàn)橐坏┌l(fā)生泄漏,會占用你的大量本地內(nèi)存,并且還可能導(dǎo)致交換區(qū)交換更加糟糕。
元空間內(nèi)存管理
元空間的內(nèi)存管理由元空間虛擬機(jī)來完成。先前,對于類的元數(shù)據(jù)我們需要不同的垃圾回收器進(jìn)行處理,現(xiàn)在只需要執(zhí)行元空間虛擬機(jī)的 C++ 代碼即可完成。在元空間中,類和其元數(shù)據(jù)的生命周期和其對應(yīng)的類加載器是相同的。話句話說,只要類加載器存活,其加載的類的元數(shù)據(jù)也是存活的,因而不會被回收掉。
我們從行文到現(xiàn)在提到的元空間稍微有點(diǎn)不嚴(yán)謹(jǐn)。準(zhǔn)確的來說,每一個類加載器的存儲區(qū)域都稱作一個元空間,所有的元空間合在一起就是我們一直說的元空間。當(dāng)一個類加載器被垃圾回收器標(biāo)記為不再存活,其對應(yīng)的元空間會被回收。在元空間的回收過程中沒有重定位和壓縮等操作。但是元空間內(nèi)的元數(shù)據(jù)會進(jìn)行掃描來確定 Java 引用。
元空間虛擬機(jī)負(fù)責(zé)元空間的分配,其采用的形式為組塊分配。組塊的大小因類加載器的類型而異。在元空間虛擬機(jī)中存在一個全局的空閑組塊列表。當(dāng)一個類加載器需要組塊時,它就會從這個全局的組塊列表中獲取并維持一個自己的組塊列表。當(dāng)一個類加載器不再存活,那么其持有的組塊將會被釋放,并返回給全局組塊列表。類加載器持有的組塊又會被分成多個塊,每一個塊存儲一個單元的元信息。組塊中的塊是線性分配(指針碰撞分配形式)。組塊分配自內(nèi)存映射區(qū)域。這些全局的虛擬內(nèi)存映射區(qū)域以鏈表形式連接,一旦某個虛擬內(nèi)存映射區(qū)域清空,這部分內(nèi)存就會返回給操作系統(tǒng)。
上圖展示的是虛擬內(nèi)存映射區(qū)域如何進(jìn)行元組塊的分配。類加載器 1 和 3 表明使用了反射或者為匿名類加載器,他們使用了特定大小組塊。 而類加載器 2 和 4 根據(jù)其內(nèi)部條目的數(shù)量使用小型或者中型的組塊。
?參考:https://www.infoq.cn/article/Java-PERMGEN-Removed
命令:jstat -gc 進(jìn)程號? 打印元空間信息
? ? ? ? ? ?jmap -clstats PID? 打印類加載器數(shù)據(jù)
? ? ? ? ? jcmd PID GC.class_stats? 診斷命令
jcmd 是從jdk1.7開始增加的命令
?1. jcmd pid VM.flag:查看JVM啟動參數(shù)
?2. jcmd pid help: 列出當(dāng)前運(yùn)行的Java進(jìn)行可以執(zhí)行的操作
?3. jcmd pid help JFR.dump: 查看具體命令的選項
?4. jcmd pid PerfCounter.print: 查看JVM性能相關(guān)參數(shù)
?5. jcmd pid VM.uptime:查看JVM的啟動時長
?6. jcmd pid GC.class_histogram 查看系統(tǒng)中類的統(tǒng)計信息
?7. jcmd pid Thread.print: 查看線程堆棧信息
?8. jcmd pid GC.heap_dump filename: 導(dǎo)出heap dump文件,導(dǎo)出的文件可以通過jvisualvm查看
?9. jcmd pid VM.system_properties: 查看JVM的屬性信息
10. jcmd pid VM.version: 查看目標(biāo)JVM進(jìn)程的版本信息
11. jcmd pid VM.command_line:查看JVM啟動的命令行參數(shù)信息
jstack: 可以查看或是導(dǎo)出Java應(yīng)用程序中棧線程的堆棧信息
jmc: java? Mission Control
補(bǔ)充:
?針對于犯法調(diào)用動態(tài)分派的過程,虛擬機(jī)會在類的方法區(qū)建立一個虛方法表的數(shù)據(jù)結(jié)構(gòu)(virtual method table, vtable)
?針對于invokeinterface指令來說,迅疾會建立一個叫接口方法表的數(shù)據(jù)結(jié)構(gòu)(interface method table, itable)?
JVM運(yùn)行時數(shù)據(jù)區(qū):
程序計數(shù)器
本地方法棧
Java虛擬機(jī)棧(JVM Stack)
- ? ? Java虛擬機(jī)棧描述的是Java方法的執(zhí)行模型: 每個方法執(zhí)行的時候都會創(chuàng)建一個幀(Frame)棧用于存放局部變量表,操作棧,動態(tài)鏈接,方法出口等信息。一個方法的執(zhí)行過程,就是這個方法對于幀棧的入棧出棧過程。
- ? ?線程隔離
堆?
- ? 堆里存放的是對象的實(shí)例
- ? 是Java虛擬機(jī)管理內(nèi)存中最大的一塊
- ? GC主要的工作區(qū)域,為了高效的GC,會把堆細(xì)分成更多的子區(qū)域
- ?線程共享
方法區(qū):
- ? 存方法每個Class的結(jié)構(gòu)信息,包括常量池,字段描述,方法描述
- ?GC的非主要工作區(qū)域
看下面例子:
public void method(){Object obj = new Object();}生成了兩部分內(nèi)存區(qū)域:
?1.obj這個引用變量,因?yàn)槭欠椒▋?nèi)的變量,放到JVM Stack里面
?2. 真正Object class的實(shí)例對象,放到Heap里面
上述的new語句一共消耗12個byte。JVM規(guī)定引用占4個byte(JVM Stack),而空對象是8個byte(在Heap)
方法結(jié)束后,對應(yīng)Stack中的變量馬上回收,但是Heap中的對象要等GC來回收
垃圾判斷算法:
?引用計數(shù)算法(Reference Counting)?
? ? 無法解決對象循環(huán)引用的問題
?跟搜索算法(Root Tracing)?
? ?GC Root:?
? ? ? 在VM棧(幀中的本地變量)中的引用
? ? ? 方法區(qū)中的引用變量
? ? ? JNI(即一般說的Native方法) 中的引用
方法區(qū):
? Java虛擬機(jī)規(guī)范表示可以不要求虛擬機(jī)在這區(qū)實(shí)現(xiàn)GC,這區(qū)GC的“性價比”一般比較低
? 在堆中,尤其是在新生代,常規(guī)應(yīng)用進(jìn)行一次GC一般可以回收70%~95%的空間,二方法區(qū)的GC效率遠(yuǎn)小于此
? 當(dāng)前的商業(yè)JVM都有實(shí)現(xiàn)方法區(qū)的GC,主要回收兩部分內(nèi)容:廢棄常量與無用類
主要回收兩部分內(nèi)容: 廢棄常量與無用類
?類回收需要滿足如下3個條件:
? 該類所有的實(shí)例都已經(jīng)備GC, 也就是JVM中不存在該Class的任何實(shí)例
? 加載該類的ClassLoader已經(jīng)被GC
? 該類對應(yīng)的Java.lang.Class對象沒有任何地方被引用,如不能在任何地方通過反射訪問該類的方法
? 在大量使用反射、動態(tài)代理、CGLib等字節(jié)碼框架、動態(tài)生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都需要JVM具備類卸載的支持以保證方法區(qū)不會溢出
標(biāo)記清除算法:
? 標(biāo)記+清除兩個過程效率不高嗎,需要掃描所有對象。
? 產(chǎn)生不連續(xù)碎片,空間碎片提多可能會導(dǎo)致后續(xù)使用中無法找到足夠的連續(xù)內(nèi)存而提前觸發(fā)另一次的垃圾收集動作
復(fù)制算法:?
現(xiàn)在的商業(yè)虛擬機(jī)用的復(fù)制算法來回收新生代。Hotspot虛擬機(jī)默認(rèn)eden和survivor的大小比例是8:1,也就是每次只有10%的內(nèi)存是“浪費(fèi)”的
在對象存活率高的時候,效率有所下降
如果不想浪費(fèi)50%的空間,就需要有額外的空間進(jìn)行分配擔(dān)保用于應(yīng)付半?yún)^(qū)內(nèi)存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。
只需要掃描存活的對象,效率更高
不會產(chǎn)生碎片
需要浪費(fèi)額外的內(nèi)存作為復(fù)制區(qū)
適合生命周期端的對象,因?yàn)槊看蜧C總能回收大部分的對象,復(fù)制的開銷比較小
標(biāo)記整理算法:
?沒有內(nèi)存碎片
?比標(biāo)記清除耗費(fèi)更多的時間
分代收集:
?一般是把Java對分為新生代和老年代,這樣就可以根據(jù)年齡特點(diǎn)采用最適當(dāng)?shù)氖占惴āP律鶪C有大批對象死去,只有少量存活,那就選用復(fù)制算法只需要付出少量存活對象的復(fù)制成本就可以完成收集。
老年代:
- ? 存放了經(jīng)過一次或多次GC還存活的對象
- ? 一般采用Mark-Sweep或者M(jìn)ark-Compact算法進(jìn)行GC
- ?有多種垃圾收集器可以選擇。每種垃圾收集器可以看做一個GC算法的具體實(shí)現(xiàn)。可以根據(jù)具體應(yīng)用的需求選擇合適的垃圾收集器(吞吐量?響應(yīng)時間?)
永久代:
?并不屬于對(Heap)但是GC也會涉及到這個區(qū)域
?存放了每個Class的機(jī)構(gòu)信息,包括常量池,字段描述,方法描述。與垃圾收集要收集的Java對象關(guān)系不大
內(nèi)存結(jié)構(gòu):
?備注: 在Hotspot中本地方法棧和JVM方法棧是同一個,因此也可以用-Xss控制
GC要做的是將那些dead的對象所占用的內(nèi)存回收掉
- ? ?HotSpot認(rèn)為沒有引用的對象是dead的
- ? HotSpot將引用分為四種: Strong、Soft、 Weak、 Phantom? ? ?
Strong即默認(rèn)通過Object o = new Object() 這種方式賦值的引用
Soft、Weak、Phantom這三種原則都是集成Reference
在Full GC時會對Reference類型的引用進(jìn)行特殊處理
? Soft: 內(nèi)存不夠時候,一定會被GC。長期不用也會被GC
? Weak: 一定會被GC,當(dāng)Mark為Dead,會在ReferenceQueue中通知
? Phantom: 本來就沒有引用,當(dāng)從JVM heap中釋放時會通知
GC的時機(jī):
?在分代模型的基礎(chǔ)上,GC從時機(jī)上分為兩種: Scavenge GC 和 Full GC
? ?1.Scanvenge GC(Minor GC) :
? ? ? ? ? ? 觸發(fā)時機(jī): 新對象生成時,Eden空間滿了
? ? ? ? ? ? 理論上Eden區(qū)大多數(shù)對象會在Scavenge GC回收,復(fù)制算法的執(zhí)行效率會很高,Scavenge GC時間比較短
? 2. Full GC(stop the world)
? ? ?對整個JVM進(jìn)行整理,包括Young、Old 和Perm
? ? 主要的觸發(fā)時機(jī): 1.Old滿了? 2.Perm滿了 3.System.gc()
? ? 效率很低,盡量減少Full GC
垃圾收集器的并發(fā)和并行:
?并行: 多個垃圾收集器的線程同時工作,但是用戶線程處于等待狀態(tài)
?并發(fā): 收集器在工作的同事,可以允許用戶線程工作。 并發(fā)不代表解決了GC停頓的問題,在關(guān)鍵的步驟還是要停頓。比如在垃圾收集器標(biāo)記垃圾的時候。但是在清除垃圾的時候,用戶線程可以和GC線程并發(fā)執(zhí)行。
Serial收集器:
?單線程收集器,收集時會暫停所有工作線程(Stop The World)使用復(fù)制收集算法,虛擬機(jī)運(yùn)行在Client模式時的默認(rèn)新生代收集器。
?最早的收集器,單線程進(jìn)行GC
?新生代和老年代都可以使用
?在新生代,采用復(fù)制算法;在老年代,采用標(biāo)記整理算法
?因?yàn)槭菃尉€程GC,沒有多線昵稱切換的額外開銷,簡單使用
?Hotpot Client模式缺省的收集器
?
?ParNew收集器
? ParNew收集器就是Serial的多線程版本,除了使用多個收集線程外,其余行為包括算法,STW,對象分配規(guī)則,回收策略等都與Serial收集器一模一樣
?對應(yīng)的這種收集器是JVM運(yùn)行在Server模式的默認(rèn)新生代收集器,在單CPU的環(huán)境中,ParNew收集器并不會比Serial收集器有更好的效果。只有在多CPU的環(huán)境下,效率才會比Serial收集器高
?使用復(fù)制算法(因?yàn)獒槍π律?
?通過-XX:ParallelGCThreads來控制GC線程數(shù)的多少。需要結(jié)合具體的CPU個數(shù)
?Server模式下新生代的缺省收集器
?Parallel Scavenge收集器
? Parallel Scavenge 收集器也是一個多線程的收集器,也是使用復(fù)制算法,但它的對象分配規(guī)則與回收策略都與ParNew收集器有所不同,它是以吞吐量最大化(即GC時間棧中運(yùn)行時間最小)為目標(biāo)的收集器實(shí)現(xiàn),它允許較長時間的STW換取總吞吐量最大化
Serial Old收集器
?Serial Old是單線程收集器,使用標(biāo)記整理算法,是老年代的收集器。
?老年代版本吞吐量優(yōu)先收集器,使用多線程和標(biāo)記整理,JVM1.6提供,在此以前,新生代使用了PS收集器的話,老年代除Serial Old別無選擇。因?yàn)镻S無法與CMS收集器配合工作。
Parallel Old
?Parallel Scavenge在老年代的實(shí)現(xiàn)
?在JVM1.6才出現(xiàn) Parallel Old
?采用對線程,Mark-Compact算法
?更注重吞吐量
?Parallel Scavenge + Prallel Old = 高吞吐量,但GC停頓可能不理想
?
CMS(Concurrent Mak Sweep)
?cms是一種最短停頓時間為目標(biāo)的收集器,使用CMS不能達(dá)到GC效率最高(總體GC時間最小),但它能盡可能降低GC時服務(wù)的停頓時間,CMS收集器使用的是標(biāo)記清除算法
? 追求最短停頓時間,非常適合Web應(yīng)用
? 只針對老年區(qū),一般結(jié)合ParNew使用
? Concurrent,GC線程和用戶線程并發(fā)工作(盡量并發(fā))
? Mark-Sweep
? 只有在多CPU環(huán)境下才有意義
? 使用-XX:+UseConcMarkSweepGC打開
?缺點(diǎn):
?以犧牲CPU資源的帶勁來減少用戶線程的停頓。當(dāng)CPU個數(shù)少于4到時候,有可能對吞吐量影響非常大
?CMS在并發(fā)清理的過程中,用戶線程還在跑。這是需要預(yù)留一部分空間給用戶線程
?CMS用Mark-Sweep,會帶來碎片問題。碎片過多時候會容易頻繁觸發(fā)Full GC。
GC樂基收集器的JVM參數(shù)定義
Java內(nèi)存泄露常見原因:
?1.對象定義在錯誤范圍(Wrong Scope)
?2.異常(Exception)處理不當(dāng)
?3.集合數(shù)據(jù)管理不當(dāng)
如果Foo實(shí)例對象的生命較長,會導(dǎo)致臨時性內(nèi)存泄露。(這里的names變量其實(shí)只有臨時作用)
class Foo{private String[] names;public void doIt(int length){if (names == null || names.length < length){names = new String[length];populate(names);print(names);}} }JVM喜歡生命周期短的兌現(xiàn),這樣做已經(jīng)足夠高效。 (成員變量變成局部變量)
class Foo{public void doIt(int length){String[] names = new String[length];populate(names);print(names);} }連接不釋放:finally
rs.close() // 必須釋放集合數(shù)據(jù)管理不當(dāng):
當(dāng)使用 Array-based的數(shù)據(jù)結(jié)構(gòu)(ArrayList,HashMap等),盡量減少resize
- 比如new ArrayList時,盡量估算size,在創(chuàng)建的時候把size確定
- 減少resize可以避免沒有必要的array copying, gc碎片等問題
如果一個List只需要順序訪問,不需要隨機(jī)訪問(Random Access),用LinkedList代替ArrayList
- LinkedList本質(zhì)是鏈表,不需要resize,但只適用于順序訪問
輸出JVM垃圾回收,詳盡的情況:-verbose:gc
?初始大小-Xms20M
?堆大小-Xmx20M
(相等,啟動時候不會出現(xiàn)都抖動問題。)
?新生代大小 -Xmn10m
?垃圾回收詳細(xì)信息:-XX:+PrintGCDetails?
?Edn:survor的比值 :?-XX:SurvivorRatio=8
public class t {public static void main(String[] args) {int size = 1024 * 1024;//原生的數(shù)組 里面都是0byte[] myAlloc1 = new byte[2 * size];byte[] myAlloc2 = new byte[2 * size];byte[] myAlloc3 = new byte[2 * size];System.out.println("hello world");} }看出沒有發(fā)生GC:
繼續(xù)添加 數(shù)組:
byte[] myAlloc3 = new byte[2 * size];發(fā)生了GC
?新生代的垃圾回收,怎么回收都不夠呀。除了程序的,JVM啟動時候也會有大量的對象
?Full GC 會導(dǎo)致Stop? the World 要避免之 Full GC后老年代有可能會變多哦
來個GC的
?PS: Parallel Scavenge收集器
9216K / 1024 = 9K?
這樣的新生代使用的是9k? 因?yàn)閒rom to? 有一個區(qū)域始終是閑置的
5646 - 624 = 5022K? 執(zhí)行完gc后,新生代釋放的空間容量(包括晉升到老年代的)
5646-4728 = 918? 執(zhí)行完gc后,總的堆空間釋放的容量?
5022 - 918 = 4104K? 老年代使用的容量?
當(dāng)新生代已經(jīng)容納下,待分配的對象時候,新創(chuàng)建的,直接誕生在老年代。
看下面的例子:
public class t {public static void main(String[] args) {int size = 1024 * 1024;//原生的數(shù)組 里面都是0byte[] myAlloc1 = new byte[2 * size];byte[] myAlloc2 = new byte[2 * size];byte[] myAlloc3 = new byte[2 * size];//這個數(shù)組直接在老年代分配!byte[] myAlloc4 = new byte[4 * size];System.out.println("hello world");} }沒有發(fā)生Full GC:
直接在老年代分配對象!
不指定時候,默認(rèn)使用:
PSYoungGen: Parallel Scavenge(新生代垃圾收集器)
ParOldGen: Parallel Old (老年代垃圾收集器)
命令行指令:
?java -XX:+PrintCommandLineFlags -version?
虛擬機(jī)參數(shù):?-XX:PretenureSizeThreshold=4194304 ?閾值? 必須配合另外一個參數(shù)!-XX:UseSerialGC 否則不起作用
當(dāng)新創(chuàng)建的對象大小超過這個,就直接誕生在老年代了
public class t {public static void main(String[] args) {int size = 1024 * 1024;//5M 超過閾值 byte[] myAlloc1 = new byte[5 * size];} }運(yùn)行結(jié)果:
?JVM:
-Xms20M
-Xmx20M
-Xmn10m
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
-XX:PretenureSizeThreshold=4194304
-XX:+UseSerialGC
結(jié)論:
?PretenureSizeThreadshold:設(shè)置超過多個時直接在老年代分配
?如果需要分配10M
public class t {public static void main(String[] args) {int size = 1024 * 1024;//10M byte[] myAlloc1 = new byte[10 * size];} }?經(jīng)過大量GC后,發(fā)現(xiàn)不能分配。放不下10M! 新生代+老年代一共才10M
設(shè)置閾值后,即便是新生代能容納,一樣會分配到老年代!
MaxTenuringThreshold作用: 在可以自動調(diào)節(jié)對象晉升(Promote)到老年代閾值的GC中,設(shè)置該閾值的最大值(再怎么自動調(diào)節(jié)也不會超過這個最大值)。?
--XX:MaxTenuringThreshold=5
改參數(shù)默認(rèn)15,CMS中默認(rèn)值為6,G1默認(rèn)為15(在JVM中,該數(shù)值是由4個bit來表示的,所以最大值111 即15)
-XX:+PrintTenuringDistribution 打印的效果,比如打印年齡為1的對象的情況等等
經(jīng)歷了多次GC后,存活的對象會在From? Survivor 和 To Survivor之間來回存放,而這里的一個前提是這兩個空間有足夠的空間來存放數(shù)據(jù),在GC算法中,會計算每個對象年齡的大小。如果達(dá)到了某個年齡后發(fā)現(xiàn)總大小已經(jīng)大于了Survivor空間的50%,那么這時就需要調(diào)調(diào)整閾值,不能在繼續(xù)等到默認(rèn)的15次GC后才完成晉升。
因?yàn)檫@樣會導(dǎo)致Survivor空間不足,所以需要調(diào)整閾值,讓這些存活對象盡快完成晉升。
看下面的例子:
-Xms20M
-Xmx20M
-Xmn10m
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=5
-XX:+PrintTenuringDistribution
-XX:+PrintCommandLineFlags
運(yùn)行結(jié)果:
?-XX:+PrintCommandLineFlags : 圖中箭頭的啟動參數(shù)信息
max 是設(shè)置的閾值? new Threshold 5 是自動調(diào)整的值
看下面的例子:
配置:
-verbose:gc
-Xmx20M
-Xmn50m
-XX:TargetSurvivorRatio=60
-XX:+PrintTenuringDistribution
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-XX:+UseConcMarkSweepGC
-XX:+UseParNewGC
-XX:MaxTenuringThreshold=3
解釋:
-verbose:gc
-Xmx20M
-Xmn50m
-XX:targetSurvivorRatio=60 #在某一個survivor空間已經(jīng)被存活的占據(jù)空間60,重新計算閾值。
-XX:+PrintTenuringDistribution #打印對象在survivor在對空間的年齡情況
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps? ?#打印當(dāng)前GC執(zhí)行的的時間戳
-XX:+UserConcMarkSweepGC? #指定垃圾收集器CMS(CMS是用在老年代的)
-XX:UserParNewGC? #新生代ParNew
-XX:MaxTenuringThreshold=3
public class t {public static void main(String[] args) throws InterruptedException {byte[] byte1 = new byte[512 * 1024];byte[] byte2 = new byte[512 * 1024];myGC();Thread.sleep(1000);System.out.println("111");myGC();Thread.sleep(1000);System.out.println("222");myGC();Thread.sleep(1000);System.out.println("333");myGC();Thread.sleep(1000);System.out.println("444");byte[] byte3 = new byte[512 * 1024];byte[] byte4 = new byte[512 * 1024];byte[] byte5 = new byte[512 * 1024];myGC();Thread.sleep(100);System.out.println("555");myGC();Thread.sleep(100);System.out.println("666");System.out.println("hello world");}private static void myGC() {for (int i = 0; i < 40; i++) {byte[] byteArray = new byte[2014 * 1024];}} }?多次垃圾回收情況
?CMS:
枚舉根節(jié)點(diǎn):
?當(dāng)執(zhí)行系統(tǒng)停頓下來后,并不需要一個不漏的檢查完所有執(zhí)行上下文和全局的引用位置,虛擬機(jī)應(yīng)當(dāng)是有辦法直接得知哪些地方存放著對象引用。在HotSpot的實(shí)現(xiàn)中,是使用一組成為OopMap的數(shù)據(jù)結(jié)構(gòu)來達(dá)到這個目的的。
安全點(diǎn):
?在OopMap的協(xié)助下,HotSpot可以快速且準(zhǔn)確的完成GC Root枚舉,但一個很現(xiàn)實(shí)的問題隨之而來:可能導(dǎo)致引用關(guān)系變化,或者說OopMap內(nèi)容變化的指令非常多,如果為每一條指令都生成對應(yīng)的OopMap,那將會需要大量的額外空間,這樣GC的空間成本將會變得更? ? ?高亮
?實(shí)際上,HotSpot并沒有為每條指令都生成OopMap,而只是在“特定的位置”記錄了這些信息,這些位置成為安全點(diǎn)(SafePoint),即程序執(zhí)行時并非在所有地方都停頓下來開始GC,只有在達(dá)到安全點(diǎn)時才能暫停。
?SafePoint 的選定既不能太少以至于讓GC等待時間太長,也不能過于頻繁以至于過飯增大運(yùn)行時的負(fù)載。所以,安全點(diǎn)的選定基本上是以“是否具有讓程序長時間執(zhí)行的特征”為標(biāo)準(zhǔn)進(jìn)行選定的。因?yàn)槊織l指令執(zhí)行的時間非常短暫,程序不太可能因?yàn)橹噶盍鏖L度太長而過? ?長的時間運(yùn)行,“長時間執(zhí)行”的最明顯特征就是指令序列復(fù)用,例如方法調(diào)用、循環(huán)跳轉(zhuǎn)、異常跳轉(zhuǎn)等。所以具有這些功能的指令才會產(chǎn)生SafePoint.
? 對于SafePoint,另一個需要考慮的問題時如何在GC發(fā)生時讓所有線程(這里不包括執(zhí)行JNI調(diào)用的線程)都“跑”到最近的安全點(diǎn)上再停頓下來:搶占式中斷(Preemptive Suspension)和主動式中斷(Voluntary Suspension)
- ? ? 搶占式中斷: 他不需線程的執(zhí)行代碼主動去配合,在GC發(fā)生時,首先把所有線程全部中斷,如果有線程中斷的地方不在安全點(diǎn)上,就恢復(fù)線程,讓它“跑”到安全點(diǎn)上。
- ? ? 主動式中斷: 當(dāng)GC需要中斷線程的時候,不直接對線程操作,僅僅簡單的設(shè)置一個標(biāo)志,各個線程執(zhí)行時主動去輪訓(xùn)這個標(biāo)志,發(fā)現(xiàn)中斷標(biāo)志為真時就自己中斷掛起。輪訓(xùn)標(biāo)志的地方和安全點(diǎn)是重合的,另外再加上創(chuàng)建對象需要分配內(nèi)存的地方
現(xiàn)在幾乎沒有虛擬機(jī)采用搶占式中斷來暫停線程從而響應(yīng)GC事件
安全區(qū)域:
?在使用SafePoint似乎已經(jīng)完美地解決了如何進(jìn)入GC的問題,但實(shí)際上情況卻不一定。SafePoint機(jī)制保證了程序執(zhí)行時候,在不太長時間內(nèi)就會遇到可進(jìn)入GC的SafePoint。但如果程序在“不執(zhí)行”的時候呢?所謂程序不執(zhí)行就是沒有分配CPU時間,典型的例子就是出于Sleep狀態(tài)或者Blocked狀態(tài),這時候線程無法響應(yīng)JVM的中斷請求,JVM也顯然不太可能等地線程重新分配CPU時間,對于這種情況,就需要安全區(qū)域(Safe Regin)來解決了。
?在線程執(zhí)行到Safe Regin中的代碼時候,首先標(biāo)識自己已經(jīng)進(jìn)入了Safe Regin,那樣,當(dāng)在這段時間里JVM要發(fā)起GC時,就不用管標(biāo)識自己為Safe Region狀態(tài)的線程線程了。在線程要離開Safe Region時,他要檢查系統(tǒng)是否已經(jīng)完成了根節(jié)點(diǎn)枚舉(或者是整個GC過程),如果完成了,那線程就繼續(xù)執(zhí)行,否則他就必須等待線程直到收到可以安全離開Safe Region的信號為止。
CMS收集器
?CMS收集器,以獲取最短回收停頓時間為目標(biāo),多數(shù)應(yīng)用于互聯(lián)網(wǎng)站或者B/S系統(tǒng)的服務(wù)器上
?CMS是基于“標(biāo)記-清除”算法實(shí)現(xiàn)的,整個工程分為四個步驟:
其中,初始標(biāo)記、重新標(biāo)記這兩個步驟仍然需要“Stop The World”
初始標(biāo)記只是標(biāo)記一下GC Roots能直接關(guān)聯(lián)到的對象,速度很快
并發(fā)標(biāo)記階段就是進(jìn)行GC Roots Tracing的過程?
重新標(biāo)記階段則是為了修正并發(fā)標(biāo)記期間因用戶程序繼續(xù)運(yùn)作而導(dǎo)致標(biāo)記產(chǎn)生變動的那一部分對象的標(biāo)記記錄,這個階段的停頓時間一般會比初始標(biāo)記階段稍微長一些,但遠(yuǎn)比并發(fā)標(biāo)記的時間短。
CMS收集器的運(yùn)作步驟如下圖所示,在整個過程中耗時最長的并發(fā)標(biāo)記和并發(fā)清除過程收集器線程都是可以與用戶線程一起工作,因此,從總體上看,CMS收集器的內(nèi)存回收過程是與用戶線程一起并發(fā)執(zhí)行的
?優(yōu)點(diǎn):
?并發(fā)收集、低停頓,Oracle公司的一些官方文檔中也稱之為并發(fā)低停頓收集器(Concurrent Low Pause Collector)
?缺點(diǎn):
- CMS收集器對CPU資源非常敏感
- CMS收集器無法處理浮動垃圾(FloatingGarbage),可能出現(xiàn)“Concurrent mode Failure”失敗而導(dǎo)致另一次Full GC的產(chǎn)生。如果在應(yīng)用中老年代增長不是太快,可以適當(dāng)調(diào)高參數(shù)-XX:CMSInitiatingOccupancyFraction的值來提高觸發(fā)百分比,以便降低內(nèi)存回收次數(shù)從而獲取更好的性能,要是CMS運(yùn)行期間預(yù)留的內(nèi)存無法滿足程序需要時,虛擬機(jī)將啟動后備預(yù)案:臨時啟用Serial Old收集器來重新進(jìn)行老年代的垃圾收集,這樣停頓時間就很長了。所有說參數(shù)-XX:CMSInitiatingOccupancyFraction設(shè)置的太高很容易導(dǎo)致大量“Concurrent Mode Failure”失敗,性能反而降低。
- 收集結(jié)束時候,會有大量空間碎片產(chǎn)生,空間碎片過多時,將會給大對象分配帶來很大麻煩,往往出現(xiàn)老年代還有很大空間剩余,但是無法找到足夠大的連續(xù)空間來支配當(dāng)前對象,不得不提前進(jìn)行一次Full GC。CMS收集器提供了一個 XX:+UseCMSCompactAtFullCollection開關(guān)參數(shù)(默認(rèn)就是開啟的),用于在CMS收集器頂不住時候要進(jìn)行Full GC時開啟內(nèi)存碎片的合并整理過程,內(nèi)存整理的過程是無法自拔的,空間碎片問題沒有了,但停頓時間不得不邊長。
? 空間分配擔(dān)保:
?在發(fā)生Minor GC之前,虛擬機(jī)會先檢查老年代最大可用的連續(xù)空間是否大于新生代所有對象總空間,如果這個條件成立,那么Minor GC可以確保是安全的。當(dāng)大量對象在MinorGC后仍存活,就需要老年代進(jìn)行空間分配擔(dān)保,把Survivor無法容納的對象直接進(jìn)入老年代。如果老年代判斷到剩余空間不足(根據(jù)以往每一次回收晉升到老年代對象容量的平均值作為經(jīng)驗(yàn)值),則進(jìn)行一次Full GC
CMS收集器收集步驟:
1. 是CMS兩次Stop The World 事件中的其中一次,這個階段的目標(biāo)是:標(biāo)記哪些直接被GC Root 引用或者被年輕代存活對象所引用的所有對象。
?2.在這個階段Garbage Collector會遍歷老年代,然后標(biāo)記所有存活的對象,它會根據(jù)上個階段找到的GC Root是遍歷查找。并發(fā)標(biāo)記階段,他會與用戶的應(yīng)用程序并發(fā)運(yùn)行。并不是老年代的所有的存活對象都會被標(biāo)記,因?yàn)樵跇?biāo)記期間用戶的程序可能會改變一些引用。
?在上面的圖中,與階段1的圖進(jìn)行對比,就會發(fā)現(xiàn)有一個對象的引用地址發(fā)生了變化。
3. 這是個并發(fā)階段,與應(yīng)用的線程并發(fā)運(yùn)行,并不會stop應(yīng)用的線程。在并發(fā)運(yùn)行的過程中,一些對象的引用可能會發(fā)生變化,但是這種情況發(fā)生時,JVM會將包含這個對象的區(qū)域(Card)標(biāo)記為Dirty,也就是Card Marking
? ? 在pre-clean階段,那些能夠從Dirty對象到達(dá)的對象也會被標(biāo)記,這個標(biāo)記做完之后,dirty card 標(biāo)記就會被清除了
4.?這也是一個并發(fā)階段,但是同樣不會影響影響用戶的應(yīng)用線程,這個階段是為了盡量承擔(dān) STW(stop-the-world)中最終標(biāo)記階段的工作。這個階段持續(xù)時間依賴于很多的因素,由于這個階段是在重復(fù)做很多相同的工作,直接滿足一些條件(比如:重復(fù)迭代的次數(shù)、? ? ? 完成的工作量或者時鐘時間等)
5.這是第二個 STW 階段,也是 CMS 中的最后一個,這個階段的目標(biāo)是標(biāo)記所有老年代所有的存活對象,由于之前的階段是并發(fā)執(zhí)行的,gc 線程可能跟不上應(yīng)用程序的變化,為了完成標(biāo)記老年代所有存活對象的目標(biāo),STW 就非常有必要了。
? 通常 CMS 的 Final Remark 階段會在年輕代盡可能干凈的時候運(yùn)行,目的是為了減少連續(xù) STW 發(fā)生的可能性(年輕代存活對象過多的話,也會導(dǎo)致老年代涉及的存活對象會很多)。這個階段會比前面的幾個階段更復(fù)雜一些。
經(jīng)歷著五個階段之后,老年代所有存活的對象都被標(biāo)記過來,現(xiàn)在可以通過清除算法去清除哪些老年代不再使用的對象。
6.這里不需要STW,它是與用戶的應(yīng)用程序并發(fā)運(yùn)行,這個階段是:清除哪些不再使用的對象,回收他們的占用空間為將來使用
?
?7. 這個階段也是并發(fā)執(zhí)行的,它會重設(shè)CMS內(nèi)部的數(shù)據(jù)機(jī)構(gòu),為下次的GC做準(zhǔn)備
總結(jié):
- CMS通過將大量工作分散到并發(fā)處理階段來減少STW時間,在這塊做的非常優(yōu)秀,但是CMS也有一些其他問題。
- CMS收集器無法處理浮動垃圾(Floating garbage)可能出現(xiàn)“Concurrent Mode Failure”失敗而導(dǎo)致另一次Full GC的產(chǎn)生,可能引發(fā)串行Full GC
- 空間碎片,導(dǎo)致無法分配大對象,CMS收集器提供了一個-XX:+UseCMSCompactAtFullCollection開發(fā)參數(shù)(默認(rèn)就是開啟的),用于在CMS收集器頂不住要進(jìn)行Full GC時開啟內(nèi)存碎片的合并整理過程,內(nèi)存整理的過程是無法并發(fā)的,空間碎片問題沒有了,但停頓時間不得不變長;
- 對于堆比較大的應(yīng)用,GC的時間難以估計。
看下面例子:
配置參數(shù):
-verbose:gc
-Xmx20M
-Xms20M
-Xmn10m
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
-XX:+UseConcMarkSweepGC
?默認(rèn)情況下,老年代的CMS 和新生代的 ParNew是成對出現(xiàn)的。
?可以看到CMS收集過程
吞吐量:
? 吞吐量關(guān)注的是,在一個指定的時間內(nèi),最大化一個應(yīng)用的工作量。?
? 如下方式來衡量一個系統(tǒng)吞吐量的好壞:
? ? 1.在一小時內(nèi)同一個事務(wù)(或者任務(wù)、請求)完成的次數(shù)(tps)。?
? ? 2.數(shù)據(jù)庫一小時可以完成多少次查詢;
? ? 3.對于關(guān)注吞吐量的系統(tǒng),卡頓是可以接受的,因?yàn)檫@個系統(tǒng)關(guān)注長時間的大量任務(wù)的執(zhí)行能力,單次快速的響應(yīng)并不值得考慮
響應(yīng)能力:
響應(yīng)能力指一個程序或者系統(tǒng)對請求的是否能夠及時響應(yīng)。?
比如:?
? ?一個桌面UI能多快的響應(yīng)一個事件;?
? ?一個網(wǎng)站能夠多快返回一個頁面請求;?
? ?數(shù)據(jù)庫能夠多快返回查詢的數(shù)據(jù);
? ?對于這類對響應(yīng)能力敏感的場景,長時間的停頓是無法接受的。
G1收集器是一個面向服務(wù)端的垃圾收集器,適用于多核處理器,大內(nèi)存容量的服務(wù)端系統(tǒng)
它滿足短時間GC停頓的同時達(dá)到一個較高的吞吐量
JDK7以上版本使用
G1收集器的設(shè)計目標(biāo)
- 與應(yīng)用線程同時工作,幾乎不需要stop-the-world(與CMS類似);
- 整理剩余空間,不產(chǎn)生內(nèi)存碎片;(CMS只能在full-GC時,用stop-the-world整理碎片內(nèi)存)
- GC停頓更加可控;
- 不犧牲系統(tǒng)的吞吐量;
- GC不要求額外的內(nèi)存空間(CMS需要預(yù)留空間存儲浮動垃圾);
G1的設(shè)計規(guī)劃是要替換掉CMS
- ?G1在某些方面彌補(bǔ)了CMS的不足,比如,CMS使用的是mark-sweep算法,自然會產(chǎn)生碎片;然而G1基于copying算法,高效的剩余內(nèi)存,而不需要管理內(nèi)存碎片。
- ?另外,G1提供了更多手段,以達(dá)到對gc停頓的可控。
?堆:
下面看看G1:
?G1收集器堆結(jié)構(gòu)
- ? heap被劃分為一個個相等的不連續(xù)的內(nèi)存區(qū)域(regions), 每個region都有一個分代的角色: eden、survivor、old
- ?對每個角色的數(shù)量并沒有強(qiáng)制限定,也就是說對每種分代內(nèi)存的大小,可以動態(tài)變化
- ?G1最大的特點(diǎn)就是高效的執(zhí)行回收,優(yōu)先去執(zhí)行哪些大量對象可回收的區(qū)域(region)
- ?G1使用了gc停頓可預(yù)測的模型,來滿足用戶設(shè)定的gc停頓時間,根據(jù)用戶設(shè)定的目標(biāo)時間,G1會自動的選擇哪些region需要清除,一次清除多少個region
- ?G1從多個region中復(fù)制存活的對象,然后集中放入一個region中,同時整理、清除內(nèi)存(copying收集算法)
G1 VS CMS
- ?對比使用mark-sweep的CMS,G1使用的copying算法不會造成內(nèi)存碎片
- ?對比Parallel Scavenge(基于copying)、parallel Old收集器(基于mark-compact-sweep), Parallel會對整個區(qū)域做整理導(dǎo)致gc停頓時間比較長,而G1只是特定的整理幾個region
- G1并非一個實(shí)時的收集器,與parallel Scavenge一樣,對gc停頓時間的設(shè)置并不絕對生效,只是G1有較高的幾率保證不超過設(shè)定的gc停頓時間。與之前的gc收集器對比,G1會根據(jù)用戶設(shè)定的gc停頓時間,智能評估哪幾region需要被回收可以滿足用戶的設(shè)定。
G1重要概念:
- ?分區(qū)(Region): G1采取了不同的策略來解決并行、串行和CMS收集器的碎片、暫停時間不可控等問題---G1將整個的堆分成相同大小的分區(qū)(region)每個分區(qū)都可能是年輕代也可能是老年代,但是在同一時刻只能屬于某個代。
- 年輕代、幸存區(qū)、老年代這些概念還存在,成為邏輯上的概念,這樣方便復(fù)用之前分代框架的邏輯。
- 在物理上不需要連續(xù),則帶來了額外的好處——有的分區(qū)內(nèi)垃圾對象特別多,有的分區(qū)內(nèi)垃圾對象很少,G1會優(yōu)先回收垃圾對象特別多的分區(qū),這樣可以花費(fèi)較少的時間來回收這些分區(qū)的垃圾,這也就是G1名字的由來,即首先收集垃圾最多的分區(qū)。
- 新生代其實(shí)并不是適用于這種算法的,依然是在新生代滿了的時候,對整個新生代進(jìn)行回收—— 整個新生代中的對象,要么被回收、要么晉升到老年代,至于新生代也采取分區(qū)機(jī)制的原因,則是因?yàn)檫@樣跟老年代的策略統(tǒng)一,方便調(diào)整代的大小。
- G1還是一種帶壓縮的收集器,在回收老年代的分區(qū)時,是將存活的對象從一個分區(qū)拷貝到另一個可用分區(qū),這個拷貝的過程就實(shí)現(xiàn)了局部的壓縮。每個分區(qū)的大小從1M到32M不等,但是都是2的冥次方。
-
收集集合(CSet):一組可被回收的分區(qū)的集合。在CSet中存活的數(shù)據(jù)會在GC過程中被移動到另一個可用分區(qū),CSet中的分區(qū)可以來自Eden空間、survivor空間、或者老年代。CSet會占用不到整個堆空間的1%大小。
-
已記憶集合(RSet)每個Region都有一個關(guān)聯(lián)的Remembered Set。RSet記錄了其他Region中的對象引用本Region中對象的關(guān)系,屬于points-into結(jié)構(gòu)(誰引用了我的對象)。RSet的價值在于使得垃圾收集器不需要掃描整個堆找到誰引用了當(dāng)前分區(qū)中的對象,只需要掃描RSet即可。
如下圖所示,Region1和Region3中的對象都引用了Region2中的對象,因此在Region2的RSet中記錄了這兩個引用。
?
- ?G1 GC則是在points-out的card table之上再加了一層結(jié)構(gòu)來構(gòu)成points-into RSet:每個region會記錄下到底哪些別的region有指向自己的指針,而這些指針分別在哪些card的范圍內(nèi)。
- ?這個RSet其實(shí)是一個hash table,key是別的region的起始地址,value是一個集合,里面的元素是card table的index。 舉例來說,如果region A的RSet里有一項的key是region B,value里有index為1234的card,它的意思就是region B的一個card里有引用指向region A。所以對region A來說,該RSet記錄的是points-into的關(guān)系;而card table仍然記錄了points-out的關(guān)系。
-
?Snapshot-At-The-Beginning(SATB):SATB是維持并發(fā)GC的正確性的一個手段,G1 GC的并發(fā)理論基礎(chǔ)就是SATB。SATB是由Taiichi Yuasa為增量式標(biāo)記清除垃圾收集器設(shè)計的一個標(biāo)記算法。
- 并發(fā)標(biāo)記是并發(fā)多線程的,但并發(fā)線程在同一時刻只掃描一個分區(qū)。
G1相對于GMS的優(yōu)勢:
1、G1在壓縮空間方面有優(yōu)勢
2、G1通過將內(nèi)存空間分成區(qū)域(Region)的方式避免內(nèi)存碎片問題
3、Eden, Survivor, Old區(qū)不再固定、在內(nèi)存使用效率上來說更靈活
4、G1可以通過設(shè)置預(yù)期停頓時間(Pause Time)來控制垃圾收集時間避免應(yīng)用雪崩現(xiàn)象(預(yù)測模型,統(tǒng)計數(shù)據(jù)分析)
5、G1在回收內(nèi)存后會馬上同時做合并空閑內(nèi)存的工作、而CMS默認(rèn)是在STW(stop the world)的時候做
6、G1會在Young GC中使用、而CMS只能在O區(qū)使用
重要概念:
- ?每個分區(qū)都可能是年輕代也可能是老年代,但是在同一時刻只能屬于某個代。年輕代,幸存區(qū),老年代這些概念還存在,成為邏輯上的概念,這樣方便復(fù)用之前分代框架的邏輯。
- ?在物理上不需要連續(xù),則帶來了額外的好處---有的分區(qū)內(nèi)垃圾對象特別多,有的分區(qū)內(nèi)垃圾對象很少,G1會優(yōu)先回收垃圾對象特別多的分區(qū),這樣可以花費(fèi)比較少的時間來回收這些分區(qū)的垃圾,這也是G1名字的由來,即首先收集垃圾最多的分區(qū)。
- ?依然是在新生代滿了的時候,對整個新生代進(jìn)行回收---整個新生代中的對象,要么被回收,要么晉升,至于新生代也采取分區(qū)機(jī)制的原因,則是因?yàn)檫@樣老年代的策略統(tǒng)一,方便調(diào)整代的大小。
G1的適合場景
- 服務(wù)端多核CPU、JVM內(nèi)存占用較大的應(yīng)用(至少大于4G)
- 應(yīng)用在運(yùn)行過程中會產(chǎn)生大量內(nèi)存碎片、需要經(jīng)常壓縮空間
- 想要更可控、可預(yù)期的GC停頓周期;防止高并發(fā)下應(yīng)用雪崩現(xiàn)象
G1 GC模式:
- G1提供了兩種GC模式,Young GC和Mixed GC,兩種都是Stop The World(STW)的
- ?Young GC: 選定所有年輕代里的Region。通過控制年輕代的Region個數(shù),即年輕代內(nèi)存大小,來控制Young GC的時間開銷
- ?Mixed GC: 選定所有年輕代里的Region,外加根據(jù)global concurrent marking統(tǒng)計得出收集收益高的若干老年Region(垃圾對象多的老年代Region).在用戶指定的開銷目標(biāo)范圍內(nèi)盡可能選擇收益高的老年代Region
- ?Mixed GC不是Full GC,它只能回收部分老年代的Region,如果Mixed GC實(shí)在無法跟上程序分配內(nèi)存的速度,導(dǎo)致老年代填滿無法繼續(xù)進(jìn)行Mixed GC,就會使用serial old GC(Full GC)來收集整個GC heap。所以本質(zhì)上,G1是不提供Full GC的
- global concurrent marking,它的執(zhí)行過程類似CMS,但是不同的是,在G1 GC中,它主要是為Mixed GC提供標(biāo)記服務(wù)的,并不是一次GC過程的一個必須環(huán)節(jié)。
global concurrent marking的執(zhí)行過程分為四個步驟:
初始標(biāo)記(initial mark,STW)。它標(biāo)記了從GC Root開始直接可達(dá)的對象。
并發(fā)標(biāo)記(Concurrent Marking)。這個階段從GC Root開始對heap中的對象標(biāo)記,標(biāo)記線程與應(yīng)用程序線程并行執(zhí)行,并且收集各個Region的存活對象信息。
重新標(biāo)記(Remark,STW)。標(biāo)記那些在并發(fā)標(biāo)記階段發(fā)生變化的對象,將被回收。
清除垃圾(Cleanup)。清除空Region(沒有存活對象的),加入到free list。
第一階段initial mark是共用了Young GC的暫停,這是因?yàn)樗麄兛梢詮?fù)用root scan操作,所以可以說global concurrent marking是伴隨Young GC而發(fā)生的。
第四階段Cleanup只是回收了沒有存活對象的Region,所以它并不需要STW。
G1在運(yùn)行過程中的主要模式:
1. YGC(不同于CMS)
2. 并發(fā)階段
3. 混合模式
4. Full GC(一般是G1出現(xiàn)問題時發(fā)生)
注:
在Eden充滿時觸發(fā),在回收之后所有之前屬于Eden的區(qū)塊全變成空白。然后把剩余的存活對象移動到S區(qū)。
什么時候觸發(fā)Mixed GC?
由一些參數(shù)控制,另外也控制著哪些老年代Region會被選入CSet(收集集合)
- G1HeapWastePercent:在global concurrent marking結(jié)束之后,我們可以知道old gen regions中有多少空間要被回收,在每次YGC之后和再次發(fā)生Mixed GC之前,會檢查垃圾占比是否達(dá)到此參數(shù),只有達(dá)到了,下次才會發(fā)生Mixed GC。
- G1MixedGCLiveThresholdPercent:old generation region中的存活對象的占比,只有在此參數(shù)之下,才會被選入CSet。
- G1MixedGCCountTarget:一次global concurrent marking之后,最多執(zhí)行Mixed GC的次數(shù)。
- G1OldCSetRegionThresholdPercent:一次Mixed GC中能被選入CSet的最多old generation region數(shù)量
除了以上的參數(shù),G1 GC相關(guān)的其他主要的參數(shù)有:
| -XX:G1HeapRegionSize=n | 設(shè)置Region大小,并非最終值 |
| -XX:MaxGCPauseMillis | 設(shè)置G1收集過程目標(biāo)時間,默認(rèn)值200ms,不是硬性條件 |
| -XX:G1NewSizePercent | 新生代最小值,默認(rèn)值5% |
| -XX:G1MaxNewSizePercent | 新生代最大值,默認(rèn)值60% |
| -XX:ParallelGCThreads | STW期間,并行GC線程數(shù) |
| -XX:ConcGCThreads=n | 并發(fā)標(biāo)記階段,并行執(zhí)行的線程數(shù) |
| -XX:InitiatingHeapOccupancyPercent | 設(shè)置觸發(fā)標(biāo)記周期的 Java 堆占用率閾值。默認(rèn)值是45%。這里的java堆占比指的是non_young_capacity_bytes,包括old+humongous |
G1收集概覽:
- G1算法將堆劃分為若干區(qū)域(Region),它仍然屬于分代收集器。不過,這些區(qū)域的一部分包含新生代,新生代的垃圾收集依然采用暫停所有應(yīng)用線程的方式,將存活對象拷貝到老年代或者Survivor空間。老年代也分成很多區(qū)域,G1收集器通過將對象從一個區(qū)域復(fù)制到另一個區(qū)域,完成了清理工作。這就意味著,在正常的處理 過程中,G1完成了堆的壓縮(至少是部分堆壓縮),這樣就不會有CMS內(nèi)存碎片問題的存在。
- ?在G1中,還有一種特殊的區(qū)域,叫Humongous區(qū)域。 如果一個對象占用的空間超過了分區(qū)容量50%以上,G1收集器就認(rèn)為這是一個巨型對象。這些巨型對象,默認(rèn)直接會被分配在年老代,但是如果它是一個短期存在的巨型對象,就會對垃圾收集器造成負(fù)面影響。為了解決這個問題,G1劃分了一個Humongous區(qū),它用來專門存放巨型對象。如果一個H區(qū)裝不下一個巨型對象,那么G1會尋找連續(xù)的H分區(qū)來存儲。為了能找到連續(xù)的H區(qū),有時候不得不啟動Full GC
- Young GC主要是對Eden區(qū)進(jìn)行GC,它在Eden空間耗盡時會被觸發(fā)。在這種情況下,Eden空間的數(shù)據(jù)移動到Survivor空間中,如果Survivor空間不夠,Eden空間的部分?jǐn)?shù)據(jù)會直接晉升到年老代空間。Survivor區(qū)的數(shù)據(jù)移動到新的Survivor區(qū)中,也有部分?jǐn)?shù)據(jù)晉升到老年代空間中。最終Eden空間的數(shù)據(jù)為空,GC停止工作,應(yīng)用線程繼續(xù)執(zhí)行。
- 如果僅僅GC 新生代對象,我們?nèi)绾握业剿械母鶎ο竽?#xff1f; 老年代的所有對象都是根么?那這樣掃描下來會耗費(fèi)大量的時間。于是,G1引進(jìn)了RSet的概念。它的全稱是Remembered Set,作用是跟蹤指向某個heap區(qū)內(nèi)的對象引用。
??
- 在CMS中,也有RSet的概念,在老年代中有一塊區(qū)域用來記錄指向新生代的引用。這是一種point-out,在進(jìn)行Young GC時,掃描根時,僅僅需要掃描這一塊區(qū)域,而不需要掃描整個老年代。
-
但在G1中,并沒有使用point-out,這是由于一個分區(qū)太小,分區(qū)數(shù)量太多,如果是用point-out的話,會造成大量的掃描浪費(fèi),有些根本不需要GC的分區(qū)引用也掃描了。
-
于是G1中使用point-in來解決。point-in的意思是哪些分區(qū)引用了當(dāng)前分區(qū)中的對象。這樣,僅僅將這些對象當(dāng)做根來掃描就避免了無效的掃描。
-
由于新生代有多個,那么我們需要在新生代之間記錄引用嗎?這是不必要的,原因在于每次GC時,所有新生代都會被掃描,所以只需要記錄老年代到新生代之間的引用即可。
- 需要注意的是,如果引用的對象很多,賦值器需要對每個引用做處理,賦值器開銷會很大,為了解決賦值器開銷這個問題,在G1 中又引入了另外一個概念,卡表(Card Table)。一個Card Table將一個分區(qū)在邏輯上劃分為固定大小的連續(xù)區(qū)域,每個區(qū)域稱之為卡。卡通常較小,介于128到512字節(jié)之間。Card Table通常為字節(jié)數(shù)組,由Card的索引(即數(shù)組下標(biāo))來標(biāo)識每個分區(qū)的空間地址
Young GC 階段:
階段1:根掃描
靜態(tài)和本地對象被掃描
階段2:更新RS
處理dirty card隊列更新RS
階段3:處理RS
檢測從年輕代指向年老代的對象
階段4:對象拷貝
拷貝存活的對象到survivor/old區(qū)域
階段5:處理引用隊列
軟引用,弱引用,虛引用處理關(guān)于G1 Mix GC:?Mix GC不僅進(jìn)行正常的新生代垃圾收集,同時也回收部分后臺掃描線程標(biāo)記的老年代分區(qū)。
它的GC步驟分2步:
- 全局并發(fā)標(biāo)記(global concurrent marking)
- 拷貝存活對象(evacuation)
提到并發(fā)標(biāo)記,我們不得不了解并發(fā)標(biāo)記的三色標(biāo)記算法。它是描述追蹤式回收器的一種有用的方法,利用它可以推演回收器的正確性。 首先,我們將對象分成三種類型的。
- 黑色:根對象,或者該對象與它的子對象都被掃描
- 灰色:對象本身被掃描,但還沒掃描完該對象中的子對象
- 白色:未被掃描對象,掃描完成所有對象之后,最終為白色的為不可達(dá)對象,即垃圾對象
根對象被置為黑色,子對象被置為灰色。
繼續(xù)由灰色遍歷,將已掃描了子對象的對象置為黑色。
遍歷了所有可達(dá)的對象后,所有可達(dá)的對象都變成了黑色。不可達(dá)的對象即為白色,需要被清理。
這看起來很美好,但是如果在標(biāo)記過程中,應(yīng)用程序也在運(yùn)行,那么對象的指針就有可能改變。這樣的話,我們就會遇到一個問題:對象丟失問題。
我們看下面一種情況,當(dāng)垃圾收集器掃描到下面情況時:
這時候應(yīng)用程序執(zhí)行了以下操作:
A.c=C
B.c=null
這樣,對象的狀態(tài)圖變成如下情形:
這時候垃圾收集器再標(biāo)記掃描的時候就會下圖成這樣:
引申SATB
在G1中,使用的是STAB(snapshot-at-the-beginning)的方式,刪除的時候記錄所有的對象,它有3個步驟:
1,在開始標(biāo)記的時候生成一個快照圖標(biāo)記存活對象
2,在并發(fā)標(biāo)記的時候所有被改變的對象入隊(在write barrier里把所有舊的引用所指向的對象都變成非白的)
3,可能存在游離的垃圾,將在下次被收集
G1混合式回收
?這樣,G1到現(xiàn)在可以知道哪些老的分區(qū)可回收垃圾最多。 當(dāng)全局并發(fā)標(biāo)記完成后,在某個時刻,就開始了Mix GC。這些垃圾回收被稱作“混合式”是因?yàn)樗麄儾粌H僅進(jìn)行正常的新生代垃圾收集,同時也回收部分后臺掃描線程標(biāo)記的分區(qū)。
?混合式GC也是采用的復(fù)制的清理策略,當(dāng)GC完成后,會重新釋放空間。
G1分代算法:
為老年代設(shè)置分區(qū)的目的是老年代里有的分區(qū)垃圾多,有的分區(qū)垃圾少,這樣在回收的時候可以專注于收集垃圾多的分區(qū),這也是G1名稱的由來。
不過這個算法并不適合新生代垃圾收集,因?yàn)樾律睦占惴ㄊ菑?fù)制算法,但是新生代也使用了分區(qū)機(jī)制主要是因?yàn)楸阌诖笮〉恼{(diào)整。
SATB:
SATB的全稱是Snapchat-At-The_Beginning。SATB是維持并發(fā)GC的一種手段。G1并發(fā)的基礎(chǔ)就是SATB。SATB可以理解成在GC開始之前對堆內(nèi)存里的對象做一次快照,此時活的對象就認(rèn)為是活的,從而形成一個對象圖。
在GC收集的時候,新生代的對象也認(rèn)為是活的對象,除此之外其他不可達(dá)的對象都認(rèn)為是垃圾對象。除此之外其他不可達(dá)的對象都認(rèn)為是垃圾對象。
如何找到在GC的過程中分配的對象呢(對象引用的變更,新對象的生成)?
每個region記錄著兩個top-at-mark-start(TAMS)指針,分別為prevTAMS和nextTAMS。在TAMS以上的對象就是新分配的,因而被視為隱式marked。
通過這種方式我們就找到了在GC過程中新分配的對象,并把這些對象認(rèn)為是活的對象。
解決了對象在GC過程中分配的問題,那么在GC過程中引用發(fā)生變化的問題怎么解決呢,
G1給出的解決辦法是通過Write Barrier。Write Barrier就是對引用字段進(jìn)行賦值做了環(huán)切。通過Write Barrier就可以了解到哪些引用對象發(fā)生了什么樣的變化。
SATB全稱為Snapshot At The Beginning,其要點(diǎn)如下:
- mark的過程就是遍歷heap標(biāo)記live object的過程,采用的是三色標(biāo)記算法,這三種顏色為white(表示還未訪問到)、gray(訪問到但是它用到的引用還沒有完全掃描)、back(訪問到而且其用到的引用已經(jīng)完全掃描完)。
- 整個三色標(biāo)記算法就是從GC roots出發(fā)遍歷heap,針對可達(dá)對象先標(biāo)記white為gray,然后再標(biāo)記gray為black;遍歷完成之后所有可達(dá)對象都是balck的,所有white都是可以回收的。
- SATB僅僅對于在marking開始階段進(jìn)行“snapshot”(marked all reachable at mark start),但是concurrent的時候并發(fā)修改可能造成對象漏標(biāo)記。
- 對black新引用了一個white對象,然后又從gray對象中刪除了對該white對象的引用,這樣會造成了該white對象漏標(biāo)記。
- 對black新引用了一個white對象,然后從gray對象刪了一個引用該white對象的white對象,這樣也會造成了該white對象漏標(biāo)記。
- 對black新引用了一個剛new出來的white對象,沒有其他gray對象引用該white對象,這樣也會造成了該white對象漏標(biāo)記。
其實(shí)誤標(biāo)沒什么關(guān)系,頂多造成浮動垃圾,在下次gc還是可以回收的,但是漏標(biāo)的后果是致命的,把本應(yīng)該存活的對象給回收了,從而影響的程序的正確性。
漏標(biāo)的情況只會發(fā)生在白色對象中,且滿足以下任意一個條件:
對于第一種情況,利用post-write barrier,記錄所有新增的引用關(guān)系,然后根據(jù)這些引用關(guān)系為根重新掃描一遍
對于第二種情況,利用pre-write barrier,將所有即將被刪除的引用關(guān)系的舊引用記錄下來,最后以這些舊引用為根重新掃描一遍
停頓預(yù)測模型 G1收集器突出表現(xiàn)出來的一點(diǎn)是通過一個停頓預(yù)測模型來根據(jù)用戶配置的停頓時間來選擇CSet的大小,從而達(dá)到用戶期待的應(yīng)用程序暫停時間。通過-XX:MaxGCPauseMillis參數(shù)來設(shè)置。這一點(diǎn)有點(diǎn)類似于ParallelScavenge收集器。關(guān)于停頓時間的設(shè)置并不是越短越好。設(shè)置的時間越短意味著每次收集的CSet越小,導(dǎo)致垃圾逐步積累變多,最終不得不退化成Serial GC;停頓時間設(shè)置的過長,那么會導(dǎo)致每次都會產(chǎn)生長時間的停頓,影響了程序?qū)ν獾捻憫?yīng)時間。 通過-XX:MaxGCPauseMillis參數(shù)來設(shè)置。這一點(diǎn)有點(diǎn)類似于ParallelScavenge收集器。關(guān)于停頓時間的設(shè)置并不是越短越好。設(shè)置的時間越短意味著每次收集的CSet越小,導(dǎo)致垃圾逐步積累變多,最終不得不退化成Serial GC;停頓時間設(shè)置的過長,那么會導(dǎo)致每次都會產(chǎn)生長時間的停頓,影響了程序?qū)ν獾捻憫?yīng)時間。G1的收集模式:
YoungGC:收集年輕代里的Region
MixGC:年輕代的所有Region+全局并發(fā)標(biāo)記階段選出的收益高的Region
無論是YoungGC還是MixGC都只是并發(fā)拷貝的階段
分代G1模式下選擇CSet有兩種子模式,分別對應(yīng)YoungGC和mixedGC:
YoungGC:CSet就是所有年輕代里面的Region
MixedGC:CSet是所有年輕代里的Region加上在全局并發(fā)標(biāo)記階段標(biāo)記出來的收益高的Region
G1的運(yùn)行過程是這樣的,會在Young GC和Mix GC之間不斷的切換運(yùn)行,同時定期的做全局并發(fā)標(biāo)記,在實(shí)在趕不上回收速度的情況下使用Full GC(Serial GC)。
?初始標(biāo)記是搭在YoungGC上執(zhí)行的,在進(jìn)行全局并發(fā)標(biāo)記的時候不會做Mix GC,在做Mix GC的時候也不會啟動初始標(biāo)記階段。當(dāng)MixGC趕不上對象產(chǎn)生的速度的時候就退化成Full GC,這一點(diǎn)是需要重點(diǎn)調(diào)優(yōu)的地方。
G1調(diào)優(yōu)實(shí)戰(zhàn):
1.不斷調(diào)優(yōu)暫停時間指標(biāo):
?通過XX:MaxGCPauseMillis=x可以設(shè)置啟動應(yīng)用程序暫停的時間,G1在運(yùn)行的時候會根據(jù)這個參數(shù)選擇CSet來滿足響應(yīng)時間的設(shè)置。一般情況下這個值設(shè)置到100ms或者200ms都是可以的(不同情況下會不一樣),但如果設(shè)置成50ms就不太合理。暫停時間設(shè)置的太短,就會導(dǎo)致出現(xiàn)G1跟不上垃圾產(chǎn)生的速度。最終退化成Full GC。所以對這個參數(shù)的調(diào)優(yōu)是一個持續(xù)的過程,逐步調(diào)整到最佳狀態(tài)。
2.不要設(shè)置新生代和老年代的大小
?G1收集器在運(yùn)行的時候會調(diào)整新生代和老年代的大小。通過改變代的大小來調(diào)整對象晉升的速度以及晉升年齡,從而達(dá)到我們?yōu)槭占髟O(shè)置的暫停時間目標(biāo)。
?設(shè)置了新生代大小相當(dāng)于放棄了G1為我們做的自動調(diào)優(yōu)。我們需要做的只是設(shè)置整個堆內(nèi)存的大小,剩下的交給G1自己去分配各個代的大小。
3. 關(guān)注Evacuation Failure
Evacuation Failure類似于CMS里面的晉升失敗,堆空間的垃圾太多導(dǎo)致無法完成Region之間的拷貝,于是不得不退化成Full GC來做一次全局范圍內(nèi)的垃圾收集 。
-XX:MaxGCPauseMillis=200m? ##最大GC停頓時間是200m
總結(jié)
以上是生活随笔為你收集整理的深入理解JVM虚拟机(总结篇)的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: SpringMvc项目加载顺序及上下文小
- 下一篇: 通信工程师考试基本情况及经验分享