關於類的加載:java
Java虛擬機與程序的生命週期:linux
在以下幾種狀況下,Java虛擬機將會結束生命週期:程序員
類的加載、鏈接與初始化:算法
加載:查找並加載類的二進制數據數據庫
鏈接: api
注:
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程序「首次主動使用」時才初始化他們。即初始化只會執行一次。
主動使用,七種(非精確劃分,大致劃分):
好比: class Parent{} class Child extends Parent{} 初始化Child時候,先去初始化Parent
Java虛擬機啓動時候,被標明爲啓動的類,即爲有main方法的類,也會主動使用
注:
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文件的方式:
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 類的全路徑名字
助記符引伸:
助記符是在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是反射的入口。像一面鏡子同樣。
有兩種類型的類加載器:
1.Java虛擬機自帶的加載器
2.用戶自定義的類加載器
類的加載:
類加載器並不須要等到某個類被「首次主動使用」時候再加載它
注:
類的驗證:
類被加載後,就進入鏈接階段。鏈接就是將已經讀入到內存中的類的二進制數據合併到虛擬機的運行時的環境中去。
類的驗證的內容:
在準備階段:
初始化階段:
類的初始化步驟:
只有當程序訪問的靜態變量或靜態方法確實在當前類或當前接口定義時,才能夠認爲是對類或接口的主動使用。
調用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修飾後,哪一個類去主動調用就將這個常量放入到本身類的常量池裏面。
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)"); } }
總結:
類加載器的雙親委派機制:
在雙親委派機制中,各個加載器按照父子關係造成了樹形結構,除了根類加載器以外,其他的類加載器都有且只有一個父類加載器。
若是有一個類加載器可以成功加載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(); } }
總結:
看下面例子:
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); } }
總結:
關於雙親委派機制:
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()); } }
總結:
本身定義類加載器,看下面例子:
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目錄下面找,找到了加載之。
關於命名空間:
關於類的卸載:
加載 <----> 卸載
看下面的例子:
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。執行報錯。
緣由
雙親委派的好處:
知識總結:
簡單看下:
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()
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還沒有被加載)
線程上下文類加載器:
線程上下文類加載器的重要性:
應用場景:
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類所對應的常量池主要由常量池數量與常量池數組(常量表)這兩部分共同組成。常量池數量緊跟在主版本後面,佔據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的做用是保存該方法的結構,如所對應的字節碼
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)
堆
方法區:
看下面例子:
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來回收