JVM2-類加載

歡迎你們關注 github.com/hsfxuebao/j… ,但願對你們有所幫助,要是以爲能夠的話麻煩給點一下Star哈html

轉自:www.cnblogs.com/xjwhaha/p/1…java

1. jvm內存結構概述

jvm運行,有哪些重要的 組件,以下圖git

共可分紅三個大類github

  • 將class 文件 加載到內存的 加載系統
  • class 存儲區域,程序運行時內存
  • jvm讀取class字節碼,執行解釋class命令的 執行引擎

下面挨個說明數據庫

2. 類加載子系統

當咱們把 代碼寫完,編譯成字節碼 class文件後,打包運行,接下來就是jvm的工做了,jvm經過類加載器ClassLoader完成 類加載的過程,bootstrap

class file存在於本地硬盤上,能夠理解爲設計師畫在紙上的模板,而最終這個模板在執行的時候是要加載到JVM當中來 , 根據這個文件實例化出n個如出一轍的實例。而這個模板就是咱們使用反射時常常看到的東西,Class對象,每一個類都對應一個此對象,存放類的元數據。api

示意圖:數組

整個類加載過程 可分紅三個部分,加載,連接(驗證 --> 準備 --> 解析) ,初始化安全

2.1 加載階段:

  1. 經過一個類的全限定名獲取定義此類的二進制字節流
  2. 將這個字節流所表明的靜態存儲結構轉化爲方法區的運行時數據結構
  3. 在內存中生成一個表明這個類的java.lang.Class對象,做爲方法區這個類的各類數據的訪問入口

加載class文件的方式可有如下幾種markdown

  1. 從本地系統中直接加載
  2. 經過網絡獲取,典型場景:Web Applet
  3. 從zip壓縮包中讀取,成爲往後jar、war格式的基礎
  4. 運行時計算生成,使用最多的是:動態代理技術
  5. 由其餘文件生成,典型場景:JSP應用
  6. 從專有數據庫中提取.class文件,比較少見
  7. 從加密文件中獲取,典型的防Class文件被反編譯的保護措施

2.2 連接階段:

連接分爲三個子階段:驗證 --> 準備 --> 解析

驗證(Verify):

目的在於確保Class文件的字節流中包含信息符合當前虛擬機要求,保證被加載類的正確性,不會危害虛擬機自身安全。主要包括四種驗證,文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證。

例如:字節碼文件(BinaryViewer查看),其開頭均爲 CAFE BABE ,若是出現不合法的字節碼文件,那麼將會驗證不經過

準備(Prepare)

  1. 爲類變量(靜態變量)分配內存而且設置該類變量的默認初始值
  2. 這裏不包含用final修飾的static,由於final在編譯的時候就會分配好了默認值,準備階段會顯式初始化
  3. 也不會爲實例變量(對象類型)分配初始化,普通類變量會分配在方法區中,而實例變量是會隨着對象一塊兒分配到Java堆中

舉例:

public class HelloApp {
    private static int a = 1;   //變量a在準備階段會賦初始值,但不是1,而是0,在初始化階段會被賦值爲 1

    public static void main(String[] args) {
        System.out.println(a);
    }
}
複製代碼

解析(Resolve)

  1. 將常量池內的符號引用轉換爲直接引用的過程
  2. 事實上,解析操做每每會伴隨着JVM在執行完初始化以後再執行
  3. 符號引用就是一組符號來描述所引用的目標。符號引用的字面量形式明肯定義在《java虛擬機規範》的class文件格式中。直接引用就是直接指向目標的指針、相對偏移量或一個間接定位到目標的句柄
  4. 解析動做主要針對類或接口、字段、類方法、接口方法、方法類型等。對應常量池中的CONSTANT Class info、CONSTANT Fieldref info、CONSTANT Methodref info等

2.3 初始化階段:

  1. 初始化階段就是執行類構造器方法()的過程
  2. 此方法不需定義,是javac編譯器自動收集類中的全部類變量(靜態變量)的賦值動做和靜態代碼塊中的語句合併而來。也就是說,當咱們代碼中包含static變量的時候,就會有clinit方法(沒有static變量則沒有)

舉例:

public class ClinitTest {
    private int a = 1;
    private static int c = 3;
    
    public static void main(String[] args) {
        int b = 2;
    }
}
複製代碼

類中有靜態變量生成 clinit 方法

若沒有static變量

public class ClinitTest {
    private int a = 1;

    public static void main(String[] args) {
        int b = 2;
    }
}
複製代碼

則沒有clinit方法

  1. ()方法中的指令按賦值語句在源文件中出現的順序執行

舉例:

public class ClassInitTest {
    private static int number = 10;      //linking之prepare: number = 0 --> initial: 10 --> 20

    static {
        number = 20;
        System.out.println(num);
    }

    public static void main(String[] args) {
        System.out.println(ClassInitTest.num);//2
        System.out.println(ClassInitTest.number);//10
    }
}
複製代碼

靜態變量 number 的值變化過程以下

  • 準備階段時:0
  • 執行靜態變量初始化:10
  • 執行靜態代碼塊:20

  1. <clinit>()不一樣於類的構造器。(關聯:構造器是虛擬機視角下的<init>(),)
  2. 若該類具備父類,JVM會保證子類的<clinit>()執行前,父類的<clinit>()已經執行完畢
  3. 虛擬機必須保證一個類的<clinit>()方法在多線程下被同步加鎖(多個線程同時加載同一個類)

回到頂部

3. 類加載器的分類

JVM支持兩種類型的類加載器 。分別爲引導類加載器(Bootstrap ClassLoader)和自定義類加載器(User-Defined ClassLoader )

從概念上來說,自定義類加載器通常指的是程序中由開發人員自定義的一類類加載器,可是Java虛擬機規範卻沒有這麼定義,而是將全部派生於抽象類ClassLoader的類加載器都劃分爲自定義類加載器

不管類加載器的類型如何劃分,在程序中咱們最多見的類加載器始終只有3個,這裏的四者之間是包含關係,不是上層和下層,也不是子父類的繼承關係。 以下所示

代碼:

public class ClassLoaderTest {
    public static void main(String[] args) {

        //獲取系統類加載器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

        //獲取其上層:擴展類加載器
        ClassLoader extClassLoader = systemClassLoader.getParent();
        System.out.println(extClassLoader);//sun.misc.Launcher$ExtClassLoader@1540e19d

        //獲取其上層:獲取不到引導類加載器
        ClassLoader bootstrapClassLoader = extClassLoader.getParent();
        System.out.println(bootstrapClassLoader);//null

        //對於用戶自定義類來講:默認使用系統類加載器進行加載
        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println(classLoader);//sun.misc.Launcher$AppClassLoader@18b4aac2

        //String類使用引導類加載器進行加載的。---> Java的核心類庫都是使用引導類加載器進行加載的。
        ClassLoader classLoader1 = String.class.getClassLoader();
        System.out.println(classLoader1);//null

    }
}
複製代碼
  • 嘗試獲取引導類加載器,獲取到的值爲 null ,這並不表明引導類加載器不存在,由於引導類加載器是由 C/C++ 語言編寫,咱們獲取不到
  • 兩次獲取系統類加載器的值都相同:sun.misc.Launcher$AppClassLoader@18b4aac2 ,這說明系統類加載器是全局惟一的

經常使用加載器介紹:

啓動類加載器(引導類加載器,Bootstrap ClassLoader)

  1. 這個類加載使用C/C++語言實現的,嵌套在JVM內部
  2. 它用來加載Java的核心庫,用於提供JVM自身須要的類
  3. 並不繼承自java.lang.ClassLoader,沒有父加載器
  4. 加載擴展類(Extension ClassLoader )和應用程序類(AppClassLoade )加載器,並做爲他們的父類加載器(這兩個加載器,也是java類)
  5. 出於安全考慮,Bootstrap啓動類加載器只加載包名爲java、javax、sun等開頭的類

擴展類加載器(Extension ClassLoader)

  1. Java語言編寫,由sun.misc.Launcher$ExtClassLoader實現
  2. 派生於ClassLoader類(則爲自定義加載器)
  3. 父類加載器爲啓動類加載器
  4. 從java.ext.dirs系統屬性所指定的目錄中加載類庫,或從JDK的安裝目錄的jre/lib/ext子目錄(擴展目錄)下加載類庫。若是用戶建立的JAR放在此目錄下,也會自動由擴展類加載器加載

應用程序類加載器(系統類加載器,AppClassLoader)

  1. Java語言編寫,由sun.misc.LaunchersAppClassLoader實現
  2. 派生於ClassLoader類(則爲自定義加載器)
  3. 父類加載器爲擴展類加載器
  4. 它負責加載環境變量classpath或系統屬性java.class.path指定路徑下的類庫
  5. 該類加載是程序中默認的類加載器,通常來講,Java應用的類都是由它來完成加載
  6. 經過classLoader.getSystemclassLoader()方法能夠獲取到該類加載器

代碼:

public class ClassLoaderTest1 {
    public static void main(String[] args) {

        System.out.println("**********啓動類加載器**************");
        //獲取BootstrapClassLoader可以加載的api的路徑
        URL[] urLs = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        for (URL element : urLs) {
            System.out.println(element.toExternalForm());
        }
        //從上面的路徑中隨意選擇一個類,來看看他的類加載器是什麼:引導類加載器
        ClassLoader classLoader = Provider.class.getClassLoader();
        System.out.println(classLoader);//null

        System.out.println("***********擴展類加載器*************");
        String extDirs = System.getProperty("java.ext.dirs");
        for (String path : extDirs.split(";")) {
            System.out.println(path);
        }

        //從上面的路徑中隨意選擇一個類,來看看他的類加載器是什麼:擴展類加載器
        ClassLoader classLoader1 = CurveDB.class.getClassLoader();
        System.out.println(classLoader1);//sun.misc.Launcher$ExtClassLoader@1540e19d

    }
}
複製代碼

打印結果

String 類就在 其中的rt.jar中, 由 啓動類加載器加載

擴展類加載器加載 lib下的 ext下的jar包類

**********啓動類加載器**************
file:/C:/Program%20Files/Java/jdk1.8.0_144/jre/lib/resources.jar
file:/C:/Program%20Files/Java/jdk1.8.0_144/jre/lib/rt.jar
file:/C:/Program%20Files/Java/jdk1.8.0_144/jre/lib/sunrsasign.jar
file:/C:/Program%20Files/Java/jdk1.8.0_144/jre/lib/jsse.jar
file:/C:/Program%20Files/Java/jdk1.8.0_144/jre/lib/jce.jar
file:/C:/Program%20Files/Java/jdk1.8.0_144/jre/lib/charsets.jar
file:/C:/Program%20Files/Java/jdk1.8.0_144/jre/lib/jfr.jar
file:/C:/Program%20Files/Java/jdk1.8.0_144/jre/classes
null
***********擴展類加載器*************
C:\Program Files\Java\jdk1.8.0_144\jre\lib\ext
C:\WINDOWS\Sun\Java\lib\ext
sun.misc.Launcher$ExtClassLoader@7ea987ac
複製代碼

回到頂部

4. 用戶自定義加載器

注意,此爲用戶自定義,不是jvm規範中的 自定義 加載器,此處概述 ,後面詳細介紹

爲何須要自定義類加載器?

  1. 隔離加載類(不用的中間件,框架中的類隔離)
  2. 修改類加載的方式
  3. 擴展加載源(從特殊的環境中加載class信息)
  4. 防止源碼泄漏(class 加密,自定義加載器類 進行解密)

如何自定義類加載器?

  1. 開發人員能夠經過繼承抽象類java.lang.ClassLoader類的方式,實現本身的類加載器,以知足一些特殊的需求
  2. 在JDK1.2以前,在自定義類加載器時,總會去繼承ClassLoader類並重寫loadClass()方法,從而實現自定義的類加載類,可是在JDK1.2以後已再也不建議用戶去覆蓋loadClass()方法,而是建議把自定義的類加載邏輯寫在findclass()方法中
  3. 在編寫自定義類加載器時,若是沒有太過於複雜的需求,能夠直接繼承URIClassLoader類,這樣就能夠避免本身去編寫findclass()方法及其獲取字節碼流的方式,使自定義類加載器編寫更加簡潔。

回到頂部

5. ClassLoader 經常使用方法

ClassLoader類,它是一個抽象類,其後全部的類加載器都繼承自ClassLoader(不包括啓動類加載器)而且都定義在sun.misc.Launcher 類中,都是其子類,sun.misc.Launcher 它是一個java虛擬機的入口應用

經常使用方法:

方法名稱

描述

getParent()

返回該類加載器的父類加載器

loadClass(String name)

加載名稱爲name的類,返回結果爲java.lang.Class類的實例

findClass(String name)

查找名稱爲name的類,返回結果爲java.lang.Class類的實例

findLoadedClass(String name)

查找名稱爲name的已經被加載的類,返回結果爲java.lang.Class類的實例

defineClass(String name,byte[] b,int off,int len)

把字節數組b中的內容轉換爲一個Java類,返回結果爲java.lang.Class類的實例

resolveClass(Class<?> c)

鏈接指定的一個Java類

獲取ClassLoader 實例的途徑

  • 獲取當前類的 ClassLoader :clazz.getClassLoader()
  • 獲取當前線程上下文的ClassLoader:Thread.currentThread().getContextClassLoader()
  • 獲取系統的ClassLoader : ClassLoader.getSystemClassLoader();
  • 獲取調用者的ClassLoader : DriverManager.getCallerClassLoader()

代碼演示:

public class ClassLoaderTest2 {
    public static void main(String[] args) {
        try {
            
            //1.Class.forName().getClassLoader()
            ClassLoader classLoader = Class.forName("java.lang.String").getClassLoader();
            System.out.println(classLoader); // String 類由啓動類加載器加載,咱們沒法獲取

            //2.Thread.currentThread().getContextClassLoader()
            ClassLoader classLoader1 = Thread.currentThread().getContextClassLoader();
            System.out.println(classLoader1);

            //3.ClassLoader.getSystemClassLoader().getParent()
            ClassLoader classLoader2 = ClassLoader.getSystemClassLoader();
            System.out.println(classLoader2);

        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}

// 輸出
null
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$AppClassLoader@18b4aac2
複製代碼

回到頂部

6. 雙親委派機制

Java虛擬機對class文件採用的是按需加載的方式,也就是說當須要使用該類時纔會將它的class文件加載到內存生成class對象。並且加載某個類的class文件時,Java虛擬機採用的是雙親委派模式,即把請求交由父類處理(父類再向上委派,一直到啓動類加載器),它是一種任務委派模式

  1. 若是一個類加載器收到了類加載請求,它並不會本身先去加載,而是把這個請求委託給父類的加載器去執行;
  2. 若是父類加載器還存在其父類加載器,則進一步向上委託,依次遞歸,請求最終將到達頂層的啓動類加載器;
  3. 若是父類加載器能夠完成類加載任務,就成功返回,假若父類加載器沒法完成此加載任務,子加載器纔會嘗試本身去加載,這就是雙親委派模式。
  4. 父類加載器一層一層往下分配任務,若是子類加載器能加載,則加載此類,若是將加載任務分配至應用程序加載器也沒法加載此類,則拋出異常

舉例:

本身創建一個java.lang.String類,若是此類加載 將打印 語句

package java.lang;

public class String {
    static{
        System.out.println("我是自定義的String類的靜態代碼塊");
    }
}
複製代碼

在另外的程序中加載 String 類,上面的語句並無打印,默認加載就是 原生的String類

public class StringTest {

    public static void main(String[] args) {
        java.lang.String str = new java.lang.String();
        System.out.println("hello,atguigu.com");

        StringTest test = new StringTest();
        System.out.println(test.getClass().getClassLoader());
    }
}
複製代碼

由於 當程序須要加載 java.lang.String 類時,類加載器將一直向上委託, 直到 啓動器加載類,而啓動器加載能夠加載String 類,則爲 原生java.lang下的String 類,這將保證原生api的安全性,不會由於用戶環境重寫的類,致使全部之前使用原生api的地方受到干擾

代碼2

package java.lang;

public class String {
    static{
        System.out.println("我是自定義的String類的靜態代碼塊");
    }
    //錯誤: 在類 java.lang.String 中找不到 main 方法
    public static void main(String[] args) {
        System.out.println("hello,String");
    }
}
複製代碼

運行時報錯:

當加載main方法時, 加載其承載類String,一樣委託到啓動器加載類,致使加載成原生的String 類,而原生的String 類可沒有 main方法

代碼3

在自定義的java.lang 包下定義自定義的類

package java.lang;

public class ShkStart {
    public static void main(String[] args) {
        System.out.println("hello!");
    }
}
複製代碼

運行報錯

由於報名爲java.lang 屬於啓動類加載器的加載範疇, 出於對 啓動類加載器的保護,jvm禁止 自定義的類被 啓動類加載器加載,防止自定義類 對啓動器加載器產生破壞,

經過上面的例子,咱們能夠知道,雙親機制能夠

  1. 避免類的重複加載
  2. 保護程序安全,防止核心API被隨意篡改
    1. 自定義類:java.lang.String 沒有用
    2. 自定義類:java.lang.ShkStart(報錯:阻止建立 java.lang開頭的類)

當咱們使用 jdbc 等 第三方 實現類jar包時,使用到ClassLoader

  1. 首先咱們須要知道的是 jdbc.jar是基於SPI接口進行實現的
  2. 因此在加載的時候,會進行雙親委派,最終從根加載器中加載 SPI核心類,而後再加載SPI接口類
  3. 接着在進行反向委託,經過線程上下文類加載器進行實現類 jdbc.jar的加載。

回到頂部

7. 沙箱安全機制

自定義String類時:在加載自定義String類的時候會率先使用引導類加載器加載,而引導類加載器在加載的過程當中會先加載jdk自帶的文件(rt.jar包中java.lang.String.class),報錯信息說沒有main方法,就是由於加載的是rt.jar包中的String類。這樣能夠對java核心源代碼的保護,保證java核心代碼的運行環境絕對的獨立,這就是沙箱安全機制

相關問題:

如何判斷兩個class對象是否相同?

在JVM中表示兩個class對象是否爲同一個類存在兩個必要條件:

  1. 類的完整類名必須一致,包括包名
  2. 加載這個類的ClassLoader(指ClassLoader實例對象)必須相同
  3. 換句話說,在JVM中,即便這兩個類對象(class對象)來源同一個Class文件,被同一個虛擬機所加載,但只要加載它們的ClassLoader實例對象不一樣,那麼這兩個類對象也是不相等的

類的主動使用和被動使用

Java程序對類的使用方式分爲:主動使用和被動使用。主動使用,又分爲七種狀況:

  1. 建立類的實例
  2. 訪問某個類或接口的靜態變量,或者對該靜態變量賦值
  3. 調用類的靜態方法
  4. 反射(好比:Class.forName(「com.atguigu.Test」))
  5. 初始化一個類的子類
  6. Java虛擬機啓動時被標明爲啓動類的類
  7. JDK7開始提供的動態語言支持:java.lang.invoke.MethodHandle實例的解析結果REF_getStatic、REF putStatic、REF_invokeStatic句柄對應的類沒有初始化,則初始化

除了以上七種狀況,其餘使用Java類的方式都被看做是對類的被動使用,都不會致使類的初始化,即不會執行初始化階段(不會調用 clinit() 方法和 init() 方法)

相關文章
相關標籤/搜索