JVM平台上還可以運行其他語言,運行的是Class字節碼。只要能翻譯成Class的語言就OK了。挺強大的。
- JVM廠商很多
- 垃圾收集器、收集算法
- JVM檢測工具
關於類的加載:
- Java代碼中,類型(interface, class,enum等,有些是在運行時候生成的,比如動態代理)的加載、連接與初始化過程都是在程序運行期間完成的。不涉及到對象的概念。同時也是個Runtime階段。
- 提供了更大的靈活性,增加了更多的可能性。提供了一些擴展,靈活擴展。
Java虛擬機與程序的生命周期:
在如下幾種情況下,Java虛擬機將會結束生命周期:
- 執行了System.exit()方法
- 程序正常執行結束
- 程序執行過程遇到了異常或者錯誤異常終止了
- 操作系統出現錯誤導致Java虛擬機進行終止
類的加載、連接與初始化:
加載:查找並加載類的二進制數據
連接:
- 驗證: 確保被加載類的正確性。Class有格式的。
- 准備:為類的靜態變量分配內存,並將其初始化為默認值
-
注:
1.類的靜態變量或類的靜態方法,通常可以看做全局的,由類去直接調用。此時還是個類的概念,不存在對象。
2.關於默認值問題:
class Test{
public static int a = 1;
}
中間過程: Test類加載到內存的過程中,會給a分配一個內存。然后將a初始化為默認值0(整型變量) - 解析: 把類中的符號引用轉為直接引用。符號的引用也是間接的引用方式。
初始化: 為類的靜態變量賦予正確的初始值
-
class Test{ public static int a = 1; } 此時的a才真正成為1了
類的使用與卸載
使用: 類的方法變量使用等
卸載: class字節碼文件,加載到內存里面。形成了自己的數據結構,駐留在內存里面。可以銷毀掉。卸載到了就不能進行new 對象了。
總體流程:
Java程序對類的使用方式分為兩種:
- 主動使用
- 被動使用
所有的Java虛擬機實現必須在每個類或接口被Java程序“首次主動使用”時才初始化他們。即初始化只會執行一次。
主動使用,七種(非精確划分,大體划分):
- 創建類的實例。
- 訪問某個類或接口的靜態變量,或者對靜態變量賦值。 字節碼層面上,使用的助記符:get static、 put static
- 調用類的靜態方法。 invoke static
- 反射(如Class.forName("com.test.t1"))
- 初始化一個類的子類
比如: class Parent{} class Child extends Parent{} 初始化Child時候,先去初始化Parent
- Java虛擬機啟動時被表明為啟動類的類(Java Test)
Java虛擬機啟動時候,被標明為啟動的類,即為有main方法的類,也會主動使用
- JDK1.7開始提供動態語言支持:
注:
1.java.lang.invoke.MethodHandle實例的解析結果REF_getStatic, REF_putStatic, REF_invokeStatic句柄對應的類沒有初始化,則初始化
2.1.7開始提供了對動態語言的支持。特別的JVM平台上通過腳本引擎調用JS代碼(動態語言)。
注:助記符了解即可
除了以上七種情況,其他使用Java類的方式都被看做是對類的被動使用,都不會導致類的初始化。
類的加載:
類的加載指的是將類 .class文件中的二進制數據讀入內存中,將其放在運行時數據區的方法區內,然后在內存中創建一個java.lang.Class對象(規范並說明Class對象位於哪里,HotSpot虛擬機將其放在了方法區中,JVM沒有規范這個)用來封裝類在方法區內的數據結構。
引申:一個類不管生成了多少實例,所有的實例對應只有一份Class對象。 Class對象是面鏡子,能反映到方法區中的Class文件的內容、結構等各種信息。
加載.class文件的方式:
- 從本地系統中直接加載
- 通過網絡下載
- 從zip、jar等貴方文件中加載
- 從專有數據庫中提取
- 將Java源文件動態編譯為.class文件
public class MyTest1 { public static void main(String[] args) { System.out.println(MyChild1.str1); // System.out.println(MyChild1.str2); } } class MyParent1{ //靜態成員變量 public static String str1 = "str1"; // 靜態代碼塊(程序加載初始化時候去執行) static { System.out.println("MyParent1 -----> static block running"); } } class MyChild1 extends MyParent1{ //靜態成員變量 public static String str2 = "str2"; static { System.out.println("MyChild1 -----> static block running"); } }
str1 子類調用了繼承到的父類的str1,子類的靜態代碼塊沒有執行。str1是父類中定義的。MyParent1的主動使用,但是沒有主動使用MyChild1. 總結:看定義的!
str2 可以執行,同時初始化子類時候,父類會主動使用。所有的父類都會被初始化!
MyTest1是一個啟動類,主動使用。先加載之。
總結:
- 對於靜態字段來說,只有直接定義了該字段的類才會被初始化。
- 當一個類在初始化時候,要求其父類全部已經初始化完畢。每個父類最多只能初始化一次!
引申: -XX:+TraceClassLoading,用於追蹤類的加載信息並打印出來。可以看到類的加載情況。
打印: 虛擬機在當前啟動情況下所加載的類的信息。
總結設置方式:
所有JVM參數都是: -XX: 開頭
類似於Boolean類型的開關:
-XX:+<option> 表示開啟option選項
-XX: - <option> 表示關閉option選項
賦值:
-XX:<option>=<value>, 表示將option選項的值設置為value
關於常量:
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"); } }
在編譯階段這個常量被存入到 調用這個常量的方法所在的類的常量池中。
本例中:
“hello world”是一個常量,會放置到MyTest2類的常量池中。
這里指的時將常量存放到了MyTest2的常量池匯總,之后MyTest2與MyParent2就沒有任何關系了
甚至,極端一些。我們可以將MyParent3的class文件刪除。(編譯完畢后,把class字節碼刪除)
總結:
- 常量編譯階段會存入到調用這個常量的方法所在的類的常量池中。
- 本質上,調用類並沒有直接引用到定義常量的類,因此並不會觸發定義常量類的初始化。
引申反編譯: javap -c 類的全路徑名字
助記符引申:
- ldc表示將int,float 或 String類型的常量值從常量池中推送至棧頂。
- bipush表示將單字節(-128 ~ 127)的常量值推送至棧頂
- sipush表示將一個短整型常量值(-32768 ~ 32767)推送至棧頂
- iconst_1 表示將int類型的1推送至棧頂 (iconst_1 ~ iconst_5)
助記符是在rt.jar中相關類去實現的。
如果常量的值,在編譯器不能確定下來呢?
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類的常量池中沒有意義的。
總結:
當一個常量值並非編譯期間可以確定的,那么其值就不會被放到調用類的常量池中。這時在程序運行時,會導致主動使用這個常量所在的類,顯然會導致這個類被初始化。
new對象實例情況:
public class MyTest4 { public static void main(String[] args) { MyParent4 myParent4 = new MyParent4(); } } class MyParent4{ static { System.out.println("MyParent4 --> run"); } }
對這個類的主動使用。
如果多次new,只會初始化一次。首次主動使用。
數組情況:
public class MyTest4 { public static void main(String[] args) { MyParent4[] myParent4s = new MyParent4[1]; } } class MyParent4{ static { System.out.println("MyParent4 --> run"); } }
不在七種情況范圍內。不會初始化!
不是MyParent4的實例!
到底創建的什么實例?getClass!,數組的實例到底是個啥玩意兒?
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虛擬機在運行期,創建出來的類型。是個數組類型。有點類似動態代理
數組類型也是比較特殊的。[Lxxxx
二維數組也是一樣的特殊
看下父類型:
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"); } }
父類型其實是Object
總結:
對於數組實例來說,其類型是由JVM在運行期動態生成的
動態生成的類型,其父類就是Object
對於數組來說,JavaDoc經常將構成數組的元素為Component,實際上就是將數組降低一個維度后的類型。
看下原生類型的數組:
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: 表示創建一個引用類型的(比如類、接口、數組)數組,並將其引用值壓如棧頂。
newarray: 表示創建一個指定的原始類型(如:int,float,char等)的數組,並將其引用值壓入棧頂。
以上所總結的是類與類之間的關系,包括繼承的。下面接口的特點:
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; }
接口是沒有靜態代碼塊的。可以通過手動刪除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 { // 只有在運行時候才會賦值,會放到MyTest5的常量池里面。如果Class刪除了,運行時候就會報錯! public static int b = new Random().nextInt(2); }
結論:
- 當一個接口在初始化時候,並不要求其父類接口都完成了初始化。
- 只有在真正使用到父類接口的時候(如引用接口中定義的常量時),才會初始化。
- 類,一定要先初始化父類。
public class MyTest6 { public static void main(String[] args) { Singleton instance = Singleton.getInstance(); System.out.println("counter"+ instance.counter1); System.out.println("counter"+ instance.counter2); } } class Singleton{ public static int counter1; public static int counter2 = 0; private static Singleton singleton = new Singleton(); private Singleton(){ counter1++; counter2++; } public static Singleton getInstance(){ return singleton; } }
分析: 先賦值: 默認的0 和 給定的0,然后構造方法進行++操作。
如果更改位置:
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; } }
解析:
按照從上到下的順序進行初始化。
類主動使用時候,先准備,給類的靜態變量賦初始值。
此時:
counter1 初始值 0
singleton 初始值 null
counter2 初始值 0
接着調用靜態方法 getInstance時候,賦初始值。
sigleton 會指向一個實例,然后執行私有構造方法。
然后執行到 public static int counter2 = 0時候,顯示賦值0了。
總結:
先准備
再初始化: 根據類里面代碼的順序去執行的.真正的賦值(准備為其提供初始值,要不談不上做++操作)
畫個圖:
關於類的實例化:
為對象分配內存,即為new對象,在堆上面。
為實例變量賦默認值、為實例變量賦正確的初始值都跟靜態變量似的了。賦予默認值之后,再去賦予開發者指定的值。
類的加載:
- 類的加載的最終產品是位於內充中的Class對象
- Class對象封裝了類在方法區內的數據結構,並且向Java程序員提供了訪問方法區內的數據結構的接口
Class是反射的入口。像一面鏡子一樣。
有兩種類型的類加載器:
1.Java虛擬機自帶的加載器
- 根類加載器(BootStrap)
- 擴展類加載器(Extension)
- 系統(應用)類加載器(System)
2.用戶自定義的類加載器
- java.lang.ClassLoader的子類
- 用戶可以定制類的加載方式
類的加載:
類加載器並不需要等到某個類被“首次主動使用”時候再加載它
注:
- JVM規范允許類加載器在預料某個類將要被使用時就預先加載它。如果在預先加載的過程中遇到了.class文件確實或者存在錯誤,類加載器必須在程序首次主動使用該類時候才報告錯誤(LinkageaError錯誤)
- 如果這個類一直沒有被程序主動使用,那么類加載器就不會報告錯誤
類的驗證:
類被加載后,就進入連接階段。連接就是將已經讀入到內存中的類的二進制數據合並到虛擬機的運行時的環境中去。
類的驗證的內容:
- 類文件的結構檢查
- 語義檢查
- 字節碼驗證
- 二進制兼容性的驗證
在准備階段:
初始化階段:
類的初始化步驟:
- 假如這個類還沒有被加載和連接,那就先進行加載和連接
- 假如類存在直接父類,並且這個父類還沒有被初始化,那就先初始直接父類
- 假如類中存在初始化語句,那就依次執行這些初始化語句
只有當程序訪問的靜態變量或靜態方法確實在當前類或當前接口定義時,才可以認為是對類或接口的主動使用。
調用ClassLoader類的loadClass方法加載一個類,並不是對類的主動使用,不會導致類的初始化。
除了以上虛擬機自帶的加載器外,用戶還可以定制自己的類加載器。Java提供了抽象類java.lang.ClassLoader,所有用戶自定義的類加載器都應該繼承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函數。會主動使用,先去加載。
接口和類其實是不同的,如下:
加載層面:
如果是類的話,MyChild肯定會被加載。如果是接口的話,不會被加載。
如果把b 修改為 Random(運行期才知道的值)。會將Parend 和 Child都加載. 很重要的一點是變量是編譯器的還是運行期才能確定的
如果 parent和child都是final,test用到的常量會放入自己的常量池中,則不會對parent和child進行加載了。
如果把接口換做class,則存在加載,不加載的話必須是final的!
總結出了final關鍵字的區別小結:
- final修飾的變量,決定當前類是否加載。(static修飾的,不會這樣)
- implement 實現的接口,不會加載
final修飾后,哪個類去主動調用就將這個常量放入到自己類的常量池里面。
Remember:
block 優先 構造函數執行,每次都執行。
證明初始化一個類時候,不會初始化他的接口:
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 則會初始化父類
當一個類被初始化時候,他所實現的類是不會被初始化的。
繼續看下面例子:
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)"); } }
總結:
- 先看是否是finanl修飾,是的話,就不用加載別的類。前提是編譯器的。
- 再看interface否。
類加載器的雙親委派機制:
在雙親委派機制中,各個加載器按照父子關系形成了樹形結構,除了根類加載器之外,其余的類加載器都有且只有一個父類加載器。
如果有一個類加載器能夠成功加載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"); } }
便於查看加載過程清晰:
輸出結果:
看下面的例子:
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已經被初始化了,只會初始化一次。
總結:
初始化一次就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) { //訪問父類的。調用父類的Parent的(主動使用) System.out.println(Child3.a); //訪問的父類的。調用父類的Parent的(主動使用) Child3.doSomeThing(); } }
總結:
- 雖然名字是Child3 但是沒有對其主動使用。
- 如果使用子類去訪問父類定義的變量、方法,本質上都表示對於父類的主動使用!
看下面例子:
class CL{ static { System.out.println("static block class CL"); } } public class MyTest12 { public static void main(String[] args) throws ClassNotFoundException { //系統類加載器(應用類加載器) ClassLoader classLoader = ClassLoader.getSystemClassLoader(); //指定加載的類 //這個不會導致類的初始 Class<?> clazz = classLoader.loadClass("com.jvm.t1.CL"); System.out.println(clazz); System.out.println("-------"); //類的初始化,反射導致類的初始化 clazz = Class.forName("com.jvm.t1.CL"); System.out.println(clazz); } }
總結:
- 調用classLoader.loadClass 不是對類的主動使用,不會導致初始化
- 反射是對類的主動使用
關於雙親委派機制:
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); } } }
結論:
在HotSpot中,BootStrap ClassLoader使用null表示的.(啟動類加載器)
看下面例子:
public class MyTest14 { public static void main(String[] args) { //獲取上下文的類加載器。線程創建者提供的。(有默認值的) ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); System.out.println(contextClassLoader); } }
類型是APPClassLoader,加載應用的類加載器(系統類加載器)。
看下面的例子:
public class MyTest14 { public static void main(String[] args) throws IOException { //獲取上下文的類加載器。線程創建者提供的。(有默認值的) ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader(); //存在磁盤上的字節碼(磁盤上的目錄) 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類繼承下來。
數組類的Class對象並不是由類加載器創建的,運行時由於Java虛擬機自動創建的。只有數組如此
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()); } }
總結:
- 根據里面的每個元素的類型定義的!String、MyTest15。
- 雖然獲取到了數組的類加載器,但是數組對應的Class對象並不是ClassLoader加載的,是JVM動態創建的。
- 原生類型,沒有加載器。
自己定義類加載器,看下面例子:
public class MyTest16 extends ClassLoader { private String classLoaderName = ""; private String fileExtension = ".class"; public MyTest16(String classLoaderName) { super(); // 將系統類加載器當做該類加載器的父類加載器 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和linux this.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); } // 字節數組輸出流轉換成字節數組 data = baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally { try { is.close(); baos.close(); } catch (Exception e) { e.printStackTrace(); } } return data; } @Override protected 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 { //內部底層的api已經被我們重寫了 Class<?> clazz = classLoader.loadClass("com.jvm.t1.MyTest15"); Object object = clazz.newInstance(); System.out.println(object); } @Override public String toString() { return "[" + this.classLoaderName + "]"; } public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException { MyTest16 loader1 = new MyTest16("loader1"); test(loader1); } }
其實此時我們定義的 findClass是沒有被調用的!以為雙親委派機制,讓父類去加載了!
看下面例子:
public class MyTest16 extends ClassLoader { private String classLoaderName = ""; private String fileExtension = ".class"; private String path; public MyTest16(String classLoaderName) { super(); // 將系統類加載器當做該類加載器的父類加載器 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和linux this.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); } // 字節數組輸出流轉換成字節數組 data = baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally { try { is.close(); baos.close(); } catch (Exception e) { e.printStackTrace(); } } return data; } @Override protected 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); } @Override public String toString() { return "[" + this.classLoaderName + "]"; } public static void main(String[] args) throws IllegalAccessException, InstantiationException, ClassNotFoundException { // 創建自定義類加載器 名字“loader1” 父類加載器是系統類加載器 MyTest16 loader1 = new MyTest16("loader1"); //此路徑為classPath,故 findClass方法不會被調用執行! 如果換個路徑,不是classPath就會去執行了! 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目錄下面找,找到了加載之。
關於命名空間:
- 每個類加載器都有自己的命名空間,命名空間由該加載器及所有父加載器所加載的類組成
- 同一個命名 空間中,不會出現類的完整名字(包括類的包名)相同的兩個類
- 在不同的命名空間中,有可能會出現類的完整名字(包括類的包名)相同的兩個類
關於類的卸載:
- 當MySample類被加載、連接和初始化后,他的聲明周期就開始了。當代表MySample類的Class對象不再被引用,即不可觸及時,Class對象就會結束聲明周期,MySample類在方法區內的數據也會被卸載,從而結束Sample類的生命周期。
- 一個類何時結束生命周期,取決於代表他的Class對象何時結束生命周期。
- 由用戶自定義的類加載器所加載的類是可以被卸載的。
加載 <----> 卸載
看下面的例子:
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()); //如果注釋掉改行,那么並不會實例化MySample對象,即MySample構造方法不會被調用 // 因此不會實例化MyCat對象,即沒有對MyCat進行主動使用,這里就不會加載MyCat class Object object = clazz.newInstance();// new instance 沒有任何參數。調用無參構造方法 } }
關於命名空間的說明:
- 子加載器加載的類,能夠訪問父加載器加載的類。
- 父加載器加載的類,不能訪問子加載器加載的類。
public class Test3 { public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InstantiationException, NoSuchMethodException, InvocationTargetException { MyTest16 loader1 = new MyTest16("loader1`"); MyTest16 loader2 = new MyTest16("loader2`"); loader1.setPath("/User/test/"); loader1.setPath("/User/test/"); //加載相同的類。(都委托為appClassLoader了) Class<?> clazz1 = loader1.loadClass("com.jvm.test.Test"); //加載過了 Class<?> clazz2 = loader2.loadClass("com.jvm.test.Test"); // 都是app加載的,雙親委派 System.out.println(clazz1 == clazz2); Object o1 = clazz1.newInstance(); Object o2 = clazz2.newInstance(); Method setMyPerson = clazz1.getMethod("setMyPerson", Object.class); //執行o1的方法,參數是o2 setMyPerson.invoke(o1, o2); } }
情況1.如果 class字節碼在classPath,返回 true。 執行成功。(讀者自行考慮,提示雙親委派)
情況2.如果 class字節碼只在:"/User/test/" 。返回false。執行報錯。
原因
- 命名空間。 兩個loader不存在父子關系,是平行的。在jvm中存在兩個命名空間。
- 不同命名空間的類不可見,引用不到就報錯。(子加載器的命名空間包含所有父加載器的命名空間,子可看到父類加載的所有類。)
雙親委派的好處:
- 可以確保Java核心庫的類型安全,所有的Java應用都至少會引用java.lang.Object類,也就是說在運行期,java.lang.Object這個類會被加載到Java虛擬機中。
- 如果這個加載過程是由Java應用自己的類加載器所完成的,那么很可能就會在JVM匯總存在多個版本的java.lang.Object類。而且這些類之間是不兼容,相互不可見的(命名空間)。
- 借助雙親委派機制,java核心類庫中的類的加載工作都是啟動類加載器統一完成的。確保了Java應用所使用的都是同一個版本的Java核心類庫,他們之間是相互兼容的。Java核心類庫不會被自定義的替代。啟動類去加載之。
- 不同的類加載器可以為相同名稱(binary name)的類創建額外的命名空間,相同名稱的類可以並存在Java虛擬機中,只需要不用的類加載器(包括沒有父子關系、不同類加載器)來加載他們即可。不同類加載器所加載的類是不兼容的。這就相當於在Java虛擬機內部創建了一個又一個相互隔離的 Java類空間,這類技術在很多框架中都得到了實際應用。
知識總結:
- 關於擴展類加載器:需要做成jar包,再放到指定目錄下。
- 在運行期,一個Java類是由該類的完全限定名(binary name, 二進制名)和用於加載該類的定義列類加載器(defing loader)所共同決定的。如果同樣名字(即相同的完全限定名)的類是由兩個不同的加載器所加載,那么這些類就是不同的。即便 .class文件的字節碼完全一樣,並且從相同的位置加載亦如此。
- 在Oracle的hotSopt實現中,系統屬性sun.boot.class.path如果修改錯了,則運行會報錯,提示: Error occurred during Initialization of VM
- 內建於JVM中的啟動類加載器會加載java.lang.ClassLoader以及其他的Java平台類,當JVM啟動時候,一塊特殊的機器碼會運行,他會擴展類加載器與系統類加載器,這塊特殊的機器碼叫做啟動類加載器(BootStrap)。
- 啟動類加載器並不是Java類,而其他的加載器都是Java類。啟動類加載器是特定於平台的機器指令,它負責開啟整個加載過程。
- 所有類加載器(除啟動類加載器)都被實現為Java類。不過,總歸要有一個組件來加載第一個Java類加載器,從而讓整個加載過程能夠順利進行下去,加載第一個純Java類加載器就是啟動類加載器的職責。
- 啟動類加載器還會負責加載供JRE正常運行所需要的基本組件,這包括java.util與java.lang包中的類等等。
簡單看下:
public class test4 { public static void main(String[] args) { System.out.println(ClassLoader.class.getClassLoader()); //擴展類 System.out.println(Launcher.class.getClassLoader()); } }
可以自己做系統類加載器。略。需要控制台指令顯示指定
通過改變屬性,提示:
System.getProperty("java.system.class.loader")
引申:
getSystemClassLoader()
- 返回用於委托的系統類加載器,
- 創建的ClassLoader默認的爸爸(也是用於啟動應用的類加載器)。
- 創建類加載器,然后設置為調用這個方法的線程的上下文類加載器。(Contex Class Loader)。應用框架,服務器大量使用的!
- 默認的系統類加載器,與此類實現相關的實例。
- java.system.class.loader所指定的類,是被默認的系統類加載器加載。必須要定義public的構造方法,傳遞自定義類加載器的爸爸。
OpenJDK是JDK開源版本。
解析Class.forName:
其實:Class.forName("Foo") 等價於 Class.forName("Foo",true, this.getClass().getClassLoader() )
關於線程上下文的類加載器: 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()); // 路徑位置導致的 } }
當前類加載器(Current ClassLoader)
每個類都會使用自己的類加載器(即加載自身的類加載器)去加載其它類(指的是所依賴的類):
如果ClassX引用了ClassY,那么ClassX的類加載器就會去加載ClassY(前提是ClassY尚未被加載)
線程上下文類加載器:
- 線程上下文類加載器是從JDK1.2開始引入的,類Thread中的getContextClassLoader 與 setContextClassLoader(ClassLoader cl) 分別用來獲取和設置上下文類加載器
- 如果沒有通過setContextClassLoader進行設置的話。線程將繼承其父線程的上下文類加載器。
- Java應用運行時的初始線程的上下文類加載器是系統類加載器。在線程中運行的代碼可以通過該類加載器來加載類與資源。
線程上下文類加載器的重要性:
應用場景:
SPI(Service Provider Interface)
父ClassLoader可以使用當前線程Thread.currentThread().getContexClassLoader() 所指定的ClassLoader加載的類,這就改變了父ClassLoader不能使用子ClassLoader或是其他沒有直接父子關系的ClassLoader加載的類的情況。
線程上下文類加載器就是當前線程的Current ClassLoader
在雙親委派模型下,類加載是由下至上的,即下層的類加載器會委托上層進行加載。但是對於SPI來說,有些接口是Java類核心庫所提供的,而Java核心庫是由啟動類加載器來加載的,而這些接口的實現卻來自於不同的jar包(廠商提供。
Java的啟動類加載器是不會加 載其他來源你的Jar包 ,這樣的傳統的雙親委派模型就無法滿足SPI的要求。而通過給當前線程設置上下文類加載器,就可以由設置的上下文類加載器來實現對於接口實現類的加載。
總結:接口是啟動類加載器加載的, 實現類應用類加載器加載,通過給當前的線程設置上下文類加載器,實現對於接口實現類的加載,打破了雙親委派模型現在。(框架開發,底層開發會用到)
(JDK中沒有對於JDBC的任何實現,除了傳統的接口之外,具體實現都是由廠商趨勢線的,比如MySQL。)
看下面代碼:
public class MyTest25 implements Runnable { private Thread thread; public MyTest25(){ thread = new Thread(this); thread.start(); } @Override public 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(); } }
沒有設置,所以線程將繼承父線程的上下文類加載器。
線程上下文類加載器的一般使用模式(獲取 - 使用 - 還原)
注意:如果一個類由A加載器加載,那么這個類的依賴也是由相同的類加載器加載的(如果該依賴之前沒有被加載過的話)
ContextClassLoader的作用就是為了破壞Java的類加載委托機制
當高層提供了統一的接口讓底層去實現,同時又要在高層加載(或者實例化)低層的類時候,就必須要通過線程上下文類加載器來幫助高層的ClassLoader找到並加載該類
看下面例子:
public class MyTest26 { public static void main(String[] args) { //設置下 // 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("當前線程上下文類加載器:" + Thread.currentThread().getContextClassLoader()); System.out.println("ServiceLoader的類加載器:" + ServiceLoader.class.getClassLoader()); } }
關於字節碼:
對於能編譯成class字節碼的代碼,class的規范,合法性保證好了就OK了。
對於Idea編譯器,是非常熟悉class字節碼了,可以隨心所欲的反編譯。
對於java代碼:
public class MyTest1 { private int a = 1; public int getA() { return a; } public void setA(int a) { this.a = a; } }
idea看字節碼:
// // 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; } }
通過反編譯指令:
看到三個方法:其中一個是默認的構造方法。
詳細查看字節碼信息:輸入
javap -c com.jvm.t1.t2.MyTest1
Compiled from "MyTest1.java" public class com.jvm.t1.t2.MyTest1 {
//構造方法 public com.jvm.t1.t2.MyTest1();
//下面都是助記符 Code: 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: iconst_1 6: putfield #2 // Field a:I 9: return public int getA(); Code: 0: aload_0 1: getfield #2 // Field a:I 4: ireturn public void setA(int); Code: 0: aload_0 1: iload_1 2: putfield #2 // Field a:I 5: return }
看下面指令:
javap -verbose com.jvm.t1.t2.MyTest1
Classfile /D:/eclipse_pj/dianshang/jvmTest/out/production/jvmTest/com/jvm/t1/t2/MyTest1.class Last modified 2019-10-20; size 473 bytes MD5 checksum c5b1387c6f6c79b14c1b6a5438da3b29 Compiled from "MyTest1.java" public class com.jvm.t1.t2.MyTest1 minor version: 0 major version: 52 flags: ACC_PUBLIC, ACC_SUPER
// 常量池: 占據相當大的比重 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: ()V flags: ACC_PUBLIC Code: stack=2, locals=1, args_size=1 0: aload_0 1: invokespecial #1 // Method java/lang/Object."<init>":()V 4: aload_0 5: iconst_1 6: putfield #2 // Field a:I 9: return LineNumberTable: line 3: 0 line 5: 4 LocalVariableTable: Start Length Slot Name Signature 0 10 0 this Lcom/jvm/t1/t2/MyTest1; public int getA(); descriptor: ()I flags: ACC_PUBLIC Code: stack=1, locals=1, args_size=1 0: aload_0 1: getfield #2 // Field a:I 4: ireturn LineNumberTable: line 8: 0 LocalVariableTable: Start Length Slot Name Signature 0 5 0 this Lcom/jvm/t1/t2/MyTest1; public void setA(int); descriptor: (I)V flags: ACC_PUBLIC Code: stack=2, locals=2, args_size=2 0: aload_0 1: iload_1 2: putfield #2 // Field a:I 5: return LineNumberTable: line 12: 0 line 13: 5 LocalVariableTable: Start Length Slot Name Signature 0 6 0 this Lcom/jvm/t1/t2/MyTest1; 0 6 1 a I } SourceFile: "MyTest1.java"
使用如上的這個命令分析字節碼時候,將會分析該字節碼文件的魔數,版本號,常量池,類信息,類的構造方法,類中的方法信息,類變量與成員變量等信息。
備注:
魔數: 所有的.class字節碼文件的前4個字節都是魔數,魔數值為固定值: 0xCAFEBABE。
魔數之后的4個字節為版本信息,前兩個字節表示minor version(次版本號),后兩個字節表示major version(主版本號)。
常量池(constant pool): 緊接着主板號之后就是常量池入口。一個Java類中定義的很多信息都是由常量池來維護和描述的。常量池在整個字節碼文件中占的比重最大,里面的信息會被很多地方引用到。相當於把常量集中在一個地方,其他地方用到時候去引用之。通過Index找到常量池中特定的常量。可以將常量池看做是class文件的資源倉庫。比如:Java類總定義的方法與變量信息,都是存儲在常量池中。常量池中主要存儲兩類常量:字面量與符號引用量。
注意:常量池!里面存放的不一定都是常量。也有變量信息。
- 字面量如文本字符串,Java中聲明為final 的常量值等,而符號引用,比如說類和接口的全局限定名,字段的名稱和描述符,方法的名稱和描述符等。
常量池的總體結構: Java類所對應的常量池主要由常量池數量與常量池數組(常量表)這兩部分共同組成。常量池數量緊跟在主版本后面,占據2個字節;常量池數組則緊跟在常量池數量之后。常量池數組和一般的數組不同的是,常量池數組中不同的元素的類型,結構都是不同的,長度當然也就不同;但是每一種元素的數都是一個u1類型,該字節是個標志位,占據1個字節。JVM在解析常量池時候,會根據這個u1類型來獲取元素的具體類型。值得注意的是:常量池數組中元素的個數 = 常量池數 - 1 (其中0暫時不使用)。目的是滿足某些常量池索引值的數據在特定情況下需要表達 【不引用任何一個常量池】的含義。根本原因在於,索引為0也是一個常量(保留常量)。只不過它不位於常量表中,這個常量就對應null值。所以常量池的索引從1而非從0開始。
如下,從1開始:
常量池中數據類型:
在JVM規范中,每個變量/字段都有描述信息,描述信息主要的作用是描述字段的數據類型、方法的參數列表(包括數量、類型與順序)與返回值。根據描述符規則,基本數據類型和代表無返回值的void類型都
用一個大寫字符來表示,對象類型則使用字符L加對象的全限定名稱來表示。為了壓縮字節碼文件的體積。對於基本數據類型,JVM都只使用一個大寫字母來表示,如下所示:
B ---> byte C --> char D ---> doube F ---> float I --> int J --long S --> short Z --> boolean V --> void
L --->對象類型 ,如: L java/lang/String
對於數組類型來說,每一個維度使用一個前置的 [ 來表示,如 int[ ] 被記錄為 [I , String[][] 被記錄為[[ Ljava/lang/String
用描述符描述方法時,按照先參數列表,后返回值的順序來描述。參數列表按照參數的嚴格順序放在一組之內,如方法:
get getName (int id, String name)描述為:
常量池里面存儲的各種 index 和 信息
Java字節碼整體結構:
完整Java字節碼接口例子:
Access_Flag訪問標志
訪問標志信息包括該Class文件是類還是接口,是否被定義成public,是否是abstract,如果是類,是否被聲明稱final。
字段表集合:
字段表用於描述類和接口中聲明的變量。這里的字段包含了類級別變量(靜態變量)以及實例變量(非靜態變量),但是不包括方法內部聲明的局部變量。
一個field_info包含的信息:
方法表:
methods_count: u2
前三個字段和field_info一樣
方法中每個屬性都是一個attribute_info結構
JVM預定義了部分attribute,但是編譯器自己也可以實現自己的attribute寫入class文件里,供運行使用
不同的attribute通過attribute_name_index來區分
Code結構
Code attribute的作用是保存該方法的結構,如所對應的字節碼
- attribute_length 表示attribute所包含的字節數,不包含attribute_name_index 和 attribute_length字段
- max_stack 表示這個方法運行的任何時刻所能達到的操作數棧的最大深度
- max_locals表示方法執行期間創建的局部變量的數目,包含用來表示傳入的參數的局部變量
- code_length表示該方法所包含的字節碼的字節數以及具體的指令碼
- 具體字節碼即是該方法被調用時,虛擬機所執行的字節碼
- exception_table,這里存放的是處理異常的信息
- 每個exception_table表項由start_pc, end_pc, handler_pc, catch_type 組成
- start_pc和end_pc表示在code數組中的從start_pc到end_pc處(包含start_pc, 不包含end_pc)的指令拋出的異常會由這個表項來處理。
- handeler_pc 表示處理異常的代碼的開始處,catch_type 表示會被處理的異常類型,它指向常量池里的一個異常類。當catch_type為0時,表示處理所有異常。
- 方法中的每個屬性都是一個attribute_info結構
code attribute的作用是保存該方法的結構,如所對應的字節碼
推薦大家使用: jclasslib 閱讀字節碼信息
Java中,每一個方法都是可以訪問this(表示對當前對象的引用),
字節碼角度,如果方法本身是個非靜態(實例)的,this可以作為方法的第一個方法,可以隱式的傳遞進來。會使得每個實例方法都可以訪問this。至少會有個局部變量,這個局部變量就是this。
對於某各類Test,中的靜態方法 使用了synchronized 關鍵字,相當於給這個Test對應的Class對象加鎖了。
關於this關鍵字:
Java編譯器在編譯時候,把對this方法的訪問,轉變成了對普通參數的訪問。在Java中,每一個非靜態實例的方法的局部變量中,至少會存在一個指向當前對象的局部變量。即:
對於Java類中的每一個實例方法(非static方法),其中在編譯后所生成的字節碼當中,方法參數的數量總會比源代碼匯總方法的參數多一個(this),它位於方法的第一個參數位置處;這樣我們就可以在Java實例方法中使用this訪問當前對象的屬性以及其他方法。這個操作是在編譯期間完成的,由javac編譯器,在編譯時候將對this的訪問轉化為對一個普通實例方法參數的訪問,接下來在運行期間,由JVM在調用實例方法時,自動向實例方法傳入該this參數。所以,在實例方法的局部變量表中,至少會有一個指向當前對象的局部變量。
關於異常處理:
Code結構:
attribute_length表示attribute鎖包含的字節數,不包含attribute_name_index和attribute_length字段
max_stack表示這個方法運行的任何時刻所能達到的操作數棧的最大深度
max_locals表示方法執行期間所創建的局部變量的數目,包含用來表示傳入的參數的局部變量
code_lenght表示該方法所含的字節碼的字節數以及具體的指令碼
具體字節碼即是該方法被調用時,虛擬機所執行的字節碼
exception_table, 這里存放的是處理異常的消息
每個exception_tabel 表項由start_pc, end_pc , handler_pc ,catch_type 組成
start_pc 和 end_pc 表示在code 數組中的從start_pc都end_pc處(包含start_pc, 不包含end_pc)的指令拋出的異常會由這個表項來處理
handler_pc表示處理異常的代碼的開始處。catch_type 表示會被處理的異常類型,它指向常量池里的一個異常類。當catch_type為0時,表示處理所有的異常。
Java字節碼對於異常的處理方式:
1. 統一采用異常表的方式來對異常進行處理
2. 老版本中,並不是使用遺產表的方式來對異常進行處理的,而是采用特定的指令方式(了解)
3. 當異常處理存在finally語句塊時,現代化的JVM采取的方式將finally語句塊的字節碼拼接到每一個catch塊后面,換句話說,程序存在多少個catch塊,就會在每一個catch塊后面重復多少個finally語句塊的字節碼。
棧幀,是一種用於幫助虛擬機執行方法調用與方法執行的數據結構。
棧幀, 本身是一種數據結構,封裝了風閥的局部變量表,動態鏈接信息,方法的返回地址操作數棧等信息。
Java中,對於不同的類之間的關系,編譯期間,地址關系實際上是不知道的。什么時候知道?
1. 類加載時候
2. 真正調用時候,才知道目標方法地址。
基於以上兩點,引申出了符號引用和直接引用。
有些符號引用是在類加載階段或是第一次使用時就會轉換為直接引用,這種轉換叫做靜態解析;另外一些符號引用則是在每次運行期轉為直接引用,這種轉換叫做動態鏈接,這體現為Java的多態性
比如父類因用戶指向子類實現。
Aninaml a = new Cat(); a.run(); a = new Fish(); a.run
編譯時候,a都是Animal. 字節碼角度,都是Animal
運行時候,每次運行期,都會進行一次直接引用的轉換。
JVM 方法調用的字節碼指令:
1. invokeinterface:調用接口中的方法,實際上是在運行期決定的,決定到底調用實現該接口的那個對象的特定方法(一個接口,n個實現類)。
2. invokestatic: 調用靜態方法
3.invokespecial: 調用自己的私有方法,構造方法(<init>) 以及父類的方法
4. invokevirtual: 調用虛方法,運行期動態查找的過程。
5. invokedynamic: 動態調用方法。
靜態解析的四種情況:
1. 靜態方法
2.父類方法
3. 構造方法
4. 私有方法(公有方法可以被重寫或者復寫,多態的可能。私有方法在加載時候就能夠被確定了)
以上四種稱之為: 非虛方法。他們是在類加載階段就可以將符號引用轉換為直接引用的。
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的靜態類型是Grandpa,而father的實際類型(真正指向的類型)是Father
變量本身的靜態類型是不會被改變的, GrandPa father
結論:
變量的靜態類型是不會發生變化的,而變量的實際類型是可以發生變化的(多態的一種體現)。實際類型是在運行期方可確定。
以上,方法的重載,參數類型不一樣。方法重載是一種純粹的靜態行為。
所以,當使用myTest5調用方法的時候, 是根據類型進行匹配。尋找類型是 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{ @Override public void test() { System.out.println("apple"); } } class Orange extends Fruit{ @Override public void test() { System.out.println("orange"); } }
引申:
Java中,new起到了三個作用:
1. 在堆上開辟空間
2. 執行構造方法
3. 將構造方法執行后返回的堆上的此引用值返回
方法的動態分派:
方法的動態分派涉及到一個重要概念:方法接收者
invokevirtual字節碼指令的多態查找流程
方法重載和方法重寫,我們可以得到這個方法重載是靜態的,是編譯器行為,方法重寫是動態的,是運行期行為。
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{ @Override public void test(String str) { System.out.println("dog str"); } @Override public void test(Date date) { System.out.println("dog date"); } }
針對於方法調用動態分派的過程,虛擬機會在類的方法區建立一個虛方法表的數據結構(virtual method table,簡稱 vtable)
現代JVM在執行Java代碼的時候,通常會將解釋執行與編譯執行二者結合起來執行。
所謂解釋執行:通過解釋器讀取字節碼,遇到相應的指令就去執行該指令
所謂編譯執行:通過及時編譯器(Just In Time, JIT)將字節碼轉為本地機器碼來執行,現代JVM會根據代碼熱點來生成相應的本地機器碼。
基於棧的指令集合基於寄存器的指令集之間的關系:
1. JVM執行指令時所采取的的方式是基於棧的指令集
2. 基於棧的指令集的主要操作: 入棧、出棧
3. 基於棧的指令集的優勢在於他可以在不同平台間一直,而基於寄存器的指令集是與硬件架構密切關聯的,無法做到可移植。
4. 基於棧的指令集的缺點: 完成相同的操作,執行數量通常要比基於寄存器的指令集數量多 。基於棧的指令集是在內存中操作的,而基於寄存器的指令集是直接由CPU執行的,它是在高速緩沖區中進行的,速度要快很多。雖然虛擬機可以采用一些優化手段,但總體 來說,基於棧的指令集的執行速度要慢一些。
注意:
棧 配合 局部變量表使用,局部變量表的0位置是this
對應動態代理,主要有一個類(proxy)和一個接口(InvocationHandler)去搞定。
接口:
public interface Subject { void request(); }
實現類:
public class RealSubject implements Subject { @Override public void request() { System.out.println("reslsubjct"); } }
代理類:
/** * 動態代理文件 */ public class DynamicSubject implements InvocationHandler { private Object sub; public DynamicSubject(Object obj){ this.sub = obj; } @Override public 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對象是為了,動態代理需要類加載器。 Subject subject = (Subject) Proxy.newProxyInstance(clazz.getClassLoader(), clazz.getInterfaces(), dynamicSubject); subject.request(); System.out.println(subject.getClass()); } }
程序運行期動態生成的:
首先創建代理類,然后創建代理類的實例對象。
對象分為兩部分內容:
1, 對象本身擁有的那些數據(位於堆)
2, 對象所屬的類型(元數據信息,MetaData) 所有實例對應一個Class對象。位於方法區(存儲的一部分對象的類型數據信息)
方案一:
對象引用的是一個指向對象實例的指針,另外一個指針指向方法區中的類型數據
方案二:(HotSpot的方案)
對象引用的是對象本身,和一個指向方法區匯總的類型數據指針 (對象實例數據、方法區)
兩種方案的差別L
堆發生垃圾回收頻率很高,對於垃圾回收算法來說,有幾種會涉及到對象移動(壓縮):為了保證區域連續的地方增大,移動之
方案一:對象一旦移動了,指針值會發生變化!隨着每次垃圾回收會變化。
方案二:指針不會隨之變化。
JVM內存划分:
虛擬機棧
程序計數器
本地方法棧:主要用於處理本地方法
堆: JVM管理的最大一塊內存空間
線程共享的區域,主要存儲元信息。從JDK1.8開始,徹底廢棄永久代。使用元空間(meta space)
運行時常量池(方法區的一部分): 方法區的一部分內容。編譯后的字節碼的符號引用等等。加載完后,放入到方法區的運行時常量池。
直接內存: Direct Memory。 與Java NIO密切相關,JVM通過堆上的DirectByteBuffer來直接操作內存。
現代幾乎所有的垃圾收集器都是采用的分代收集算法,所以堆空間也基於這一點進行了相應的划分。
Java對象的創建:
new
反射
克隆
反序列化
new關鍵字創建對象的3個步驟:
1, 在堆內存中創建出對象的實例
2, 為對象成員變量賦初始值(指的是,實例變量,區別靜態變量)
3, 將對象的引用返回。
虛擬機干的活兒: 檢查指令的參數new指令創建一個對象,指令參數是不是能在常量池中定位成一個類的符號引用。查看這個類是不是已經加載、鏈接、初始化了。
指針碰撞: 前提是堆中的空間通過一個指針進行分割,一側是已經被占用的空間,另一側是未被占用的空間。
空閑列表:(前提是堆內存空間中已被使用與未被使用的空間交織在一起的。這時,虛擬機就需要通過一個列表來記錄那些空間是可以用的,哪些空間是已被使用的,接下來找出可以容納下新創建對象的且未被使用的空間,在此空間存放該對象,同時還要修改列表的記錄)
一個對象包含三部分布局:
1.對象的頭,
2.實例數據(class中定義的成員變量)
3.對齊填充
永久代屬於與堆連接的一個空間,對於永久代處理是比較麻煩的。
元空間,使用的操作系統的本地內存。可以不連續的。元空間里還有元空間虛擬機,管理元空間的內存的分配和回收情況。 初始大小21M,隨着對於內存占用,會進行垃圾回收,甚至內存擴展,可以擴展到內存大小的最大值。
存放一個類的元數據信息,在框架中,用到運行期動態生成類的手段。動態創建出來的類,元信息放在元空間。
元空間參數: -XX:MaxMetaspaceSize=200M
在Java虛擬機(以下簡稱JVM)中,類包含其對應的元數據,比如類的層級信息,方法數據和方法信息(如字節碼,棧和變量大小),運行時常量池,已確定的符號引用和虛方法表。
在過去(當自定義類加載器使用不普遍的時候,幾乎不動態搭理),類幾乎是“靜態的”並且很少被卸載和回收,因此類也可以被看成“永久的”。另外由於類作為JVM實現的一部分,它們不由程序來創建,因為它們也被認為是“非堆”的內存。
在JDK8之前的HotSpot虛擬機中,類的這些“永久的”數據存放在一個叫做永久代的區域。永久代一段連續的內存空間,我們在JVM啟動之前可以通過設置-XX:MaxPermSize的值來控制永久代的大小,32位機器默認的永久代的大小為64M,64位的機器則為85M。永久代的垃圾回收和老年代的垃圾回收是綁定的,一旦其中一個區域被占滿,這兩個區都要進行垃圾回收。但是有一個明顯的問題,由於我們可以通過‑XX:MaxPermSize 設置永久代的大小,一旦類的元數據超過了設定的大小,程序就會耗盡內存,並出現內存溢出錯誤(OOM)。
備注:在JDK7之前的HotSpot虛擬機中,納入字符串常量池的字符串被存儲在永久代中,因此導致了一系列的性能問題和內存溢出錯誤。想要了解這些永久代移除這些字符串的信息,請訪問這里查看。
隨着Java8的到來,我們再也見不到永久代了。但是這並不意味着類的元數據信息也消失了。這些數據被移到了一個與堆不相連的本地內存區域,這個區域就是我們要提到的元空間。
這項改動是很有必要的,因為對永久代進行調優是很困難的。永久代中的元數據可能會隨着每一次Full GC發生而進行移動。並且為永久代設置空間大小也是很難確定的,因為這其中有很多影響因素,比如類的總數,常量池的大小和方法數量等。
同時,HotSpot虛擬機的每種類型的垃圾回收器都需要特殊處理永久代中的元數據。將元數據從永久代剝離出來,不僅實現了對元空間的無縫管理,還可以簡化Full GC以及對以后的並發隔離類元數據等方面進行優化。
移除永久代的影響
由於類的元數據分配在本地內存中,元空間的最大可分配空間就是系統可用內存空間。因此,我們就不會遇到永久代存在時的內存溢出錯誤,也不會出現泄漏的數據移到交換區這樣的事情。最終用戶可以為元空間設置一個可用空間最大值,如果不進行設置,JVM 會自動根據類的元數據大小動態增加元空間的容量。
注意:永久代的移除並不代表自定義的類加載器泄露問題就解決了。因此,你還必須監控你的內存消耗情況,因為一旦發生泄漏,會占用你的大量本地內存,並且還可能導致交換區交換更加糟糕。
元空間內存管理
元空間的內存管理由元空間虛擬機來完成。先前,對於類的元數據我們需要不同的垃圾回收器進行處理,現在只需要執行元空間虛擬機的 C++ 代碼即可完成。在元空間中,類和其元數據的生命周期和其對應的類加載器是相同的。話句話說,只要類加載器存活,其加載的類的元數據也是存活的,因而不會被回收掉。
我們從行文到現在提到的元空間稍微有點不嚴謹。准確的來說,每一個類加載器的存儲區域都稱作一個元空間,所有的元空間合在一起就是我們一直說的元空間。當一個類加載器被垃圾回收器標記為不再存活,其對應的元空間會被回收。在元空間的回收過程中沒有重定位和壓縮等操作。但是元空間內的元數據會進行掃描來確定 Java 引用。
元空間虛擬機負責元空間的分配,其采用的形式為組塊分配。組塊的大小因類加載器的類型而異。在元空間虛擬機中存在一個全局的空閑組塊列表。當一個類加載器需要組塊時,它就會從這個全局的組塊列表中獲取並維持一個自己的組塊列表。當一個類加載器不再存活,那么其持有的組塊將會被釋放,並返回給全局組塊列表。類加載器持有的組塊又會被分成多個塊,每一個塊存儲一個單元的元信息。組塊中的塊是線性分配(指針碰撞分配形式)。組塊分配自內存映射區域。這些全局的虛擬內存映射區域以鏈表形式連接,一旦某個虛擬內存映射區域清空,這部分內存就會返回給操作系統。
上圖展示的是虛擬內存映射區域如何進行元組塊的分配。類加載器 1 和 3 表明使用了反射或者為匿名類加載器,他們使用了特定大小組塊。 而類加載器 2 和 4 根據其內部條目的數量使用小型或者中型的組塊。
參考:https://www.infoq.cn/article/Java-PERMGEN-Removed
命令:jstat -gc 進程號 打印元空間信息
jmap -clstats PID 打印類加載器數據
jcmd PID GC.class_stats 診斷命令
jcmd 是從jdk1.7開始增加的命令
1. jcmd pid VM.flag:查看JVM啟動參數
2. jcmd pid help: 列出當前運行的Java進行可以執行的操作
3. jcmd pid help JFR.dump: 查看具體命令的選項
4. jcmd pid PerfCounter.print: 查看JVM性能相關參數
5. jcmd pid VM.uptime:查看JVM的啟動時長
6. jcmd pid GC.class_histogram 查看系統中類的統計信息
7. jcmd pid Thread.print: 查看線程堆棧信息
8. jcmd pid GC.heap_dump filename: 導出heap dump文件,導出的文件可以通過jvisualvm查看
9. jcmd pid VM.system_properties: 查看JVM的屬性信息
10. jcmd pid VM.version: 查看目標JVM進程的版本信息
11. jcmd pid VM.command_line:查看JVM啟動的命令行參數信息
jstack: 可以查看或是導出Java應用程序中棧線程的堆棧信息
jmc: java Mission Control
補充:
針對於犯法調用動態分派的過程,虛擬機會在類的方法區建立一個虛方法表的數據結構(virtual method table, vtable)
針對於invokeinterface指令來說,迅疾會建立一個叫接口方法表的數據結構(interface method table, itable)
JVM運行時數據區:
程序計數器
本地方法棧
Java虛擬機棧(JVM Stack)
- Java虛擬機棧描述的是Java方法的執行模型: 每個方法執行的時候都會創建一個幀(Frame)棧用於存放局部變量表,操作棧,動態鏈接,方法出口等信息。一個方法的執行過程,就是這個方法對於幀棧的入棧出棧過程。
- 線程隔離
堆
- 堆里存放的是對象的實例
- 是Java虛擬機管理內存中最大的一塊
- GC主要的工作區域,為了高效的GC,會把堆細分成更多的子區域
- 線程共享
方法區:
- 存方法每個Class的結構信息,包括常量池,字段描述,方法描述
- GC的非主要工作區域
看下面例子:
public void method(){ Object obj = new Object(); }
生成了兩部分內存區域:
1.obj這個引用變量,因為是方法內的變量,放到JVM Stack里面
2. 真正Object class的實例對象,放到Heap里面
上述的new語句一共消耗12個byte。JVM規定引用占4個byte(JVM Stack),而空對象是8個byte(在Heap)
方法結束后,對應Stack中的變量馬上回收,但是Heap中的對象要等GC來回收
垃圾判斷算法:
引用計數算法(Reference Counting)
無法解決對象循環引用的問題
跟搜索算法(Root Tracing)
GC Root:
在VM棧(幀中的本地變量)中的引用
方法區中的引用變量
JNI(即一般說的Native方法) 中的引用
方法區:
Java虛擬機規范表示可以不要求虛擬機在這區實現GC,這區GC的“性價比”一般比較低
在堆中,尤其是在新生代,常規應用進行一次GC一般可以回收70%~95%的空間,二方法區的GC效率遠小於此
當前的商業JVM都有實現方法區的GC,主要回收兩部分內容:廢棄常量與無用類
主要回收兩部分內容: 廢棄常量與無用類
類回收需要滿足如下3個條件:
該類所有的實例都已經備GC, 也就是JVM中不存在該Class的任何實例
加載該類的ClassLoader已經被GC
該類對應的Java.lang.Class對象沒有任何地方被引用,如不能在任何地方通過反射訪問該類的方法
在大量使用反射、動態代理、CGLib等字節碼框架、動態生成JSP以及OSGi這類頻繁自定義ClassLoader的場景都需要JVM具備類卸載的支持以保證方法區不會溢出
標記清除算法:
標記+清除兩個過程效率不高嗎,需要掃描所有對象。
產生不連續碎片,空間碎片提多可能會導致后續使用中無法找到足夠的連續內存而提前觸發另一次的垃圾收集動作
復制算法:
現在的商業虛擬機用的復制算法來回收新生代。Hotspot虛擬機默認eden和survivor的大小比例是8:1,也就是每次只有10%的內存是“浪費”的
在對象存活率高的時候,效率有所下降
如果不想浪費50%的空間,就需要有額外的空間進行分配擔保用於應付半區內存中所有對象都100%存活的極端情況,所以在老年代一般不能直接選用這種算法。
只需要掃描存活的對象,效率更高
不會產生碎片
需要浪費額外的內存作為復制區
適合生命周期端的對象,因為每次GC總能回收大部分的對象,復制的開銷比較小
標記整理算法:
沒有內存碎片
比標記清除耗費更多的時間
分代收集:
一般是把Java對分為新生代和老年代,這樣就可以根據年齡特點采用最適當的收集算法。新生代GC有大批對象死去,只有少量存活,那就選用復制算法只需要付出少量存活對象的復制成本就可以完成收集。
老年代:
- 存放了經過一次或多次GC還存活的對象
- 一般采用Mark-Sweep或者Mark-Compact算法進行GC
- 有多種垃圾收集器可以選擇。每種垃圾收集器可以看做一個GC算法的具體實現。可以根據具體應用的需求選擇合適的垃圾收集器(吞吐量?響應時間?)
永久代:
並不屬於對(Heap)但是GC也會涉及到這個區域
存放了每個Class的機構信息,包括常量池,字段描述,方法描述。與垃圾收集要收集的Java對象關系不大
內存結構:
備注: 在Hotspot中本地方法棧和JVM方法棧是同一個,因此也可以用-Xss控制
GC要做的是將那些dead的對象所占用的內存回收掉
- HotSpot認為沒有引用的對象是dead的
- HotSpot將引用分為四種: Strong、Soft、 Weak、 Phantom
Strong即默認通過Object o = new Object() 這種方式賦值的引用
Soft、Weak、Phantom這三種原則都是集成Reference
在Full GC時會對Reference類型的引用進行特殊處理
Soft: 內存不夠時候,一定會被GC。長期不用也會被GC
Weak: 一定會被GC,當Mark為Dead,會在ReferenceQueue中通知
Phantom: 本來就沒有引用,當從JVM heap中釋放時會通知
GC的時機:
在分代模型的基礎上,GC從時機上分為兩種: Scavenge GC 和 Full GC
1.Scanvenge GC(Minor GC) :
觸發時機: 新對象生成時,Eden空間滿了
理論上Eden區大多數對象會在Scavenge GC回收,復制算法的執行效率會很高,Scavenge GC時間比較短
2. Full GC(stop the world)
對整個JVM進行整理,包括Young、Old 和Perm
主要的觸發時機: 1.Old滿了 2.Perm滿了 3.System.gc()
效率很低,盡量減少Full GC
垃圾收集器的並發和並行:
並行: 多個垃圾收集器的線程同時工作,但是用戶線程處於等待狀態
並發: 收集器在工作的同事,可以允許用戶線程工作。 並發不代表解決了GC停頓的問題,在關鍵的步驟還是要停頓。比如在垃圾收集器標記垃圾的時候。但是在清除垃圾的時候,用戶線程可以和GC線程並發執行。
Serial收集器:
單線程收集器,收集時會暫停所有工作線程(Stop The World)使用復制收集算法,虛擬機運行在Client模式時的默認新生代收集器。
最早的收集器,單線程進行GC
新生代和老年代都可以使用
在新生代,采用復制算法;在老年代,采用標記整理算法
因為是單線程GC,沒有多線昵稱切換的額外開銷,簡單使用
Hotpot Client模式缺省的收集器
ParNew收集器
ParNew收集器就是Serial的多線程版本,除了使用多個收集線程外,其余行為包括算法,STW,對象分配規則,回收策略等都與Serial收集器一模一樣
對應的這種收集器是JVM運行在Server模式的默認新生代收集器,在單CPU的環境中,ParNew收集器並不會比Serial收集器有更好的效果。只有在多CPU的環境下,效率才會比Serial收集器高
使用復制算法(因為針對新生代)
通過-XX:ParallelGCThreads來控制GC線程數的多少。需要結合具體的CPU個數
Server模式下新生代的缺省收集器
Parallel Scavenge收集器
Parallel Scavenge 收集器也是一個多線程的收集器,也是使用復制算法,但它的對象分配規則與回收策略都與ParNew收集器有所不同,它是以吞吐量最大化(即GC時間棧中運行時間最小)為目標的收集器實現,它允許較長時間的STW換取總吞吐量最大化
Serial Old收集器
Serial Old是單線程收集器,使用標記整理算法,是老年代的收集器。
老年代版本吞吐量優先收集器,使用多線程和標記整理,JVM1.6提供,在此以前,新生代使用了PS收集器的話,老年代除Serial Old別無選擇。因為PS無法與CMS收集器配合工作。
Parallel Old
Parallel Scavenge在老年代的實現
在JVM1.6才出現 Parallel Old
采用對線程,Mark-Compact算法
更注重吞吐量
Parallel Scavenge + Prallel Old = 高吞吐量,但GC停頓可能不理想
CMS(Concurrent Mak Sweep)
cms是一種最短停頓時間為目標的收集器,使用CMS不能達到GC效率最高(總體GC時間最小),但它能盡可能降低GC時服務的停頓時間,CMS收集器使用的是標記清除算法
追求最短停頓時間,非常適合Web應用
只針對老年區,一般結合ParNew使用
Concurrent,GC線程和用戶線程並發工作(盡量並發)
Mark-Sweep
只有在多CPU環境下才有意義
使用-XX:+UseConcMarkSweepGC打開
缺點:
以犧牲CPU資源的帶勁來減少用戶線程的停頓。當CPU個數少於4到時候,有可能對吞吐量影響非常大
CMS在並發清理的過程中,用戶線程還在跑。這是需要預留一部分空間給用戶線程
CMS用Mark-Sweep,會帶來碎片問題。碎片過多時候會容易頻繁觸發Full GC。
GC樂基收集器的JVM參數定義
Java內存泄露常見原因:
1.對象定義在錯誤范圍(Wrong Scope)
2.異常(Exception)處理不當
3.集合數據管理不當
如果Foo實例對象的生命較長,會導致臨時性內存泄露。(這里的names變量其實只有臨時作用)
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喜歡生命周期短的兌現,這樣做已經足夠高效。 (成員變量變成局部變量)
class Foo{ public void doIt(int length){ String[] names = new String[length]; populate(names); print(names); } }
連接不釋放:finally
rs.close() // 必須釋放
集合數據管理不當:
當使用 Array-based的數據結構(ArrayList,HashMap等),盡量減少resize
- 比如new ArrayList時,盡量估算size,在創建的時候把size確定
- 減少resize可以避免沒有必要的array copying, gc碎片等問題
如果一個List只需要順序訪問,不需要隨機訪問(Random Access),用LinkedList代替ArrayList
- LinkedList本質是鏈表,不需要resize,但只適用於順序訪問
輸出JVM垃圾回收,詳盡的情況:-verbose:gc
初始大小-Xms20M
堆大小-Xmx20M
(相等,啟動時候不會出現都抖動問題。)
新生代大小 -Xmn10m
垃圾回收詳細信息:-XX:+PrintGCDetails
Edn:survor的比值 : -XX:SurvivorRatio=8
public class t { public static void main(String[] args) { int size = 1024 * 1024; //原生的數組 里面都是0 byte[] myAlloc1 = new byte[2 * size]; byte[] myAlloc2 = new byte[2 * size]; byte[] myAlloc3 = new byte[2 * size]; System.out.println("hello world"); } }
看出沒有發生GC:
繼續添加 數組:
byte[] myAlloc3 = new byte[2 * size];
發生了GC
新生代的垃圾回收,怎么回收都不夠呀。除了程序的,JVM啟動時候也會有大量的對象
Full GC 會導致Stop the World 要避免之 Full GC后老年代有可能會變多哦
來個GC的
PS: Parallel Scavenge收集器
9216K / 1024 = 9K
這樣的新生代使用的是9k 因為from to 有一個區域始終是閑置的
5646 - 624 = 5022K 執行完gc后,新生代釋放的空間容量(包括晉升到老年代的)
5646-4728 = 918 執行完gc后,總的堆空間釋放的容量
5022 - 918 = 4104K 老年代使用的容量
當新生代已經容納下,待分配的對象時候,新創建的,直接誕生在老年代。
看下面的例子:
public class t { public static void main(String[] args) { int size = 1024 * 1024; //原生的數組 里面都是0 byte[] myAlloc1 = new byte[2 * size]; byte[] myAlloc2 = new byte[2 * size]; byte[] myAlloc3 = new byte[2 * size]; //這個數組直接在老年代分配! byte[] myAlloc4 = new byte[4 * size]; System.out.println("hello world"); } }
沒有發生Full GC:
直接在老年代分配對象!
不指定時候,默認使用:
PSYoungGen: Parallel Scavenge(新生代垃圾收集器)
ParOldGen: Parallel Old (老年代垃圾收集器)
命令行指令:
java -XX:+PrintCommandLineFlags -version
虛擬機參數: -XX:PretenureSizeThreshold=4194304 閾值 必須配合另外一個參數!-XX:UseSerialGC 否則不起作用
當新創建的對象大小超過這個,就直接誕生在老年代了
public class t { public static void main(String[] args) { int size = 1024 * 1024; //5M 超過閾值 byte[] myAlloc1 = new byte[5 * size]; } }
運行結果:
JVM:
-Xms20M
-Xmx20M
-Xmn10m
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
-XX:PretenureSizeThreshold=4194304
-XX:+UseSerialGC
結論:
PretenureSizeThreadshold:設置超過多個時直接在老年代分配
如果需要分配10M
public class t { public static void main(String[] args) { int size = 1024 * 1024; //10M byte[] myAlloc1 = new byte[10 * size]; } }
經過大量GC后,發現不能分配。放不下10M! 新生代+老年代一共才10M
設置閾值后,即便是新生代能容納,一樣會分配到老年代!
MaxTenuringThreshold作用: 在可以自動調節對象晉升(Promote)到老年代閾值的GC中,設置該閾值的最大值(再怎么自動調節也不會超過這個最大值)。
--XX:MaxTenuringThreshold=5
改參數默認15,CMS中默認值為6,G1默認為15(在JVM中,該數值是由4個bit來表示的,所以最大值111 即15)
-XX:+PrintTenuringDistribution 打印的效果,比如打印年齡為1的對象的情況等等
經歷了多次GC后,存活的對象會在From Survivor 和 To Survivor之間來回存放,而這里的一個前提是這兩個空間有足夠的空間來存放數據,在GC算法中,會計算每個對象年齡的大小。如果達到了某個年齡后發現總大小已經大於了Survivor空間的50%,那么這時就需要調調整閾值,不能在繼續等到默認的15次GC后才完成晉升。
因為這樣會導致Survivor空間不足,所以需要調整閾值,讓這些存活對象盡快完成晉升。
看下面的例子:
-Xms20M
-Xmx20M
-Xmn10m
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
-XX:MaxTenuringThreshold=5
-XX:+PrintTenuringDistribution
-XX:+PrintCommandLineFlags
public class t { public static void main(String[] args) { int size = 1024 * 1024; //10M byte[] myAlloc1 = new byte[2 * size]; byte[] myAlloc2 = new byte[2 * size]; byte[] myAlloc3 = new byte[2 * size]; byte[] myAlloc4 = new byte[2 * size]; System.out.println("hello world"); } }
運行結果:
-XX:+PrintCommandLineFlags : 圖中箭頭的啟動參數信息
max 是設置的閾值 new Threshold 5 是自動調整的值
看下面的例子:
配置:
-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空間已經被存活的占據空間60,重新計算閾值。
-XX:+PrintTenuringDistribution #打印對象在survivor在對空間的年齡情況
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps #打印當前GC執行的的時間戳
-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:
枚舉根節點:
當執行系統停頓下來后,並不需要一個不漏的檢查完所有執行上下文和全局的引用位置,虛擬機應當是有辦法直接得知哪些地方存放着對象引用。在HotSpot的實現中,是使用一組成為OopMap的數據結構來達到這個目的的。
安全點:
在OopMap的協助下,HotSpot可以快速且准確的完成GC Root枚舉,但一個很現實的問題隨之而來:可能導致引用關系變化,或者說OopMap內容變化的指令非常多,如果為每一條指令都生成對應的OopMap,那將會需要大量的額外空間,這樣GC的空間成本將會變得更 高亮
實際上,HotSpot並沒有為每條指令都生成OopMap,而只是在“特定的位置”記錄了這些信息,這些位置成為安全點(SafePoint),即程序執行時並非在所有地方都停頓下來開始GC,只有在達到安全點時才能暫停。
SafePoint 的選定既不能太少以至於讓GC等待時間太長,也不能過於頻繁以至於過飯增大運行時的負載。所以,安全點的選定基本上是以“是否具有讓程序長時間執行的特征”為標准進行選定的。因為每條指令執行的時間非常短暫,程序不太可能因為指令流長度太長而過 長的時間運行,“長時間執行”的最明顯特征就是指令序列復用,例如方法調用、循環跳轉、異常跳轉等。所以具有這些功能的指令才會產生SafePoint.
對於SafePoint,另一個需要考慮的問題時如何在GC發生時讓所有線程(這里不包括執行JNI調用的線程)都“跑”到最近的安全點上再停頓下來:搶占式中斷(Preemptive Suspension)和主動式中斷(Voluntary Suspension)
- 搶占式中斷: 他不需線程的執行代碼主動去配合,在GC發生時,首先把所有線程全部中斷,如果有線程中斷的地方不在安全點上,就恢復線程,讓它“跑”到安全點上。
- 主動式中斷: 當GC需要中斷線程的時候,不直接對線程操作,僅僅簡單的設置一個標志,各個線程執行時主動去輪訓這個標志,發現中斷標志為真時就自己中斷掛起。輪訓標志的地方和安全點是重合的,另外再加上創建對象需要分配內存的地方
現在幾乎沒有虛擬機采用搶占式中斷來暫停線程從而響應GC事件
安全區域:
在使用SafePoint似乎已經完美地解決了如何進入GC的問題,但實際上情況卻不一定。SafePoint機制保證了程序執行時候,在不太長時間內就會遇到可進入GC的SafePoint。但如果程序在“不執行”的時候呢?所謂程序不執行就是沒有分配CPU時間,典型的例子就是出於Sleep狀態或者Blocked狀態,這時候線程無法響應JVM的中斷請求,JVM也顯然不太可能等地線程重新分配CPU時間,對於這種情況,就需要安全區域(Safe Regin)來解決了。
在線程執行到Safe Regin中的代碼時候,首先標識自己已經進入了Safe Regin,那樣,當在這段時間里JVM要發起GC時,就不用管標識自己為Safe Region狀態的線程線程了。在線程要離開Safe Region時,他要檢查系統是否已經完成了根節點枚舉(或者是整個GC過程),如果完成了,那線程就繼續執行,否則他就必須等待線程直到收到可以安全離開Safe Region的信號為止。
CMS收集器
CMS收集器,以獲取最短回收停頓時間為目標,多數應用於互聯網站或者B/S系統的服務器上
CMS是基於“標記-清除”算法實現的,整個工程分為四個步驟:
- 初始標記(CMS initial Mark)
- 並發標記 (CMS concurrent mark)
- 重新標記 (CMS remark)
- 並發清除 (CMS concurrent sweep)
其中,初始標記、重新標記這兩個步驟仍然需要“Stop The World”
初始標記只是標記一下GC Roots能直接關聯到的對象,速度很快
並發標記階段就是進行GC Roots Tracing的過程
重新標記階段則是為了修正並發標記期間因用戶程序繼續運作而導致標記產生變動的那一部分對象的標記記錄,這個階段的停頓時間一般會比初始標記階段稍微長一些,但遠比並發標記的時間短。
CMS收集器的運作步驟如下圖所示,在整個過程中耗時最長的並發標記和並發清除過程收集器線程都是可以與用戶線程一起工作,因此,從總體上看,CMS收集器的內存回收過程是與用戶線程一起並發執行的
優點:
並發收集、低停頓,Oracle公司的一些官方文檔中也稱之為並發低停頓收集器(Concurrent Low Pause Collector)
缺點:
- CMS收集器對CPU資源非常敏感
- CMS收集器無法處理浮動垃圾(FloatingGarbage),可能出現“Concurrent mode Failure”失敗而導致另一次Full GC的產生。如果在應用中老年代增長不是太快,可以適當調高參數-XX:CMSInitiatingOccupancyFraction的值來提高觸發百分比,以便降低內存回收次數從而獲取更好的性能,要是CMS運行期間預留的內存無法滿足程序需要時,虛擬機將啟動后備預案:臨時啟用Serial Old收集器來重新進行老年代的垃圾收集,這樣停頓時間就很長了。所有說參數-XX:CMSInitiatingOccupancyFraction設置的太高很容易導致大量“Concurrent Mode Failure”失敗,性能反而降低。
- 收集結束時候,會有大量空間碎片產生,空間碎片過多時,將會給大對象分配帶來很大麻煩,往往出現老年代還有很大空間剩余,但是無法找到足夠大的連續空間來支配當前對象,不得不提前進行一次Full GC。CMS收集器提供了一個 XX:+UseCMSCompactAtFullCollection開關參數(默認就是開啟的),用於在CMS收集器頂不住時候要進行Full GC時開啟內存碎片的合並整理過程,內存整理的過程是無法自拔的,空間碎片問題沒有了,但停頓時間不得不邊長。
空間分配擔保:
在發生Minor GC之前,虛擬機會先檢查老年代最大可用的連續空間是否大於新生代所有對象總空間,如果這個條件成立,那么Minor GC可以確保是安全的。當大量對象在MinorGC后仍存活,就需要老年代進行空間分配擔保,把Survivor無法容納的對象直接進入老年代。如果老年代判斷到剩余空間不足(根據以往每一次回收晉升到老年代對象容量的平均值作為經驗值),則進行一次Full GC
CMS收集器收集步驟:
- Initial Mark
- Concurrent Mark
- Concurrent Preclean
- Concurrent Abortable Preclean
- Final Remark
- Concurrent Sweep
- Concurrent Reset
1. 是CMS兩次Stop The World 事件中的其中一次,這個階段的目標是:標記哪些直接被GC Root 引用或者被年輕代存活對象所引用的所有對象。
2.在這個階段Garbage Collector會遍歷老年代,然后標記所有存活的對象,它會根據上個階段找到的GC Root是遍歷查找。並發標記階段,他會與用戶的應用程序並發運行。並不是老年代的所有的存活對象都會被標記,因為在標記期間用戶的程序可能會改變一些引用。
在上面的圖中,與階段1的圖進行對比,就會發現有一個對象的引用地址發生了變化。
3. 這是個並發階段,與應用的線程並發運行,並不會stop應用的線程。在並發運行的過程中,一些對象的引用可能會發生變化,但是這種情況發生時,JVM會將包含這個對象的區域(Card)標記為Dirty,也就是Card Marking
在pre-clean階段,那些能夠從Dirty對象到達的對象也會被標記,這個標記做完之后,dirty card 標記就會被清除了
4. 這也是一個並發階段,但是同樣不會影響影響用戶的應用線程,這個階段是為了盡量承擔 STW(stop-the-world)中最終標記階段的工作。這個階段持續時間依賴於很多的因素,由於這個階段是在重復做很多相同的工作,直接滿足一些條件(比如:重復迭代的次數、 完成的工作量或者時鍾時間等)
5.這是第二個 STW 階段,也是 CMS 中的最后一個,這個階段的目標是標記所有老年代所有的存活對象,由於之前的階段是並發執行的,gc 線程可能跟不上應用程序的變化,為了完成標記老年代所有存活對象的目標,STW 就非常有必要了。
通常 CMS 的 Final Remark 階段會在年輕代盡可能干凈的時候運行,目的是為了減少連續 STW 發生的可能性(年輕代存活對象過多的話,也會導致老年代涉及的存活對象會很多)。這個階段會比前面的幾個階段更復雜一些。
經歷着五個階段之后,老年代所有存活的對象都被標記過來,現在可以通過清除算法去清除哪些老年代不再使用的對象。
6.這里不需要STW,它是與用戶的應用程序並發運行,這個階段是:清除哪些不再使用的對象,回收他們的占用空間為將來使用
7. 這個階段也是並發執行的,它會重設CMS內部的數據機構,為下次的GC做准備
總結:
- CMS通過將大量工作分散到並發處理階段來減少STW時間,在這塊做的非常優秀,但是CMS也有一些其他問題。
- CMS收集器無法處理浮動垃圾(Floating garbage)可能出現“Concurrent Mode Failure”失敗而導致另一次Full GC的產生,可能引發串行Full GC
- 空間碎片,導致無法分配大對象,CMS收集器提供了一個-XX:+UseCMSCompactAtFullCollection開發參數(默認就是開啟的),用於在CMS收集器頂不住要進行Full GC時開啟內存碎片的合並整理過程,內存整理的過程是無法並發的,空間碎片問題沒有了,但停頓時間不得不變長;
- 對於堆比較大的應用,GC的時間難以估計。
看下面例子:
配置參數:
-verbose:gc
-Xmx20M
-Xms20M
-Xmn10m
-XX:+PrintGCDetails
-XX:SurvivorRatio=8
-XX:+UseConcMarkSweepGC
public class t { public static void main(String[] args) { int size = 1024 * 1024; byte[] myAlloc1 = new byte[4 * size]; System.out.println("1111"); byte[] myAlloc2 = new byte[4 * size]; System.out.println("2222"); byte[] myAlloc3 = new byte[4 * size]; System.out.println("3333"); byte[] myAlloc4 = new byte[4 * size]; System.out.println("4444"); } }
默認情況下,老年代的CMS 和新生代的 ParNew是成對出現的。
可以看到CMS收集過程
吞吐量:
吞吐量關注的是,在一個指定的時間內,最大化一個應用的工作量。
如下方式來衡量一個系統吞吐量的好壞:
1.在一小時內同一個事務(或者任務、請求)完成的次數(tps)。
2.數據庫一小時可以完成多少次查詢;
3.對於關注吞吐量的系統,卡頓是可以接受的,因為這個系統關注長時間的大量任務的執行能力,單次快速的響應並不值得考慮
響應能力:
響應能力指一個程序或者系統對請求的是否能夠及時響應。
比如:
一個桌面UI能多快的響應一個事件;
一個網站能夠多快返回一個頁面請求;
數據庫能夠多快返回查詢的數據;
對於這類對響應能力敏感的場景,長時間的停頓是無法接受的。
G1收集器是一個面向服務端的垃圾收集器,適用於多核處理器,大內存容量的服務端系統
它滿足短時間GC停頓的同時達到一個較高的吞吐量
JDK7以上版本使用
G1收集器的設計目標
- 與應用線程同時工作,幾乎不需要stop-the-world(與CMS類似);
- 整理剩余空間,不產生內存碎片;(CMS只能在full-GC時,用stop-the-world整理碎片內存)
- GC停頓更加可控;
- 不犧牲系統的吞吐量;
- GC不要求額外的內存空間(CMS需要預留空間存儲浮動垃圾);
G1的設計規划是要替換掉CMS
- G1在某些方面彌補了CMS的不足,比如,CMS使用的是mark-sweep算法,自然會產生碎片;然而G1基於copying算法,高效的剩余內存,而不需要管理內存碎片。
- 另外,G1提供了更多手段,以達到對gc停頓的可控。
堆:
下面看看G1:
G1收集器堆結構
- heap被划分為一個個相等的不連續的內存區域(regions), 每個region都有一個分代的角色: eden、survivor、old
- 對每個角色的數量並沒有強制限定,也就是說對每種分代內存的大小,可以動態變化
- G1最大的特點就是高效的執行回收,優先去執行哪些大量對象可回收的區域(region)
- G1使用了gc停頓可預測的模型,來滿足用戶設定的gc停頓時間,根據用戶設定的目標時間,G1會自動的選擇哪些region需要清除,一次清除多少個region
- G1從多個region中復制存活的對象,然后集中放入一個region中,同時整理、清除內存(copying收集算法)
G1 VS CMS
- 對比使用mark-sweep的CMS,G1使用的copying算法不會造成內存碎片
- 對比Parallel Scavenge(基於copying)、parallel Old收集器(基於mark-compact-sweep), Parallel會對整個區域做整理導致gc停頓時間比較長,而G1只是特定的整理幾個region
- G1並非一個實時的收集器,與parallel Scavenge一樣,對gc停頓時間的設置並不絕對生效,只是G1有較高的幾率保證不超過設定的gc停頓時間。與之前的gc收集器對比,G1會根據用戶設定的gc停頓時間,智能評估哪幾region需要被回收可以滿足用戶的設定。
G1重要概念:
- 分區(Region): G1采取了不同的策略來解決並行、串行和CMS收集器的碎片、暫停時間不可控等問題---G1將整個的堆分成相同大小的分區(region)每個分區都可能是年輕代也可能是老年代,但是在同一時刻只能屬於某個代。
- 年輕代、幸存區、老年代這些概念還存在,成為邏輯上的概念,這樣方便復用之前分代框架的邏輯。
- 在物理上不需要連續,則帶來了額外的好處——有的分區內垃圾對象特別多,有的分區內垃圾對象很少,G1會優先回收垃圾對象特別多的分區,這樣可以花費較少的時間來回收這些分區的垃圾,這也就是G1名字的由來,即首先收集垃圾最多的分區。
- 新生代其實並不是適用於這種算法的,依然是在新生代滿了的時候,對整個新生代進行回收—— 整個新生代中的對象,要么被回收、要么晉升到老年代,至於新生代也采取分區機制的原因,則是因為這樣跟老年代的策略統一,方便調整代的大小。
- G1還是一種帶壓縮的收集器,在回收老年代的分區時,是將存活的對象從一個分區拷貝到另一個可用分區,這個拷貝的過程就實現了局部的壓縮。每個分區的大小從1M到32M不等,但是都是2的冥次方。
-
收集集合(CSet):一組可被回收的分區的集合。在CSet中存活的數據會在GC過程中被移動到另一個可用分區,CSet中的分區可以來自Eden空間、survivor空間、或者老年代。CSet會占用不到整個堆空間的1%大小。
-
已記憶集合(RSet)每個Region都有一個關聯的Remembered Set。RSet記錄了其他Region中的對象引用本Region中對象的關系,屬於points-into結構(誰引用了我的對象)。RSet的價值在於使得垃圾收集器不需要掃描整個堆找到誰引用了當前分區中的對象,只需要掃描RSet即可。
如下圖所示,Region1和Region3中的對象都引用了Region2中的對象,因此在Region2的RSet中記錄了這兩個引用。
- G1 GC則是在points-out的card table之上再加了一層結構來構成points-into RSet:每個region會記錄下到底哪些別的region有指向自己的指針,而這些指針分別在哪些card的范圍內。
- 這個RSet其實是一個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的關系;而card table仍然記錄了points-out的關系。
-
Snapshot-At-The-Beginning(SATB):SATB是維持並發GC的正確性的一個手段,G1 GC的並發理論基礎就是SATB。SATB是由Taiichi Yuasa為增量式標記清除垃圾收集器設計的一個標記算法。
- 並發標記是並發多線程的,但並發線程在同一時刻只掃描一個分區。
G1相對於GMS的優勢:
1、G1在壓縮空間方面有優勢
2、G1通過將內存空間分成區域(Region)的方式避免內存碎片問題
3、Eden, Survivor, Old區不再固定、在內存使用效率上來說更靈活
4、G1可以通過設置預期停頓時間(Pause Time)來控制垃圾收集時間避免應用雪崩現象(預測模型,統計數據分析)
5、G1在回收內存后會馬上同時做合並空閑內存的工作、而CMS默認是在STW(stop the world)的時候做
6、G1會在Young GC中使用、而CMS只能在O區使用
重要概念:
- 每個分區都可能是年輕代也可能是老年代,但是在同一時刻只能屬於某個代。年輕代,幸存區,老年代這些概念還存在,成為邏輯上的概念,這樣方便復用之前分代框架的邏輯。
- 在物理上不需要連續,則帶來了額外的好處---有的分區內垃圾對象特別多,有的分區內垃圾對象很少,G1會優先回收垃圾對象特別多的分區,這樣可以花費比較少的時間來回收這些分區的垃圾,這也是G1名字的由來,即首先收集垃圾最多的分區。
- 依然是在新生代滿了的時候,對整個新生代進行回收---整個新生代中的對象,要么被回收,要么晉升,至於新生代也采取分區機制的原因,則是因為這樣老年代的策略統一,方便調整代的大小。
G1的適合場景
- 服務端多核CPU、JVM內存占用較大的應用(至少大於4G)
- 應用在運行過程中會產生大量內存碎片、需要經常壓縮空間
- 想要更可控、可預期的GC停頓周期;防止高並發下應用雪崩現象
G1 GC模式:
- G1提供了兩種GC模式,Young GC和Mixed GC,兩種都是Stop The World(STW)的
- Young GC: 選定所有年輕代里的Region。通過控制年輕代的Region個數,即年輕代內存大小,來控制Young GC的時間開銷
- Mixed GC: 選定所有年輕代里的Region,外加根據global concurrent marking統計得出收集收益高的若干老年Region(垃圾對象多的老年代Region).在用戶指定的開銷目標范圍內盡可能選擇收益高的老年代Region
- Mixed GC不是Full GC,它只能回收部分老年代的Region,如果Mixed GC實在無法跟上程序分配內存的速度,導致老年代填滿無法繼續進行Mixed GC,就會使用serial old GC(Full GC)來收集整個GC heap。所以本質上,G1是不提供Full GC的
- global concurrent marking,它的執行過程類似CMS,但是不同的是,在G1 GC中,它主要是為Mixed GC提供標記服務的,並不是一次GC過程的一個必須環節。
global concurrent marking的執行過程分為四個步驟:
初始標記(initial mark,STW)。它標記了從GC Root開始直接可達的對象。
並發標記(Concurrent Marking)。這個階段從GC Root開始對heap中的對象標記,標記線程與應用程序線程並行執行,並且收集各個Region的存活對象信息。
重新標記(Remark,STW)。標記那些在並發標記階段發生變化的對象,將被回收。
清除垃圾(Cleanup)。清除空Region(沒有存活對象的),加入到free list。
第一階段initial mark是共用了Young GC的暫停,這是因為他們可以復用root scan操作,所以可以說global concurrent marking是伴隨Young GC而發生的。
第四階段Cleanup只是回收了沒有存活對象的Region,所以它並不需要STW。
G1在運行過程中的主要模式:
1. YGC(不同於CMS)
2. 並發階段
3. 混合模式
4. Full GC(一般是G1出現問題時發生)
注:
在Eden充滿時觸發,在回收之后所有之前屬於Eden的區塊全變成空白。然后把剩余的存活對象移動到S區。
什么時候觸發Mixed GC?
由一些參數控制,另外也控制着哪些老年代Region會被選入CSet(收集集合)
- G1HeapWastePercent:在global concurrent marking結束之后,我們可以知道old gen regions中有多少空間要被回收,在每次YGC之后和再次發生Mixed GC之前,會檢查垃圾占比是否達到此參數,只有達到了,下次才會發生Mixed GC。
- G1MixedGCLiveThresholdPercent:old generation region中的存活對象的占比,只有在此參數之下,才會被選入CSet。
- G1MixedGCCountTarget:一次global concurrent marking之后,最多執行Mixed GC的次數。
- G1OldCSetRegionThresholdPercent:一次Mixed GC中能被選入CSet的最多old generation region數量
除了以上的參數,G1 GC相關的其他主要的參數有:
參數 | 含義 |
---|---|
-XX:G1HeapRegionSize=n | 設置Region大小,並非最終值 |
-XX:MaxGCPauseMillis | 設置G1收集過程目標時間,默認值200ms,不是硬性條件 |
-XX:G1NewSizePercent | 新生代最小值,默認值5% |
-XX:G1MaxNewSizePercent | 新生代最大值,默認值60% |
-XX:ParallelGCThreads | STW期間,並行GC線程數 |
-XX:ConcGCThreads=n | 並發標記階段,並行執行的線程數 |
-XX:InitiatingHeapOccupancyPercent | 設置觸發標記周期的 Java 堆占用率閾值。默認值是45%。這里的java堆占比指的是non_young_capacity_bytes,包括old+humongous |
G1收集概覽:
- G1算法將堆划分為若干區域(Region),它仍然屬於分代收集器。不過,這些區域的一部分包含新生代,新生代的垃圾收集依然采用暫停所有應用線程的方式,將存活對象拷貝到老年代或者Survivor空間。老年代也分成很多區域,G1收集器通過將對象從一個區域復制到另一個區域,完成了清理工作。這就意味着,在正常的處理 過程中,G1完成了堆的壓縮(至少是部分堆壓縮),這樣就不會有CMS內存碎片問題的存在。
- 在G1中,還有一種特殊的區域,叫Humongous區域。 如果一個對象占用的空間超過了分區容量50%以上,G1收集器就認為這是一個巨型對象。這些巨型對象,默認直接會被分配在年老代,但是如果它是一個短期存在的巨型對象,就會對垃圾收集器造成負面影響。為了解決這個問題,G1划分了一個Humongous區,它用來專門存放巨型對象。如果一個H區裝不下一個巨型對象,那么G1會尋找連續的H分區來存儲。為了能找到連續的H區,有時候不得不啟動Full GC
- Young GC主要是對Eden區進行GC,它在Eden空間耗盡時會被觸發。在這種情況下,Eden空間的數據移動到Survivor空間中,如果Survivor空間不夠,Eden空間的部分數據會直接晉升到年老代空間。Survivor區的數據移動到新的Survivor區中,也有部分數據晉升到老年代空間中。最終Eden空間的數據為空,GC停止工作,應用線程繼續執行。
-
如果僅僅GC 新生代對象,我們如何找到所有的根對象呢? 老年代的所有對象都是根么?那這樣掃描下來會耗費大量的時間。於是,G1引進了RSet的概念。它的全稱是Remembered Set,作用是跟蹤指向某個heap區內的對象引用。
- 在CMS中,也有RSet的概念,在老年代中有一塊區域用來記錄指向新生代的引用。這是一種point-out,在進行Young GC時,掃描根時,僅僅需要掃描這一塊區域,而不需要掃描整個老年代。
-
但在G1中,並沒有使用point-out,這是由於一個分區太小,分區數量太多,如果是用point-out的話,會造成大量的掃描浪費,有些根本不需要GC的分區引用也掃描了。
-
於是G1中使用point-in來解決。point-in的意思是哪些分區引用了當前分區中的對象。這樣,僅僅將這些對象當做根來掃描就避免了無效的掃描。
-
由於新生代有多個,那么我們需要在新生代之間記錄引用嗎?這是不必要的,原因在於每次GC時,所有新生代都會被掃描,所以只需要記錄老年代到新生代之間的引用即可。
-
需要注意的是,如果引用的對象很多,賦值器需要對每個引用做處理,賦值器開銷會很大,為了解決賦值器開銷這個問題,在G1 中又引入了另外一個概念,卡表(Card Table)。一個Card Table將一個分區在邏輯上划分為固定大小的連續區域,每個區域稱之為卡。卡通常較小,介於128到512字節之間。Card Table通常為字節數組,由Card的索引(即數組下標)來標識每個分區的空間地址
Young GC 階段:
階段1:根掃描
靜態和本地對象被掃描
階段2:更新RS
處理dirty card隊列更新RS
階段3:處理RS
檢測從年輕代指向年老代的對象
階段4:對象拷貝
拷貝存活的對象到survivor/old區域
階段5:處理引用隊列
軟引用,弱引用,虛引用處理關於G1 Mix GC: Mix GC不僅進行正常的新生代垃圾收集,同時也回收部分后台掃描線程標記的老年代分區。
它的GC步驟分2步:
- 全局並發標記(global concurrent marking)
- 拷貝存活對象(evacuation)
提到並發標記,我們不得不了解並發標記的三色標記算法。它是描述追蹤式回收器的一種有用的方法,利用它可以推演回收器的正確性。 首先,我們將對象分成三種類型的。
- 黑色:根對象,或者該對象與它的子對象都被掃描
- 灰色:對象本身被掃描,但還沒掃描完該對象中的子對象
- 白色:未被掃描對象,掃描完成所有對象之后,最終為白色的為不可達對象,即垃圾對象
根對象被置為黑色,子對象被置為灰色。
繼續由灰色遍歷,將已掃描了子對象的對象置為黑色。

遍歷了所有可達的對象后,所有可達的對象都變成了黑色。不可達的對象即為白色,需要被清理。

這看起來很美好,但是如果在標記過程中,應用程序也在運行,那么對象的指針就有可能改變。這樣的話,我們就會遇到一個問題:對象丟失問題。
我們看下面一種情況,當垃圾收集器掃描到下面情況時:

這時候應用程序執行了以下操作:
A.c=C
B.c=null
這樣,對象的狀態圖變成如下情形:

這時候垃圾收集器再標記掃描的時候就會下圖成這樣:

引申SATB
在G1中,使用的是STAB(snapshot-at-the-beginning)的方式,刪除的時候記錄所有的對象,它有3個步驟:
1,在開始標記的時候生成一個快照圖標記存活對象
2,在並發標記的時候所有被改變的對象入隊(在write barrier里把所有舊的引用所指向的對象都變成非白的)
3,可能存在游離的垃圾,將在下次被收集
G1混合式回收
這樣,G1到現在可以知道哪些老的分區可回收垃圾最多。 當全局並發標記完成后,在某個時刻,就開始了Mix GC。這些垃圾回收被稱作“混合式”是因為他們不僅僅進行正常的新生代垃圾收集,同時也回收部分后台掃描線程標記的分區。
混合式GC也是采用的復制的清理策略,當GC完成后,會重新釋放空間。
G1分代算法:
為老年代設置分區的目的是老年代里有的分區垃圾多,有的分區垃圾少,這樣在回收的時候可以專注於收集垃圾多的分區,這也是G1名稱的由來。
不過這個算法並不適合新生代垃圾收集,因為新生代的垃圾收集算法是復制算法,但是新生代也使用了分區機制主要是因為便於代大小的調整。
SATB:
SATB的全稱是Snapchat-At-The_Beginning。SATB是維持並發GC的一種手段。G1並發的基礎就是SATB。SATB可以理解成在GC開始之前對堆內存里的對象做一次快照,此時活的對象就認為是活的,從而形成一個對象圖。
在GC收集的時候,新生代的對象也認為是活的對象,除此之外其他不可達的對象都認為是垃圾對象。除此之外其他不可達的對象都認為是垃圾對象。
如何找到在GC的過程中分配的對象呢(對象引用的變更,新對象的生成)?
每個region記錄着兩個top-at-mark-start(TAMS)指針,分別為prevTAMS和nextTAMS。在TAMS以上的對象就是新分配的,因而被視為隱式marked。
通過這種方式我們就找到了在GC過程中新分配的對象,並把這些對象認為是活的對象。
解決了對象在GC過程中分配的問題,那么在GC過程中引用發生變化的問題怎么解決呢,
G1給出的解決辦法是通過Write Barrier。Write Barrier就是對引用字段進行賦值做了環切。通過Write Barrier就可以了解到哪些引用對象發生了什么樣的變化。
SATB全稱為Snapshot At The Beginning,其要點如下:
- mark的過程就是遍歷heap標記live object的過程,采用的是三色標記算法,這三種顏色為white(表示還未訪問到)、gray(訪問到但是它用到的引用還沒有完全掃描)、back(訪問到而且其用到的引用已經完全掃描完)。
- 整個三色標記算法就是從GC roots出發遍歷heap,針對可達對象先標記white為gray,然后再標記gray為black;遍歷完成之后所有可達對象都是balck的,所有white都是可以回收的。
- SATB僅僅對於在marking開始階段進行“snapshot”(marked all reachable at mark start),但是concurrent的時候並發修改可能造成對象漏標記。
- 對black新引用了一個white對象,然后又從gray對象中刪除了對該white對象的引用,這樣會造成了該white對象漏標記。
- 對black新引用了一個white對象,然后從gray對象刪了一個引用該white對象的white對象,這樣也會造成了該white對象漏標記。
- 對black新引用了一個剛new出來的white對象,沒有其他gray對象引用該white對象,這樣也會造成了該white對象漏標記。
其實誤標沒什么關系,頂多造成浮動垃圾,在下次gc還是可以回收的,但是漏標的后果是致命的,把本應該存活的對象給回收了,從而影響的程序的正確性。
漏標的情況只會發生在白色對象中,且滿足以下任意一個條件:
- 並發標記時,應用線程給一個黑色對象的引用類型字段賦值了該白色對象
- 並發標記時,應用線程刪除所有灰色對象到該白色對象的引用
對於第一種情況,利用post-write barrier,記錄所有新增的引用關系,然后根據這些引用關系為根重新掃描一遍
對於第二種情況,利用pre-write barrier,將所有即將被刪除的引用關系的舊引用記錄下來,最后以這些舊引用為根重新掃描一遍
G1的收集模式:
YoungGC:收集年輕代里的Region
MixGC:年輕代的所有Region+全局並發標記階段選出的收益高的Region
無論是YoungGC還是MixGC都只是並發拷貝的階段
分代G1模式下選擇CSet有兩種子模式,分別對應YoungGC和mixedGC:
YoungGC:CSet就是所有年輕代里面的Region
MixedGC:CSet是所有年輕代里的Region加上在全局並發標記階段標記出來的收益高的Region
G1的運行過程是這樣的,會在Young GC和Mix GC之間不斷的切換運行,同時定期的做全局並發標記,在實在趕不上回收速度的情況下使用Full GC(Serial GC)。
初始標記是搭在YoungGC上執行的,在進行全局並發標記的時候不會做Mix GC,在做Mix GC的時候也不會啟動初始標記階段。當MixGC趕不上對象產生的速度的時候就退化成Full GC,這一點是需要重點調優的地方。
G1調優實戰:
1.不斷調優暫停時間指標:
通過XX:MaxGCPauseMillis=x可以設置啟動應用程序暫停的時間,G1在運行的時候會根據這個參數選擇CSet來滿足響應時間的設置。一般情況下這個值設置到100ms或者200ms都是可以的(不同情況下會不一樣),但如果設置成50ms就不太合理。暫停時間設置的太短,就會導致出現G1跟不上垃圾產生的速度。最終退化成Full GC。所以對這個參數的調優是一個持續的過程,逐步調整到最佳狀態。
2.不要設置新生代和老年代的大小
G1收集器在運行的時候會調整新生代和老年代的大小。通過改變代的大小來調整對象晉升的速度以及晉升年齡,從而達到我們為收集器設置的暫停時間目標。
設置了新生代大小相當於放棄了G1為我們做的自動調優。我們需要做的只是設置整個堆內存的大小,剩下的交給G1自己去分配各個代的大小。
3. 關注Evacuation Failure
Evacuation Failure類似於CMS里面的晉升失敗,堆空間的垃圾太多導致無法完成Region之間的拷貝,於是不得不退化成Full GC來做一次全局范圍內的垃圾收集 。
-XX:MaxGCPauseMillis=200m ##最大GC停頓時間是200m