JVM詳解(二)-- 第2章 類加載器子系統

1、JVM內存結構

1.1 內存結構---概略圖

Alt

1.2 內存結構--詳細圖

Alt text

2、類加載器子系統的做用

  • 類加載器子系統負責從文件系統或網絡中加載.Class文件,文件須要有特定的標識(cafe babe)。
  • ClassLoader只負責.Class文件的加載,至於它是否能夠運行,由執行引擎決定。
  • 加載的類信息存放於一塊被稱爲「方法區」的內存空間。除了類信息外,方法區還會存放運行時常量池信息,可能還包括字符串字面量(字面量指的是固定值,初始值)和數字常量(這部分常量信息是.Class文件中常量池部分的內存映射)
  • .Class文件被解析加載到 JVM,類的對象加載到堆區,類信息被加載到方法區(java8 中方法區的實現是「元空間」)。這部分工做是類加載子系統完成的

3、類加載的過程

假設定義了一個類,名爲HelloWorld,運行其 Main 方法,流程如圖:
Alt texthtml

3.1 加載(Loading)

加載(Loading)是狹義上的加載,「類加載」中的加載是廣義上的。java

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

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

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

3.2 連接(Linking)

連接可細分爲三步:驗證(verify)準備(prepare)、解析(resolve)編程

3.2.1 驗證

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

3.2.2 準備

  • 爲類的變量分配內存並設置默認初始值,即零值(不一樣數據類型的零值不一樣,布爾值爲false,引用類型爲null)。下面這個語句在這個階段,a將被初始化爲0。
static int a = 2;
  • 這裏不包含final修飾的static,由於final修飾的變量再也不是變量而是不可更改的常量,在編譯期就已經分配內存,準備階段會將其顯示初始化。如上面的賦值語句,加上final後,a的值就被初始化爲2。
final static int a = 2;
  • 這裏不會爲實例變量分配內存和初始化,類變量會分配在方法區中,而實例變量是會隨着對象一塊兒分配到Java堆中。

3.2.3 解析

  • 將常量池內的符號引用轉換爲直接引用的過程。
  • 事實上,解析動做每每伴隨着 JVM 在執行完初始化以後再執行。

3.3 初始化(Initialization)

  1. 初始化階段就是執行類構造方法<clinit>()的過程。
  2. 此方法不須要定義,是javac編譯器自動收集類中全部類靜態變量的賦值動做和靜態代碼塊的語句合併而來。
    • 只有當有靜態變量static int a = 1;或者靜態代碼塊static {}時纔會建立並執行該方法。
    • 靜態方法並不會使得虛擬機建立執行該方法。
  3. <clinit>()中指令按語句在源文件中出現的順序執行。具體表現就是一個靜態變量的最後的取值決定於最後一行它的賦值語句。
public class ClassInitTest {
   private static int num = 1;

   static{
       num = 2;
       number = 20;
       System.out.println(num);
       //System.out.println(number);//報錯:非法的前向引用。
   }

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

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

在以上代碼中,num和number的最終值分別爲2和10。
4. <clinit>()不一樣於咱們常說的類的構造函數。類的構造函數在虛擬機中是<init>(),在任什麼時候候都是會建立的,由於全部的類都至少有一個默認無參構造函數。
5. 若該類具備父類,JVM會保證子類的 <clinit>()執行前,父類的 <clinit>()已經執行完畢。
6. 虛擬機必須保證一個類的 <clinit>()方法在多線程下被同步加鎖。保證同一個類只被虛擬機加載一次(只調用一次 <clinit>()),後續其它線程再使用類,只須要在虛擬機的緩存中獲取便可。bootstrap

4、類加載器的分類

類加載分爲啓動類加載器、擴展類加載器、應用程序類加載器(系統類加載器)、自定義加載器。以下圖:
Altapi

須要注意的是,它們四者並不是子父類的繼承關係。如下展現瞭如何獲取類加載器:數組

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
        
    }
}

關於幾種類加載器具體的加載對象和雙親委派機制能夠參考:玩命學JVM-類加載機制緩存

4.1 用戶自定義類加載器

4.1.1 爲何要自定義類加載器

  1. 隔離加載類。
  2. 修改類加載的方式。除了bootstrap classloader是必需要用到的,其它的類加載器都沒必要須。
  3. 擴展加載源。
  4. 防止源碼泄露。java代碼很容易被反編譯和篡改。所以有對源碼進行加密的需求,在這個過程當中就會用到本身定義的類加載器去完成解密和類加載。

4.1.2 如何自定義類加載器

步驟安全

  1. 繼承java.lang.ClassLoader的方式,實現本身的類加載器。
  2. 建議不要去覆蓋loadClass(),而是重寫findClass()
  3. 若是沒有太複雜的需求(解密、從不一樣的路徑下加載),那麼可直接繼承URLClassLoader,這樣能夠避免本身去編寫findClass()方法以及其獲取字節碼流的方式,使其自定義類加載器編寫更加簡潔。
    樣例代碼
public class CustomClassLoader extends ClassLoader {
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {

        try {
            byte[] result = getClassFromCustomPath(name);
            if(result == null){
                throw new FileNotFoundException();
            }else{
                return defineClass(name,result,0,result.length);
            }
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        }

        throw new ClassNotFoundException(name);
    }

    private byte[] getClassFromCustomPath(String name){
        //從自定義路徑中加載指定類:細節略
        //若是指定路徑的字節碼文件進行了加密,則須要在此方法中進行解密操做。
        return null;
    }

    public static void main(String[] args) {
        CustomClassLoader customClassLoader = new CustomClassLoader();
        try {
            Class<?> clazz = Class.forName("One",true,customClassLoader);
            Object obj = clazz.newInstance();
            System.out.println(obj.getClass().getClassLoader());
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

4.2 關於ClassLoader

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

  1. getParent()
    返回該類加載器的超類加載器。
  2. loadClass(String name)
    加載名稱爲name的類,返回結果爲java.lang.Class類的實例。
  3. findClass(String name)
    查找名稱爲name的類,返回結果爲java.lang.Class類的實例。
  4. findLoaderClass(String name)
    查找名稱爲name的已經被加載過的類,返回結果爲java.lang.Class類的實例。
  5. defineClass(String name, byte[] b, int off, int len)
    把字節數組b中的內容轉換爲一個Java類,返回結果爲java.lang.Class類的實例。與 findClass(String name) 搭配使用
  6. resolveClass(Class<?>c)
    鏈接一個指定的Java類

4.3 雙親委派機制

詳見 http://www.javashuo.com/article/p-cloinoxk-mo.html
自定義的一個java.lang.String不能加載到的JVM中,緣由:
使用自定義的java.lang.String時,首先是應用類加載器向上委託到擴展類加載器,而後擴展類加載器向上委託給引導類加載器,引導類加載接收到類的信息,發現該類的路徑時「java.lang.String」,這在引導類加載器的加載範圍內,所以引導類加載器開始加載「java.lang.String」,只不過此時它加載的是jdk核心類庫裏的「java.lang.String」。這就是雙親委派機制中的向上委託。在完成向上委託以後,如到了引導類加載器,引導類加載器發現待加載的類不屬於本身加載的類範圍,就會再向下委託給擴展類加載器,讓下面的加載器進行類的加載。
優點

  1. 避免類的重複加載。類加載器+類自己決定了 JVM 中的類加載,雙親委派機制保證了只會有一個類加載器去加載類。
  2. 保護程序安全,防止核心api被篡改

沙箱安全機制
上文中提到的java.lang.String就是沙箱安全機制的表現,保證了對java核心源代碼的保護。

5、幾個JVM常出現的術語解析

5.1 字面量

首先來看一下百度百科的定義:

在計算機科學中, 字面量(literal)是用於表達源代碼中一個固定值的表示法(notation)。幾乎全部計算機編程語言都具備對基本值的字面量表示, 諸如: 整數, 浮點數以及字符串; 而有不少也對布爾類型和字符類型的值也支持字面量表示; 還有一些甚至對枚舉類型的元素以及像數組, 記錄和對象等複合類型的值也支持字面量表示法.

這段話不太好理解,咱們來拆解下(注意下面這段話純屬我的理解):

「字面量(literal)是用於表達源代碼中一個固定值的表示法(notation)」,這裏說明了兩點:第一,字面量是體如今源碼中的;第二,字面量是對一個固定值的表示。接下來它提到了「幾乎全部計算機編程語言都具備對基本值的字面量表示」,並以一些基本數據類型、枚舉、數組等數據類型舉例。它們都有一個特色,就是它們的賦值是能夠作到「代碼可視化的」。你能夠在代碼中給以上提到的類型進行賦值。而咱們賦值時所給出的「值」,更準確來講是一種「表示」(好比給數組賦值時,約定了須要用大括號括起來)就是字面量的含義。
舉個例子:

int i = 1;
String s = "abs";
int[] a = {1, 3, 4};
// 以上 1,「abc」,{1,3,4}均是字面量

5.2 符號引用、直接引用

一樣,先來看一下書面定義:

符號引用:符號引用以一組符號來描述所引用的目標,符號引用能夠是任何形式的字面量,只要使用時可以無歧義的定位到目標便可。好比org.simple.People類引用了org.simple.Language類,在編譯時People類並不知道Language類的實際內存地址,所以只能使用符號org.simple.Language(假設是這個,固然實際中是由相似於CONSTANT_Class_info的常量來表示的)來表示Language類的地址。各類虛擬機實現的內存佈局可能有所不一樣,可是它們能接受的符號引用都是一致的,由於符號引用的字面量形式明肯定義在Java虛擬機規範的Class文件格式中。

直接引用: 直接引用能夠是
(1)直接指向目標的指針(好比,指向「類型」【Class對象】、類變量、類方法的直接引用多是指向方法區的指針)
(2)相對偏移量(好比,指向實例變量、實例方法的直接引用都是偏移量)
(3)一個能間接定位到目標的句柄
直接引用是和虛擬機的佈局相關的,同一個符號引用在不一樣的虛擬機實例上翻譯出來的直接引用通常不會相同。若是有了直接引用,那引用的目標一定已經被加載入內存中了。

說實話看這種書面化語言抽象晦澀,下面給出一些本身的理解吧。首先找到一個.class文件(來源:玩命學JVM(一))反編譯後的結果中常量池的部分:

Constant pool:
   #1 = Methodref          #6.#15         // java/lang/Object."<init>":()V
   #2 = Fieldref           #16.#17        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #18            // Hello World
   #4 = Methodref          #19.#20        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Class              #21            // Main
   #6 = Class              #22            // java/lang/Object
   #7 = Utf8               <init>
   #8 = Utf8               ()V
   #9 = Utf8               Code
  #10 = Utf8               LineNumberTable
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               Main.java
  #15 = NameAndType        #7:#8          // "<init>":()V
  #16 = Class              #23            // java/lang/System
  #17 = NameAndType        #24:#25        // out:Ljava/io/PrintStream;
  #18 = Utf8               Hello World
  #19 = Class              #26            // java/io/PrintStream
  #20 = NameAndType        #27:#28        // println:(Ljava/lang/String;)V
  #21 = Utf8               Main
  #22 = Utf8               java/lang/Object
  #23 = Utf8               java/lang/System
  #24 = Utf8               out
  #25 = Utf8               Ljava/io/PrintStream;
  #26 = Utf8               java/io/PrintStream
  #27 = Utf8               println
  #28 = Utf8               (Ljava/lang/String;)V

爲何只看常量池呢,由於在「解析」的定義中提到了:解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。咱們來看看常量池中有什麼:

常量池:常量池指的是字節碼文件中的Constant pool部分。它是靜態的,當編譯生成字節碼文件直接就不變了。常量池中包括各類字面量和對類型、域和方法的符號引用。幾種在常量池內存儲的數據類型包括:數量值、字符串值、類引用、字段引用、方法引用。

由此咱們能夠看出,上面咱們給出的常量池中都屬於「符號引用」(符號引用自己就是一種字面量)或字面量。咱們不由要問了了,那直接引用在哪呢?
我找到了《深刻理解Java虛擬機》中的一句話:

對同一個符號引用進行屢次解析請求是很常見的事情,除invokedynamic指令外,虛擬機實現能夠對第一次解析的結果進行緩存(在運行時常量池中記錄直接引用,並把常量標識爲已解析狀態)從而避免解析動做重複進行。

關鍵是括號中話給了啓發,說明直接引用是放在運行時常量池中的,接下來咱們看看運行時常量池的一些定義或特性。

運行時常量池是方法區的一部分。常量池用於存放編譯期生成的各類字面量和符號引用,這部份內容將在類加載後存放到方法區的運行時常量池中。而加載類和接口到虛擬機後,就會建立對應的運行時常量池。
運行時常量池與常量池的不一樣點在於:
1. 運行時常量池中包括了編譯期就已經明確的數值字面量,也包括在運行期解析後才能得到的方法或字段引用。但,請注意,此時的方法或字段引用已經再也不是常量池中的「符號引用」,而是「直接引用」。
2. 運行時常量池具有「動態性」。

至此,關於「符號引用」和「直接引用」的解釋就差很少了。最後我再多說一句,再分析的時候,我一直在疑惑「直接引用」究竟是什麼,我能不能像看到常量池中的內容同樣看到「直接引用」。實際上,咱們並不能拿到這樣一個文件,裏面整齊地寫了直接引用的具體內容,由於直接引用不是所謂的「字面量」。但咱們能夠回到「直接引用」的最初定義:直接引用能夠是指向目標的指針、相對偏移量或是能間接定位到目標的句柄,能夠想象一下在運行時,在內存中存放的直接引用大概是什麼內容。

6、其它

JVM 中兩個Class對象是否爲同一個類的必要條件

  1. 類的完整類名必須一致,包括包名。
  2. 加載這個類的ClassLoader(指ClassLoader實例對象)必須相同。
    對類加載器的引用
    JVM 必須知道一個類型是由啓動類加載器加載的仍是由用戶類加載器加載的,若是一個類型是由用戶類加載器加載的,那麼 JVM 會將這個類加載器的一個引用做爲類型信息的一部分保存在方法區中,當解析一個類型到另外一個類型的引用的時候, JVM 須要保證這兩個類型的類加載器是相同的。

類的主動使用和被動使用
主動使用和被動使用的區別是,主動使用會致使類的初始化。
主動使用有如下七種狀況:

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

參考文獻:
https://blog.csdn.net/u011069294/article/details/107489721
http://www.javashuo.com/article/p-huylhhxc-mq.html

相關文章
相關標籤/搜索