深刻理解JVM虛擬機

  • JVM平臺上還能夠運行其餘語言,運行的是Class字節碼。只要能翻譯成Class的語言就OK了。挺強大的。
  • JVM廠商不少
  • 垃圾收集器、收集算法
  • JVM檢測工具

 

關於類的加載:java

  •  Java代碼中,類型(interface, class,enum等,有些是在運行時候生成的,好比動態代理)的加載、鏈接與初始化過程都是在程序運行期間完成的。不涉及到對象的概念。同時也是個Runtime階段。
  •  提供了更大的靈活性,增長了更多的可能性。提供了一些擴展,靈活擴展。

    

Java虛擬機與程序的生命週期:linux

  在以下幾種狀況下,Java虛擬機將會結束生命週期:程序員

  1. 執行了System.exit()方法
  2. 程序正常執行結束
  3. 程序執行過程遇到了異常或者錯誤異常終止了
  4. 操做系統出現錯誤致使Java虛擬機進行終止

 

類的加載、鏈接與初始化:算法

加載:查找並加載類的二進制數據數據庫

鏈接: api

  • 驗證: 確保被加載類的正確性。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程序對類的使用方式分爲兩種:

  1. 主動使用
  2. 被動使用

 

全部的Java虛擬機實現必須在每一個類或接口被Java程序「首次主動使用」時才初始化他們。即初始化只會執行一次。

 

主動使用,七種(非精確劃分,大致劃分):

  1. 建立類的實例。
  2. 訪問某個類或接口的靜態變量,或者對靜態變量賦值。 字節碼層面上,使用的助記符:get static、  put static
  3. 調用類的靜態方法。 invoke static
  4. 反射(如Class.forName("com.test.t1"))
  5. 初始化一個類的子類
    好比:
    
     class Parent{}
     class Child extends Parent{}
    
    初始化Child時候,先去初始化Parent 
  6. Java虛擬機啓動時被代表爲敵情類的類(Java Test)
    Java虛擬機啓動時候,被標明爲啓動的類,即爲有main方法的類,也會主動使用 
  7. 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文件的方式:

  1. 從本地系統中直接加載
  2. 經過網絡下載
  3. 從zip、jar等貴方文件中加載
  4. 從轉悠數據庫中提取
  5. 將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是一個啓動類,主動使用。先加載之。 

 

總結: 

  1. 對於靜態字段來講,只有直接定義了該字段的類纔會被初始化。
  2. 當一個類在初始化時候,要求其父類所有已經初始化完畢。每一個父類最多隻能初始化一次! 

 

引伸: -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)");
    }
}

 

 

 

總結:

  1.  先看是不是finanl修飾,是的話,就不用加載別的類。前提是編譯器的。
  2. 再看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 沒有任何參數。調用無參構造方法

    }
}

 

 

 關於命名空間的說明: 

  1.   子加載器加載的類,可以訪問父加載器加載的類。  
  2.   父加載器加載的類,不能訪問子加載器加載的類。

 

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中存在兩個命名空間。
  •    不一樣命名空間的類不可見,引用不到就報錯。(子加載器的命名空間包含全部父加載器的命名空間,子可看到父類加載的全部類。)

 

 

雙親委派的好處:

  1.    能夠確保Java核心庫的類型安全,全部的Java應用都至少會引用java.lang.Object類,也就是說在運行期,java.lang.Object這個類會被加載到Java虛擬機中。
  2.    若是這個加載過程是由Java應用本身的類加載器所完成的,那麼極可能就會在JVM彙總存在多個版本的java.lang.Object類。並且這些類之間是不兼容,相互不可見的(命名空間)。
  3.    藉助雙親委派機制,java核心類庫中的類的加載工做都是啓動類加載器統一完成的。確保了Java應用所使用的都是同一個版本的Java核心類庫,他們之間是相互兼容的。Java核心類庫不會被自定義的替代。啓動類去加載之。
  4.    不一樣的類加載器能夠爲相同名稱(binary name)的類建立額外的命名空間,相同名稱的類能夠並存在Java虛擬機中,只須要不用的類加載器(包括沒有父子關係、不一樣類加載器)來加載他們便可。不一樣類加載器所加載的類是不兼容的。這就至關於在Java虛擬機內部建立了一個又一個相互隔離的       Java類空間,這類技術在不少框架中都獲得了實際應用。

 

知識總結: 

  1. 關於擴展類加載器:須要作成jar包,再放到指定目錄下。
  2. 在運行期,一個Java類是由該類的徹底限定名(binary name, 二進制名)和用於加載該類的定義列類加載器(defing loader)所共同決定的。若是一樣名字(即相同的徹底限定名)的類是由兩個不一樣的加載器所加載,那麼這些類就是不一樣的。即使 .class文件的字節碼徹底同樣,而且從相同的位置加載亦如此。
  3. 在Oracle的hotSopt實現中,系統屬性sun.boot.class.path若是修改錯了,則運行會報錯,提示: Error occurred during Initialization of VM
  4. 內建於JVM中的啓動類加載器會加載java.lang.ClassLoader以及其餘的Java平臺類,當JVM啓動時候,一塊特殊的機器碼會運行,他會擴展類加載器與系統類加載器,這塊特殊的機器碼叫作啓動類加載器(BootStrap)。
  5. 啓動類加載器並非Java類,而其餘的加載器都是Java類。啓動類加載器是特定於平臺的機器指令,它負責開啓整個加載過程。
  6. 全部類加載器(除啓動類加載器)都被實現爲Java類。不過,總歸要有一個組件來加載第一個Java類加載器,從而讓整個加載過程可以順利進行下去,加載第一個純Java類加載器就是啓動類加載器的職責。
  7. 啓動類加載器還會負責加載供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 引用。

元空間虛擬機負責元空間的分配,其採用的形式爲組塊分配。組塊的大小因類加載器的類型而異。在元空間虛擬機中存在一個全局的空閒組塊列表。當一個類加載器須要組塊時,它就會從這個全局的組塊列表中獲取並維持一個本身的組塊列表。當一個類加載器再也不存活,那麼其持有的組塊將會被釋放,並返回給全局組塊列表。類加載器持有的組塊又會被分紅多個塊,每個塊存儲一個單元的元信息。組塊中的塊是線性分配(指針碰撞分配形式)。組塊分配自內存映射區域。這些全局的虛擬內存映射區域以鏈表形式鏈接,一旦某個虛擬內存映射區域清空,這部份內存就會返回給操做系統。

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來回收

相關文章
相關標籤/搜索