萬萬沒想到,面試中,連 ClassLoader類加載器 也能問出這麼多問題…..

一、類加載過程

萬萬沒想到,面試中,連 ClassLoader類加載器 也能問出這麼多問題.....

類加載時機java

「加載」mysql

將類的.class文件中的二進制數據讀入到內存中,將其放在運行時數據區的方法區內,而後在內存上建立一個java.lang.Class對象用來封裝類在方法區內的數據結構做爲這個類的各類數據的訪問入口。web

「驗證」面試

主要是爲了確保class文件中的字節流包含的信息是否符合當前JVM的要求,且不會危害JVM自身安全,好比校驗文件格式、是不是cafe baby魔術、字節碼驗證等等。sql

「準備」數據庫

爲類變量分配內存並設置類變量(是被static修飾的變量,變量不是常量,因此不是final的,就是static的)初始值的階段。這些變量所使用的內存在方法區中進行分配。好比緩存

private static int age = 26;

類變量age會在準備階段事後爲 其分配四個(int四個字節)字節的空間,而且設置初始值爲0,而不是26。tomcat

如果final的,則在編譯期就會設置上最終值。安全

「解析」數據結構

JVM會在此階段把類的二進制數據中的符號引用替換爲直接引用。

「初始化」

初始化階段是執行類構造器<clinit>()方法的過程,到了初始化階段,才真正開始執行類定義的Java程序代碼(或者說字節碼 )。好比準備階段的那個age初始值是0,到這一步就設置爲26。

「使用」

對象都出來了,業務系統直接調用階段。

「卸載」

用完了,能夠被GC回收了。

二、類加載器種類以及加載範圍

萬萬沒想到,面試中,連 ClassLoader類加載器 也能問出這麼多問題.....

類加載器種類

「啓動類加載器(Bootstrap ClassLoader)」

最頂層類加載器,他的父類加載器是個null,也就是沒有父類加載器。負責加載jvm的核心類庫,好比java.lang.*等,從系統屬性中的sun.boot.class.path所指定的目錄中加載類庫。他的具體實現由Java虛擬機底層C++代碼實現。

「擴展類加載器(Extension ClassLoader)」

父類加載器是Bootstrap ClassLoader。從java.ext.dirs系統屬性所指定的目錄中加載類庫,或者從JDK的安裝目錄的jre/lib/ext子目錄(擴展目錄)下加載類庫,若是把用戶的jar文件放在這個目錄下,也會自動由擴展類加載器加載。繼承自java.lang.ClassLoader

「應用程序類加載器(Application ClassLoader)」

父類加載器是Extension ClassLoader。從環境變量classpath或者系統屬性java.class.path所指定的目錄中加載類。繼承自java.lang.ClassLoader

「自定義類加載器(User ClassLoader)」

除了上面三個自帶的之外,用戶還能制定本身的類加載器,可是全部自定義的類加載器都應該繼承自java.lang.ClassLoader。好比熱部署、tomcat都會用到自定義類加載器。

補充:不一樣ClassLoader加載的文件路徑配置在以下源碼裏寫的:

// sun.misc.Launcher

public class Launcher {
    // Bootstrap類加載器的加載路徑,在static靜態代碼塊裏用的
    private static String bootClassPath = System.getProperty("sun.boot.class.path");
    
    // AppClassLoader 繼承 ClassLoader
    static class AppClassLoader extends URLClassLoader {
        public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {
            // java.class.path
            final String var1 = System.getProperty("java.class.path");
        }
    }
    
    // ExtClassLoader 繼承 ClassLoader
    static class ExtClassLoader extends URLClassLoader {
        public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {
            // java.ext.dirs
            String var0 = System.getProperty("java.ext.dirs");
        }
    }   
}

 

三、雙親委派是什麼

若是一個類加載器收到了類加載的請求,他首先會從本身緩存裏查找是否以前加載過這個class,加載過直接返回,沒加載過的話他不會本身親自去加載,他會把這個請求委派給父類加載器去完成,每一層都是如此,相似遞歸,一直遞歸到頂層父類。

也就是Bootstrap ClassLoader,只要加載完成就會返回結果,若是頂層父類加載器沒法加載此class,則會返回去交給子類加載器去嘗試加載,若最底層的子類加載器也沒找到,則會拋出ClassNotFoundException

源碼在java.lang.ClassLoader#loadClass(java.lang.String, boolean)

萬萬沒想到,面試中,連 ClassLoader類加載器 也能問出這麼多問題.....

雙親委派模型

四、爲啥要有雙親委派

防止內存中出現多份一樣的字節碼,安全。

好比本身重寫個java.lang.Object並放到Classpath中,沒有雙親委派的話直接本身執行了,那不安全。雙親委派能夠保證這個類只能被頂層Bootstrap Classloader類加載器加載,從而確保只有JVM中有且僅有一份正常的java核心類。若是有多個的話,那麼就亂套了。好比相同的類instance of可能返回false,由於可能父類不是同一個類加載器加載的Object。

五、爲何須要破壞雙親委派模型

Jdbc

Jdbc爲何要破壞雙親委派模型?

之前的用法是未破壞雙親委派模型的,好比Class.forName("com.mysql.cj.jdbc.Driver");

而在JDBC4.0之後,開始支持使用spi的方式來註冊這個Driver,具體作法就是在mysql的jar包中的META-INF/services/java.sql.Driver文件中指明當前使用的Driver是哪一個,而後使用的時候就不須要咱們手動的去加載驅動了,咱們只須要直接獲取鏈接就能夠了。Connection con = DriverManager.getConnection(url, username, password );

首先,理解一下爲何JDBC須要破壞雙親委派模式,緣由是原生的JDBC中Driver驅動自己只是一個接口,並無具體的實現,具體的實現是由不一樣數據庫類型去實現的。例如,MySQL的mysql-connector-*.jar中的Driver類具體實現的。

原生的JDBC中的類是放在rt.jar包的,是由Bootstrap加載器進行類加載的,在JDBC中的Driver類中須要動態去加載不一樣數據庫類型的Driver類,而mysql-connector-*.jar中的Driver類是用戶本身寫的代碼,那Bootstrap類加載器確定是不能進行加載的,既然是本身編寫的代碼,那就須要由Application類加載器去進行類加載。

這個時候就引入線程上下文件類加載器(Thread Context ClassLoader),經過這個東西程序就能夠把本來須要由Bootstrap類加載器進行加載的類由Application類加載器去進行加載了。

Tomcat

Tomcat爲何要破壞雙親委派模型?

由於一個Tomcat能夠部署N個web應用,可是每一個web應用都有本身的classloader,互不干擾。好比web1裏面有com.test.A.class,web2裏面也有com.test.A.class,若是沒打破雙親委派模型的話,那麼web1加載完後,web2在加載的話會衝突。

由於只有一套classloader,卻出現了兩個重複的類路徑,因此tomcat打破了,他是線程級別的,不一樣web應用是不一樣的classloader。

  • Java spi 方式,好比jdbc4.0開始就是其中之一。

  • 熱部署的場景會破壞,不然實現不了熱部署。

六、如何破壞雙親委派模型

重寫loadClass方法,別重寫findClass方法,由於loadClass是核心入口,將其重寫成自定義邏輯便可破壞雙親委派模型。

七、如何自定義一個類加載器

只須要繼承java.lang.Classloader類,而後覆蓋他的findClass(String name)方法便可,該方法根據參數指定的類名稱,返回對應 的Class對象的引用。

八、熱部署原理

採起破壞雙親委派模型的手段來實現熱部署,默認的loadClass()方法先找緩存,你改了class字節碼也不會熱加載,因此自定義ClassLoader,去掉找緩存那部分,直接就去加載,也就是每次都從新加載。

九、常見筆試題

問題:輸出結果是什麼?

答案:編譯報錯。

緣由:由於靜態語句塊中只能訪問定義在靜態語句塊以前的變量,定義在他以後的 變量在前面的靜態語句塊中能夠賦值,可是不能訪問。

/**
 * Description: 編譯報錯
 *
 * @author TongWei.Chen 2021-01-08 17:37:44
 */
public class Test1 {
    static {
        // 編譯沒報錯
        i = 2;
        // 編譯報錯Illegal forward reference
        System.out.println(i);
    }
    private static int i =1;
}

 

問題:輸出結果是什麼?

答案 :一、3

緣由:由於類加載過程當中會先準備類變量(也就是靜態變量),準備階段是賦初始值階段,也就是test2=null,value1=0,value2=0,而後進入初始化階段的時候test2=new Test2(),會執行構造器,結果是value1 = 1,value2 = 4,而後執行value1和value2這兩句,value1沒變化,value2被從新賦值成了3,因此結果1和3。

public class Test2 {
    private static Test2 test2 = new Test2();
    private static int value1;
    private static int value2 = 3;

    private Test2() {
        value1 ++;
        value2 ++;
    }

    public static void main(String[] args) {
        // 1
        System.out.println(test2.value1);
        // 3
        System.out.println(test2.value2);
    }
}

 

那若是把private static Test2 test2 = new Test2();放到private static int value2 = 3;下面的話結果就是1和4了。

public class Test3 {
    private static int value1;
    private static int value2 = 3;
    private static Test3 test3 = new Test3();
    
    private Test3() {
        value1 ++;
        value2 ++;
    }
    
    public static void main(String[] args) {
        // 1
        System.out.println(test3.value1);
        // 4
        System.out.println(test3.value2);
    }
}

 

END

推薦好文

強大,10k+點讚的 SpringBoot 後臺管理系統居然出了詳細教程!

分享一套基於SpringBoot和Vue的企業級中後臺開源項目,代碼很規範!

能掙錢的,開源 SpringBoot 商城系統,功能超全,超漂亮!

相關文章
相關標籤/搜索