《深刻理解JVM》 虛擬機類加載機制

1、類加載過程

一、加載

加載是文件到內存的過程。完成如下3件事:html

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

二、驗證

驗證是對類文件內容驗證。目的在於確保Class文件符合當前虛擬機要求,不會危害虛擬機自身安全。主要包括如下四種:java

  • 文件格式驗證
  • 元數據驗證
  • 字節碼驗證
  • 符號引用驗證

三、準備

準備階段是進行內存分配。爲類變量也就是類中由static修飾的變量分配內存,而且設置初始值,這裏要注意,初始值是0或者null,而不是代碼中設置的具體值,代碼中設置的值是在初始化階段完成的。另外這裏也不包含用final修飾的靜態變量,由於final在編譯的時候就會分配了。算法

四、解析

解析主要是解析字段、接口、方法。主要是將常量池中的符號引用替換爲直接引用的過程。直接引用就是直接指向目標的指針、相對偏移量等。注意,這個階段不必定要有。api

五、初始化

初始化主要完成靜態塊執行與靜態變量的賦值。這是類加載最後階段,若被加載類的父類沒有初始化,則先對父類進行初始化。緩存

2、什麼是Classloader

一個Java程序要想運行起來,首先須要通過編譯生成 .class文件,而後建立一個運行環境(jvm)來加載字節碼文件到內存運行,而.class 文件是怎樣被加載中jvm中的就是Java Classloader所作的事情。安全

那麼.class文件何時會被類加載器加載到jvm中運行呢?好比執行new操做時候,當咱們使用Class.forName(「包路徑+類名」)、Class.forName(「包路徑+類名」,initialize,classloader)、ClassLoader.loadclass(「包路徑+類名」);時候就觸發了類加載器去類加載對應的路徑去查找*.class,並建立Class對象。數據結構

Class.forName():將類的.class文件加載到jvm中以外,還會對類進行解釋並執行類中的static塊;jvm

ClassLoader.loadClass():只幹一件事情,就是將.class文件加載到jvm中,不會執行static中的內容,只有在newInstance纔會去執行static塊。函數

:Class.forName(name, initialize, loader)帶參函數也可控制是否加載static塊。而且只有調用了newInstance()方法採用調用構造函數,建立類的對象 。this

3、類加載器

如何尋找類加載器先來看個例子:

public class ClassLoaderTest {

    public static void main(String[] args) {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();
        System.out.println(loader);// 應用類加載器
        System.out.println(loader.getParent());// 擴展類加載器
        System.out.println(loader.getParent().getParent());
    }

}

運行後,輸出結果:

sun.misc.Launcher$AppClassLoader@56a96eba
sun.misc.Launcher$ExtClassLoader@da4a1c9
null

從上面的結果能夠看出,並無獲取到ExtClassLoader的父Loader,緣由是Bootstrap ClassLoader(引導類加載器)是用C語言實現的,找不到一個肯定的返回父Loader的方式,因而就返回null。

這幾種類加載器的層次關係以下圖所示:

注意:這裏父類加載器並非經過繼承關係來實現的,而是採用組合實現的。通常咱們都認爲ExtClassloader的父類加載器是BootStarpClassloader,可是其實他們之間根本是沒有父子關係的,只是在ExtClassloader找不到要加載類時候會去委託BootStrap加載器去加載。

站在Java虛擬機的角度來說,只存在兩種不一樣的類加載器:

  1. 啓動類加載器:它使用C++實現(這裏僅限於Hotspot,也就是JDK1.5以後默認的虛擬機,有不少其餘的虛擬機是用Java語言實現的),是虛擬機自身的一部分;
  2. 全部其餘的類加載器:這些類加載器都由Java語言實現,獨立於虛擬機以外,而且所有繼承自抽象類java.lang.ClassLoader,這些類加載器須要由啓動類加載器加載到內存中以後才能去加載其餘的類。

站在Java開發人員的角度來看,類加載器能夠大體劃分爲如下三類:

3.1 BootstrapClassloader

引導類加載器,又稱啓動類加載器,是最頂層的類加載器,主要用來加載Java核心類,如rt.jar、resources.jar、charsets.jar等,Sun的JVM中,執行java的命令中使用-Xbootclasspath選項或使用- D選項指定sun.boot.class.path系統屬性值能夠指定附加的類,它不是 java.lang.ClassLoader的子類,而是由JVM自身實現的該類c 語言實現,Java程序訪問不到該加載器。

經過下面代碼能夠查看該加載器加載了哪些jar包:

package test;

import java.net.URL;

public class ClassLoaderTest {

    public static void main(String[] args) {       
        getJars();
    }
    
    public static void getJars() {  
        URL[] urls = sun.misc.Launcher.getBootstrapClassPath().getURLs();    
        for (int i = 0; i < urls.length; i++) {    
            System.out.println(urls[i].toExternalForm());    
        }   
    }

}

執行結果:

file:/Z:/JDK/jre/lib/resources.jar
file:/Z:/JDK/jre/lib/rt.jar
file:/Z:/JDK/jre/lib/sunrsasign.jar
file:/Z:/JDK/jre/lib/jsse.jar
file:/Z:/JDK/jre/lib/jce.jar
file:/Z:/JDK/jre/lib/charsets.jar
file:/Z:/JDK/jre/lib/jfr.jar
file:/Z:/JDK/jre/classes

寫到這裏你們應該都知道,咱們並無在classpath裏面指定這些類的路徑,爲啥仍是能被加載到jvm並使用起來了吧,由於這些是bootstarp來加載的。

3.2 ExtClassloader

擴展類加載器,主要負責加載Java的擴展類庫,默認加載JAVA_HOME/jre/lib/ext/目下的全部jar包或者由java.ext.dirs系統屬性指定的jar包。放入這個目錄下的jar包對全部AppClassloader都是可見的(後面會知道ExtClassloader是AppClassloader的父加載器)。那麼ext都是在哪些地方加載類呢?

System.out.println(System.getProperty("java.ext.dirs"));

3.3 AppClassloader

系統類加載器,又稱應用加載器,本文說的SystemClassloader和APPClassloader是一個東西,它負責在JVM啓動時,加載來自在命令java中的-classpath或者java.class.path系統屬性或者 CLASSPATH操做系統屬性所指定的JAR類包和類路徑。調用ClassLoader.getSystemClassLoader()能夠獲取該類加載器。若是沒有特別指定,則用戶自定義的任何類加載器都將該類加載器做爲它的父加載器,這點經過ClassLoader的無參構造函數能夠知道以下:

protected ClassLoader() {
   this(checkCreateClassLoader(), getSystemClassLoader());
}

執行如下代碼便可得到classpath加載路徑:

System.out.println(System.getProperty("java.class.path"));

3.4 雙親委派模型

Java類加載器使用的是雙親委派模型,若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把請求委託給父加載器去完成,依次向上,所以,全部的類加載請求最終都應該被傳遞到頂層的啓動類加載器中,只有當父加載器在它的搜索範圍中沒有找到所需的類時,即沒法完成該加載,子加載器纔會嘗試本身去加載該類。

一、當AppClassLoader加載一個class時,它首先不會本身去嘗試加載這個類,而是把類加載請求委派給父類加載器ExtClassLoader去完成。

二、當ExtClassLoader加載一個class時,它首先也不會本身去嘗試加載這個類,而是把類加載請求委派給BootStrapClassLoader去完成。

三、若是BootStrapClassLoader加載失敗(例如在$JAVA_HOME/jre/lib裏未查找到該class),會使用ExtClassLoader來嘗試加載;

四、若ExtClassLoader也加載失敗,則會使用AppClassLoader來加載,若是AppClassLoader也加載失敗,則會報出異常ClassNotFoundException。

那麼問題來了,爲啥使用這種方式呢?由於這樣能夠避免重複加載,當父親已經加載了該類的時候,就沒有必要子ClassLoader再加載一次。考慮到安全因素,咱們試想一下,若是不使用這種委託模式,那咱們就能夠隨時使用自定義的String來動態替代java核心api中定義的類型,這樣會存在很是大的安全隱患,而雙親委託的方式,就能夠避免這種狀況,由於String已經在啓動時就被引導類加載器(Bootstrcp ClassLoader)加載,因此用戶自定義的ClassLoader永遠也沒法加載一個本身寫的String,除非你改變JDK中ClassLoader搜索類的默認算法。

雙親委派模型意義

一、系統類防止內存中出現多份一樣的字節碼,避免類的重複加載;

二、避免Java的核心API被篡改,保證Java程序安全穩定運行。

下面咱們從源碼看如何實現雙親委派模型:

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }

                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    long t1 = System.nanoTime();
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

分析代碼知道:

  1. 首先從jvm緩存查找該類,若是該類以前被加載過,則直接從jvm緩存返回該類,否者看當前類加載器是否有父加載器;
  2. 若是有父加載器的話則委託爲父類加載器進行加載,否者委託BootStrapClassloader進行加載;
  3. 若是仍是沒有找到,則調用當前Classloader的findclass方法進行查找。

從上面源碼知道要想修改類加載委託機制,實現本身的載入策略能夠經過覆蓋ClassLoader的findClass方法或者覆蓋loadClass方法來實現。

Java應用啓動過程是首先Bootstarp Classloader加載rt.jar包裏面的sun.misc.Launcher類,而該類內部使用BootstarpClassloader加載器構建和初始化Java中三種類加載和線程上下文類加載器,而後在根據不一樣場景去使用這些類加載器去本身的類查找路徑去加載類。

參考

http://ifeve.com/classloader%e8%a7%a3%e6%83%91/

http://www.cnblogs.com/ityouknow/p/5603287.html

相關文章
相關標籤/搜索