JVM解毒——類加載子系統

帶着問題,尤爲是面試問題的學習纔是最高效的。加油,奧利給!java

點贊+收藏 就學會系列,文章收錄在 GitHub JavaEgg ,N線互聯網開發必備技能兵器譜git

直擊面試

  1. 看你簡歷寫得熟悉JVM,那你說說類的加載過程吧?
  2. 咱們能夠自定義一個String類來使用嗎?
  3. 什麼是類加載器,類加載器有哪些?
  4. 多線程的狀況下,類的加載爲何不會出現重複加載的狀況?
  5. 什麼是雙親委派機制?它有啥優點?能夠打破這種機制嗎?


類加載子系統

類加載機制概念

Java虛擬機把描述類的數據從Class文件加載到內存,並對數據進行校驗、轉換解析和初始化,最終造成能夠被虛擬機直接使用的Java類型,這就是虛擬機的加載機制。Class文件由類裝載器裝載後,在JVM中將造成一份描述Class結構的元信息對象,經過該元信息對象能夠獲知Class的結構信息:如構造函數,屬性和方法等,Java容許用戶藉由這個Class相關的元信息對象間接調用Class對象的功能,這裏就是咱們常常能見到的Class類。github

類加載子系統做用

  • 類加載子系統負責從文件系統或者網絡中加載class文件,class文件在文件開頭有特定的文件標識(0xCAFEBABE)
  • ClassLoader只負責class文件的加載。至於它是否能夠運行,則由Execution Engine決定
  • 加載的類信息存放於一塊稱爲方法區的內存空間。除了類的信息外,方法區中還存放運行時常量池信息,可能還包括字符串字面量和數字常量(這部分常量信息是class文件中常量池部分的內存映射)
  • Class對象是存放在堆區的

類加載器ClassLoader角色

  1. class file存在於本地硬盤上,能夠理解爲設計師畫在紙上的模板,而最終這個模板在執行的時候是要加載到JVM當中來根據這個文件實例化出n個如出一轍的實例
  2. class file加載到JVM中,被稱爲DNA元數據模板,放在方法區
  3. 在.calss文件 -> JVM -> 最終成爲元數據模板,此過程就要一個運輸工具(類裝載器),扮演一個快遞員的角色

類加載過程

類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載、驗證、準備、解析、初始化、使用和卸載七個階段。(驗證、準備和解析又統稱爲鏈接,爲了支持Java語言的運行時綁定,因此解析階段也能夠是在初始化以後進行的。以上順序都只是說開始的順序,實際過程當中是交叉的混合式進行的,加載過程當中可能就已經開始驗證了)面試

jvm-class-load

1. 加載(Loading):

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

加載 .calss 文件的方式數據庫

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

2. 鏈接(Linking)

驗證(Verify)

  • 目的在於確保Class文件的字節流中包含信息符合當前虛擬機要求,保證被加載類的正確性,不會危害虛擬機自身安全bootstrap

  • 主要包括四種驗證,文件格式驗證,元數據驗證,字節碼驗證,符號引用驗證api

準備(Prepare)

  • 爲類變量分配內存而且設置該類變量的默認初始值,即零值數組

    數據類型 零值
    int 0
    long 0L
    short (short)0
    char '\u0000'
    byte (byte)0
    boolean false
    float 0.0f
    double 0.0d
    reference null
  • 這裏不包含用final修飾的static,由於final在編譯的時候就會分配了,準備階段會顯示初始化tomcat

  • 這裏不會爲實例變量分配初始化,類變量會分配在方法區中,而實例變量是會隨着對象一塊兒分配到Java堆中安全

    private static int i = 1;  //變量i在準備階只會被賦值爲0,初始化時纔會被賦值爲1
    private final static int j = 2;  //這裏被final修飾的變量j,直接成爲常量,編譯時就會被分配爲2
    複製代碼

解析(Resolve)

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

3. 初始化(Initialization)

  • 初始化階段就是執行類構造器方法<clinit>()的過程
  • 此方法不須要定義,是javac編譯器自動收集類中的全部類變量的賦值動做和靜態代碼塊中的語句合併而來
  • 構造器方法中指令按語句在源文件中出現的順序執行
  • <clinit>()不一樣於類的構造器(構造器是虛擬機視角下的<init>())
  • 若該類具備父類,JVM會保證子類的<clinit>()執行前,父類的<clinit>()已經執行完畢
  • 虛擬機必須保證一個類的<clinit>()方法在多線程下被同步加鎖
public class ClassInitTest{
  private static int num1 = 30;
  static{
    num1 = 10;
    num2 = 10;     //num2寫在定義變量以前,爲何不會報錯呢??
    System.out.println(num2);   //這裡直接打印能夠嗎? 報錯,非法的前向引用,能夠賦值,但不可調用
  }
  private static int num2 = 20;  //num2在準備階段就被設置了默認初始值0,初始化階段又將10改成20
  public static void main(String[] args){
    System.out.println(num1);  //10
    System.out.println(num2);   //20
  }
}
複製代碼

類的主動使用和被動使用

Java程序對類的使用方式分爲:主動使用和被動使用。虛擬機規範規定有且只有5種狀況必須當即對類進行「初始化」,即類的主動使用。

  • 建立類的實例、訪問某個類或接口的靜態變量,或者對該靜態變量賦值、調用類的靜態方法(即遇到new、getstatic、putstatic、invokestatic這四條字節碼指令時)
  • 反射
  • 初始化一個類的子類
  • Java虛擬機啓動時被標明爲啓動類的類
  • JDK7 開始提供的動態語言支持:java.lang.invoke.MethodHandle實例的解析結果,REF_getStaticREF_putStaticREF_invokeStatic句柄對應的類沒有初始化,則初始化

除以上五種狀況,其餘使用Java類的方式被看做是對類的被動使用,都不會致使類的初始化

eg:

public class NotInitialization {
    public static void main(String[] args) { 
        //只輸出SupperClass int 123,不會輸出SubClass init
        //對於靜態字段,只有直接定義這個字段的類纔會被初始化
        System.out.println(SubClass.value); 
    }
}

class SuperClass {
    static {
        System.out.println("SupperClass init");
    }
    public static int value = 123;
}

class SubClass extends SuperClass {
    static {
        System.out.println("SubClass init");
    }
}
複製代碼

類加載器

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

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

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

  • 這個類加載使用C/C++ 語言實現,嵌套在JVM 內部
  • 它用來加載Java的核心庫(JAVA_HOME/jre/lib/rt.jarresource.jarsun.boot.class.path路徑下的內容),用於提供JVM自身須要的類
  • 並不繼承自 java.lang.ClassLoader,沒有父加載器
  • 加載擴展類和應用程序類加載器,並指定爲他們的父類加載器
  • 出於安全考慮,Boostrap 啓動類加載器只加載名爲java、Javax、sun等開頭的類

擴展類加載器(Extension ClassLoader)

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

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

  • java語言編寫,由 sun.misc.Lanucher$AppClassLoader 實現
  • 派生於 ClassLoader
  • 父類加載器爲擴展類加載器
  • 它負責加載環境變量classpath或系統屬性java.class.path 指定路徑下的類庫
  • 該類加載是程序中默認的類加載器,通常來講,Java應用的類都是由它來完成加載的
  • 經過 ClassLoader#getSystemClassLoader() 方法能夠獲取到該類加載器
public class ClassLoaderTest {
    public static void main(String[] args) {
        //獲取系統類加載器
        ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
        System.out.println(systemClassLoader);  //sun.misc.Launcher$AppClassLoader@135fbaa4

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

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

        //對於用戶自定義類來講,默認使用系統類加載器進行加載,輸出和systemClassLoader同樣
        ClassLoader classLoader = ClassLoaderTest.class.getClassLoader();
        System.out.println(classLoader);  //sun.misc.Launcher$AppClassLoader@135fbaa4

        //String 類使用引導類加載器進行加載。Java的核心類庫都使用引導類加載器進行加載,因此也獲取不到
        ClassLoader classLoader1 = String.class.getClassLoader();
        System.out.println(classLoader1);  //null

        //獲取BootstrapClassLoader能夠加載的api的路徑
        URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();
        for (URL url : urls) {
            System.out.println(url.toExternalForm());
        }
    }
}
複製代碼

用戶自定義類加載器

在Java的平常應用程序開發中,類的加載幾乎是由3種類加載器相互配合執行的,在必要時,咱們還能夠自定義類加載器,來定製類的加載方式

爲何要自定義類加載器?
  • 隔離加載類
  • 修改類加載的方式
  • 擴展加載源(能夠從數據庫、雲端等指定來源加載類)
  • 防止源碼泄露(Java代碼容易被反編譯,若是加密後,自定義加載器加載類的時候就能夠先解密,再加載)
用戶自定義加載器實現步驟
  1. 開發人員能夠經過繼承抽象類 java.lang.ClassLoader 類的方式,實現本身的類加載器,以知足一些特殊的需求
  2. 在JDK1.2以前,在自定義類加載器時,總會去繼承ClassLoader類並重寫loadClass()方法,從而實現自定義的類加載類,可是JDK1.2以後已經不建議用戶去覆蓋loadClass()方式,而是建議把自定義的類加載邏輯寫在findClass()方法中
  3. 編寫自定義類加載器時,若是沒有太過於複雜的需求,能夠直接繼承URLClassLoader類,這樣就能夠避免本身去編寫findClass()方法及其獲取字節碼流的方式,使自定義類加載器編寫更加簡潔

ClassLoader經常使用方法

ClassLoader類,是一個抽象類,其後全部的類加載器都繼承自ClassLoader(不包括啓動類加載器)

方法 描述
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類

對類加載器的引用

JVM必須知道一個類型是由啓動加載器加載的仍是由用戶類加載器加載的。若是一個類型是由用戶類加載器加載的,那麼JVM會將這個類加載器的一個引用做爲類型信息的一部分保存在方法區中。當解析一個類型到另外一個類型的引用的時候,JVM須要保證這兩個類型的類加載器是相同的。


雙親委派機制

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

工做過程

  • 若是一個類加載器收到了類加載請求,它並不會本身先去加載,而是把這個請求委託給父類的加載器去執行;
  • 若是父類加載器還存在其父類加載器,則進一步向上委託,依次遞歸,請求最終將到達頂層的啓動類加載器;
  • 若是父類加載器能夠完成類加載任務,就成功返回,假若父類加載器沒法完成此加載任務,子加載器纔會嘗試本身去加載,這就是雙親委派模式

優點

  • 避免類的重複加載,JVM中區分不一樣類,不只僅是根據類名,相同的class文件被不一樣的ClassLoader加載就屬於兩個不一樣的類(好比,Java中的Object類,不管哪個類加載器要加載這個類,最終都是委派給處於模型最頂端的啓動類加載器進行加載,若是不採用雙親委派模型,由各個類加載器本身去加載的話,系統中會存在多種不一樣的Object類)
  • 保護程序安全,防止核心API被隨意篡改,避免用戶本身編寫的類動態替換 Java的一些核心類,好比咱們自定義類:java.lang.String

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

  • 類的完成類名必須一致,包括包名
  • 加載這個類的ClassLoader(指ClassLoader實例對象)必須相同

沙箱安全機制

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

破壞雙親委派模型

  • 雙親委派模型並非一個強制性的約束模型,而是Java設計者推薦給開發者的類加載器實現方式,能夠「被破壞」,只要咱們自定義類加載器,重寫loadClass()方法,指定新的加載邏輯就破壞了,重寫findClass()方法不會破壞雙親委派。
  • 雙親委派模型有一個問題:頂層ClassLoader,沒法加載底層ClassLoader的類。典型例子JNDI、JDBC,因此加入了線程上下文類加載器(Thread Context ClassLoader),能夠經過Thread.setContextClassLoaser()設置該類加載器,而後頂層ClassLoader再使用Thread.getContextClassLoader()得到底層的ClassLoader進行加載。
  • Tomcat中使用了自定ClassLoader,而且也破壞了雙親委託機制。每一個應用使用WebAppClassloader進行單獨加載,他首先使用WebAppClassloader進行類加載,若是加載不了再委託父加載器去加載,這樣能夠保證每一個應用中的類不衝突。每一個tomcat中能夠部署多個項目,每一個項目中存在不少相同的class文件(不少相同的jar包),他們加載到jvm中能夠作到互不干擾。
  • 利用破壞雙親委派來實現代碼熱替換(每次修改類文件,不須要重啓服務)。由於一個Class只能被一個ClassLoader加載一次,不然會報java.lang.LinkageError。當咱們想要實現代碼熱部署時,能夠每次都new一個自定義的ClassLoader來加載新的Class文件。JSP的實現動態修改就是使用此特性實現。

參考:《深刻理解JVM虛擬機》《尚硅谷JVM》

相關文章
相關標籤/搜索