Java虛擬機工做原理


Java虛擬機工做原理

  首先我想從宏觀上介紹一下Java虛擬機的工做原理。從最初的咱們編寫的Java源文件(.java文件)是如何一步步執行的,以下圖所示,首先Java源文件通過前端編譯器(javac或ECJ)將.java文件編譯爲Java字節碼文件,而後JRE加載Java字節碼文件,載入系統分配給JVM的內存區,而後執行引擎解釋或編譯類文件,再由即時編譯器將字節碼轉化爲機器碼。主要介紹下圖中的類加載器和運行時數據區兩個部分。前端

  • 類加載

  類加載指將類的字節碼文件(.class)中的二進制數據讀入內存,將其放在運行時數據區的方法區內,而後在堆上建立java.lang.Class對象,封裝類在方法區內的數據結構。類加載的最終產品是位於堆中的類對象,類對象封裝了類在方法區內的數據結構,而且向JAVA程序提供了訪問方法區內數據結構的接口。以下是類加載器的層次關係圖。java

    • 啓動類加載器(BootstrapClassLoader):在JVM運行時被建立,負責加載存放在JDK安裝目錄下的jre\lib的類文件,或者被-Xbootclasspath參數指定的路徑中,而且能被虛擬機識別的類庫(如rt.jar,全部的java.*開頭的類均被Bootstrap ClassLoader加載)。啓動類沒法被JAVA程序直接引用。
    • 擴展類加載器(Extension ClassLoader):該類加載器負責加載JDK安裝目錄下的\jre\lib\ext的類,或者由java.ext.dirs系統變量指定路徑中的全部類庫,開發者也能夠直接使用擴展類加載器。
    • 應用程序類加載器(AppClassLoader):負責加載用戶類路徑(Classpath)所指定的類,開發者能夠直接使用該類加載器,若是應用程序中沒有定義過本身的類加載器,該類加載器爲默認的類加載器。
    • 用戶自定義類加載器(User ClassLoader):JVM自帶的類加載器是從本地文件系統加載標準的java class文件,而自定義的類加載器能夠作到在執行非置信代碼以前,自動驗證數字簽名,動態地建立符合用戶特定須要的定製化構建類,從特定的場所(數據庫、網絡中)取得java class。

注意如上的類加載器並非經過繼承的方式實現的,而是經過組合的方式實現的。而JAVA虛擬機的加載模式是一種委派模式,如上圖中的1-7步所示。下層的加載器可以看到上層加載器中的類,反之則不行。類加載器能夠加載類可是不能卸載類。說了一大堆,仍是感受須要拿點代碼說事。算法

首先咱們先定義本身的類加載器MyClassLoader,繼承自ClassLoader,並覆蓋了父類的findClass(String name)方法,以下:數據庫

public class MyClassLoader extends ClassLoader{
    private String loaderName;  //類加載器名稱
    private String path = "";   //加載類的路徑
    private final String fileType = ".class";
    public MyClassLoader(String name){
        super();   //應用類加載器爲該類的父類
        this.loaderName = name;
    }
    public MyClassLoader(ClassLoader parent,String name){
        super(parent);
        this.loaderName = name;
    }
    public String getPath(){return this.path;}
    public void setPath(String path){this.path = path;}
    @Override
    public String toString(){return this.loaderName;}
    
    @Override
    public Class<?> findClass(String name) throws ClassNotFoundException{
        byte[] data = loaderClassData(name);
        return this.defineClass(name, data, 0, data.length);
    }
    //讀取.class文件
    private byte[] loaderClassData(String name){
        InputStream is = null;
        byte[] data = null;
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try {
            is = new FileInputStream(new File(path + name + fileType));
            int c = 0;
            while(-1 != (c = is.read())){
                baos.write(c);
            }
            data = baos.toByteArray();

        } catch (Exception e) {
            e.printStackTrace();
        } finally{
            try {
                if(is != null)
                    is.close();
                if(baos != null)
                    baos.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
        return data;
    }
}
View Code

  咱們如何利用咱們定義的類加載器加載指定的字節碼文件(.class)呢?如經過MyClassLoader加載C:\\Users\\Administrator\\下的Test.class字節碼文件,代碼以下所示:網絡

public class Client {
    public static void main(String[] args) {
        // TODO Auto-generated method stub        
        //MyClassLoader的父類加載器爲系統默認的加載器AppClassLoader
        MyClassLoader myCLoader = new MyClassLoader("MyClassLoader");
        //指定MyClassLoader的父類加載器爲ExtClassLoader
        //MyClassLoader myCLoader = new MyClassLoader(ClassLoader.getSystemClassLoader().getParent(),"MyClassLoader");
        myCLoader.setPath("C:\\Users\\Administrator\\");
        Class<?> clazz;
        try {
            clazz = myCLoader.loadClass("Test");
            Field[] filed = clazz.getFields();   //獲取加載類的屬性字段
            Method[] methods = clazz.getMethods();   //獲取加載類的方法字段
            System.out.println("該類的類加載器爲:" + clazz.getClassLoader());
            System.out.println("該類的類加載器的父類爲:" + clazz.getClassLoader().getParent());
            System.out.println("該類的名稱爲:" + clazz.getName());
        } catch (ClassNotFoundException e) {
            // TODO Auto-generated catch block
            e.printStackTrace();
        }
    }
}
  • 運行時數據區

  字節碼的加載第一步,其後分別是認證、準備、解析、初始化,那麼這些步驟又具體作了哪些工做,以及他們會對運行時數據區纏身什麼影響呢?以下圖所示:數據結構

  以下咱們將介紹運行時數據區,主要分爲方法區、Java堆、虛擬機棧、本地方法棧、程序計數器。其中方法區和Java堆同樣,是各個線程共享的內存區域,而虛擬機棧、本地方法棧、程序計數器是線程私有的內存區。ide

    1. Java堆:Java堆是Java虛擬機所管理的內存中最大的一塊,被進程的全部線程共享,在虛擬機啓動時被建立。該區域的惟一目的就是存放對象實例,幾乎全部的對象實例都在這裏分配內存,隨着JIT編譯器的發展與逃逸分支技術逐漸成熟,棧上分配、標量替換等優化技術使得對象在堆上的分配內存變得不是那麼「絕對」。Java堆是垃圾收集器管理的主要區域。因爲如今的收集器基本都採用分代收集算法,因此Java堆中還能夠分爲老年代和新生代(Eden、From Survivor、To Survivor)。根據Java虛擬機規範,Java堆能夠處於物理上不連續的內存空間,只要邏輯上連續便可。該區域的大小能夠經過-Xmx和-Xms參數來擴展,若是堆中沒有內存完成實例分配,而且堆也沒法擴展,將會拋出OutOfMemoryError異常。
    2. 方法區:用於存儲被Java虛擬機加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。不一樣於Java堆的是,Java虛擬機規範對方法區的限制很是寬鬆,能夠選擇不實現垃圾收集。但並不是數據進入了方法區就「永久」存在了,這區域內存回收目標主要是針對常量池的回收和對類型的卸載。若是該區域內存不足也會拋出OutOfMemoryError異常。
      • 常量池:這個名詞可能你們也常常見,它是方法區的一部分。Class文件除了有類的版本、字段、方法、接口等描述信息外,還有一項信息就是常量池,用於存放編譯期生成的各類字面量和符號引用。Java虛擬機運行期間,也可能將新的常量放入常量池(如String類的intern()方法)。
    3. 虛擬機棧:線程私有,生命週期與線程相同。虛擬機棧描述的是Java方法執行的內存模型:每一個方法在執行時都會建立一個棧幀用於存儲局部變量表、操做數棧、動態連接、方法出口等信息。每一個方法從調用直至執行完成的過程,就對應着一個棧幀在虛擬機棧中入棧到出棧的過程。若是請求的站深度大於虛擬機所容許的深度,將拋出StackOverflowError異常,虛擬機棧在動態擴展時若是沒法申請到足夠的內存,就會拋出OutOfMemoryError異常。
    4. 本地方法棧:與虛擬機棧相似,不過虛擬機棧是爲虛擬機執行Java方法(字節碼)服務,而本地方法棧則是爲虛擬機使用到的Native方法服務。該區域一樣會報StackOverflowError和OutOfMemoryError異常。
    5. 程序計數器:一塊較小的內存空間,能夠看做是當前線程所執行的字節碼的行號指示器。字節碼解釋器工做時就是經過改變這個計數器的值來選取下一條須要執行的字節碼指令,分支、循環、跳轉、異常處理、線程恢復等基礎功能都須要依賴這個計數器完成。若是線程正在執行一個Java方法,計數器記錄的是正在執行的虛擬機字節碼指令的地址,若是正在執行的是Native方法,這個計數器值爲空。此內存區域是惟一一個在Java虛擬機規範中沒有規定任何OutOfMemoryError狀況的區域。

  寫了這麼多,感受仍是少一個例子。經過最簡單的一段代碼解釋一下,程序在運行時數據區個部分的變化狀況。優化

public class Test{
      public static void main(String[] args){
           String name = "best.lei";
           sayHello(name);
       }
       public static void sayHello(String name){
           System.out.println("Hello " + name);
       }
}

  經過編譯器將Test.java文件編譯爲Test.class,利用javap -verbose Test.class對編譯後的字節碼進行分析,以下圖所示:this

  咱們在看看運行時數據區的變化:spa

相關文章
相關標籤/搜索