天天看點

JVM解毒——類加載系統

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

點贊+收藏 就學會系列,文章收錄在 GitHub

JavaEgg ,N線網際網路開發必備技能兵器譜

直擊面試

  1. 看你履歷寫得熟悉JVM,那你說說類的加載過程吧?
  2. 我們可以自定義一個String類來使用嗎?
  3. 什麼是類加載器,類加載器有哪些?
  4. 多線程的情況下,類的加載為什麼不會出現重複加載的情況?
  5. 什麼是雙親委派機制?它有啥優勢?可以打破這種機制嗎?
JVM解毒——類加載系統

類加載子系統

JVM解毒——類加載系統

類加載機制概念

Java虛拟機把描述類的資料從Class檔案加載到記憶體,并對資料進行校驗、轉換解析和初始化,最終形成可以被虛拟機直接使用的Java類型,這就是虛拟機的加載機制。Class檔案由類裝載器裝載後,在JVM中将形成一份描述Class結構的元資訊對象,通過該元資訊對象可以獲知Class的結構資訊:如構造函數,屬性和方法等,Java允許使用者借由這個Class相關的元資訊對象間接調用Class對象的功能,這裡就是我們經常能見到的Class類。

類加載子系統作用

  • 類加載子系統負責從檔案系統或者網絡中加載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解毒——類加載系統

1. 加載(Loading):

  1. 通過一個類的全限定名擷取定義此類的二進制位元組流
  2. 将這個位元組流所代表的的靜态存儲結構轉化為方法區的運作時資料結構
  3. 在記憶體中生成一個代表這個類的

    java.lang.Class

    對象,作為方法區這個類的各種資料的通路入口

加載

.calss

檔案的方式

  • 從本地系統中直接加載
  • 通過網絡擷取,典型場景:Web Applet
  • 從zip壓縮檔案中讀取,成為日後jar、war格式的基礎
  • 運作時計算生成,使用最多的是:動态代理技術
  • 由其他檔案生成,比如 JSP 應用
  • 從專有資料庫提取.class 檔案,比較少見
  • 從加密檔案中擷取,典型的防 Class 檔案被反編譯的保護措施

2. 連接配接(Linking)

驗證(Verify)

  • 目的在于確定Class檔案的位元組流中包含資訊符合目前虛拟機要求,保證被加載類的正确性,不會危害虛拟機自身安全
  • 主要包括四種驗證,檔案格式驗證,中繼資料驗證,位元組碼驗證,符号引用驗證

準備(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在編譯的時候就會配置設定了,準備階段會顯示初始化
  • 這裡不會為執行個體變量配置設定初始化,類變量會配置設定在方法區中,而執行個體變量是會随着對象一起配置設定到Java堆中
    private static int i = 1;  //變量i在準備階隻會被指派為0,初始化時才會被指派為1
    private final static int j = 2;  //這裡被final修飾的變量j,直接成為常量,編譯時就會被配置設定為2           

解析(Resolve)

  • 将常量池内的符号引用轉換為直接引用的過程
  • 事實上,解析操作往往會伴随着JVM在執行完初始化之後再執行
  • 符号引用就是一組符号來描述所引用的目标。符号引用的字面量形式明确定義在《Java虛拟機規範》的Class檔案格式中。直接引用就是直接指向目标的指針、相對偏移量或一個間接定位到目标的句柄
  • 解析動作主要針對類或接口、字段、類方法、接口方法、方法類型等。對應常量池中的

    CONSTANT_Class_info

    CONSTANT_Fieldref_info

    CONSTANT_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_getStatic

    REF_putStatic

    REF_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.jar

    resource.jar

    sun.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)

  • sun.misc.Lanucher$AppClassLoader

  • 父類加載器為擴充類加載器
  • 它負責加載環境變量

    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解毒——類加載系統

優勢

  • 避免類的重複加載,JVM中區分不同類,不僅僅是根據類名,相同的class檔案被不同的ClassLoader加載就屬于兩個不同的類(比如,Java中的Object類,無論哪一個類加載器要加載這個類,最終都是委派給處于模型最頂端的啟動類加載器進行加載,如果不采用雙親委派模型,由各個類加載器自己去加載的話,系統中會存在多種不同的Object類)
  • 保護程式安全,防止核心API被随意篡改,避免使用者自己編寫的類動态替換 Java的一些核心類,比如我們自定義類:java.lang.String

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

  • 類的完成類名必須一緻,包括包名
  • 加載這個類的ClassLoader(指ClassLoader執行個體對象)必須相同

沙箱安全機制

如果我們自定義String類,但是在加載自定義String類的時候會率先使用引導類加載器加載,而引導類加載器在加載的過程中會先加載jdk自帶的檔案(rt.jar包中javalangString.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解毒——類加載系統