1. 程式人生 > 實用技巧 >JVM:類載入 & 類載入器

JVM:類載入 & 類載入器

System virtual machine: a.k.a.full virtualization VM. Provides a substitute for a real machine (functionality needed to execute entire OS). Allows for multiple environments which are isolated from one another yet exist on the same physical machine.

Process Virtual Machine: a.k.a.application virtual machine

orManaged Runtime Environment.

Process VM runs as a normal application inside a host OS and support a single process, is created when that process is started and destroyed when it exits.

Process VM’s purpose is to provide aplatform-independent programming environmentthat abstracts away details of the underlying hardware and operating system and allows a program to execute in the same way on any platform.

e.g. JVM; Parrot virtual machine; .Net Framework.

*使用工具:

Source Insight -檢視openjdk原始碼

clion - 編寫底層程式

idea / netbeans - 單步除錯jdk

hsdb

類載入

Klass

Klass:(存在於元空間,)Java的每個類的物件在JVM中都有一個對應的Klass類例項,用於儲存類的元資訊(e.g.常量池,屬性資訊,方法資訊)

Klass的繼承結構

MasterspaceObj

|-Metadata

|-Klass

|-InstanceKlass

|-InstanceMirrorKlass

|-InstanceRefKlass

|-InstanceClassLoaderKlass

|-ArrayKlass

|-TyperArrayKlass

|-ObjArrayKlass

InstaneKlass:表示普通(非陣列)Java類。類載入器將.class檔案載入進系統,將.class檔案解析生成類的元資訊,儲存在InstanceKlass中。子類包括InstanceMirrorKlass、InstanceRefKlass和InstanceClassLoaderKlass。

InstanceMirrorKlass:表示Java程式碼中的java.lang.Class類,儲存在堆區。

InstanceRefKlass:表示java.lang.ref.Reference類的子類。

InstanceClassLoaeder:用於遍歷某個載入器載入的類。

Java中的陣列不是靜態資料型別(e.g. JVM內建的8種資料型別),是動態資料型別(i.e.在執行期生成的)。

ArrayKlass:儲存陣列類的元資訊。

TyperArrayKlass:表示基本型別的陣列。

ObjArrayKlass:表示引用型別的陣列。

實驗

證明java陣列是動態資料型別

->java main方法中new一個int/物件陣列,編譯執行

->IDEA使用外掛jclasslib檢視位元組碼(idea->view->show bytecode with Jclasslib),main中顯示newarray/anewarray,對應位元組碼手冊中含義:“建立一個原始型別/引用型陣列並將其引用至壓入棧頂”。

檢視java類對應的klass

->HSDB -> Tools/Class Browser->找到目標類名即可找到對應記憶體地址

->HSDB->Tools/Inspector->輸入記憶體地址->可檢視java類在記憶體中對應的klass類

or

->while true維持程式執行,terminal輸入jps –l獲取當前執行程序id;

->HSDB->file/attach to hotspot process->輸入目標程序ID

->選中main執行緒,工具欄第二個按鈕檢視執行緒堆疊->可檢視java物件底層記憶體地址

->複製記憶體地址->HSBD->tool/inspector->可檢視java物件底層的實現類

HSDB attach後記得detach。

類載入的過程

載入--可以隨便使用任何語言實現類載入器,只要能夠達到這三個效果。

->通過類的全限定名獲取儲存該類的class檔案(沒有指明必須從哪獲取)

->解析成執行時資料(instanceKlass例項),存放在方法區;

->在堆區生成該類的Class物件(instanceMirrorKlass例項)。

JVM載入類是懶載入模式。--根載入器載入jar檔案時並沒有把其中所有的類都進行載入,而是隻載入了一部分(預載入模式,只先載入常用的String,Thread,Integer等類)。

類載入的時機?--主動使用時

1)new, getstatic, putstatic, invokestatic位元組碼i.e. java程式碼中使用new關鍵字例項化物件、讀取或設定類的靜態欄位(final修飾的常量除外)、呼叫類的靜態方法

2)反射

3)初始化子類時會去載入其父類

4)啟動類(main函式所在類)

5)當使用JDK1.7動態語言支援時,如果一個java.lang.invoke.MethodHandle例項最後的解析結果REF_getStatic、REF_putStatic、REF_invokeStatic的方法控制代碼,並且這個方法控制代碼所對應的類沒有進行過初始化,則需要先觸發其初始化。

從哪載入?-因為沒有指明從哪獲取class檔案,可採用的思路:

1)從壓縮包中讀取e.g. jar, war

2)從網路中獲取e.g. Web Applet

3)動態生成e.g.動態代理、CGLIB

4)由其他檔案生成e.g. JSP

5)從資料庫讀取

6)從加密檔案讀取

驗證–檢查klass檔案是否符合規範,判斷版本, jvm能否正常執行klass,…etc.

1.檔案格式驗證

2.元資料驗證

3.位元組碼驗證

4.符號引用驗證

//參考《深入理解java虛擬機器》

準備

為靜態變數分配記憶體和賦初值。例項變數沒有賦初值一說,而是在建立物件時完成賦值。

例外:如果被Final修飾,編譯時會給新增ConstantValue屬性,準備階段直接完成賦值,沒有賦初值步驟。

不同資料型別對應不同的初值:

解析–間接引用轉為直接引用

*常量池:包括靜態常量池(class檔案常量池)、執行時常量池(可在HSDB中檢視)和字串常量池(StringTable)。

間接引用a.k.a.符號引用:指向執行時常量池的引用

直接引用:記憶體地址

1.類或介面的解析

2.欄位解析

3.方法解析

4.介面方法解析

解析後的資訊儲存在ConstantPoolCache類例項中。

何時解析?--思路有:

1)載入階段解析常量池時

2)用時// openjdk採用,在執行特定位元組碼指令(e.g. anewarray, checkcast, getfield, …)前進行解析。

初始化–執行靜態程式碼段,完成靜態變數的賦值。

java程式碼中定義一個static屬性(靜態欄位、靜態程式碼段),位元組碼層面就會生成clint方法(只有一個)

clint方法(i.e.位元組碼中生成的靜態塊)中語句順序跟定義靜態屬性的java程式碼的編寫順序是保持一致的。

E.g. java程式碼:

public static int a=10;
public static int b=10;

位元組碼:

實驗

證明final成員沒有賦初值而是直接賦值

->演示類中宣告成員static final int a=10, static int b=10;

->檢視位元組碼,a中有屬性ConstantValue,代表沒有賦初值,準備階段就完成賦值。(b在準備階段被賦初值0)

檢視常量池

-> idea terminal切換到classes目錄下, javap –verbose物件全限定名(全限定名可在ide中選中類名右鍵copy reference得),輸出的Constant pool:部分表示靜態常量池

e.g.靜態常量池中Class對應#32,翻閱下方對應當前類名字串。’#32’即為一個符號引用(指向常量池的引用)。

->執行程式,HSDB attach到目標程序,class browser點選目標物件,下方可檢視動態常量池

e.g.動態常量池中Class不再指向常量池,而是指向記憶體地址@0x000….,即為一個直接引用。

初始化實驗1

public class Test {
  public static void main(String[] args) {
      TestA obj=TestA.getInstance();
      System.out.println(TestA.val1);
      System.out.println(TestA.val2);
  }
}
class TestA {
  public static int val1;
  public static val2=1;
  public static TestA instance=new TestA();
  TestA() {
      val1++;
      val2++;
  }
  public static TestA getInstance() {
      return instance;
  }
}

輸出12

原因:初始化時執行靜態程式碼段,val1賦初值為0,val1賦值為1;執行建構函式後都+1;輸出1 2.

初始化實驗2 //將static val2定義移到構造方法後

public class Test {
  public static void main(String[] args) {
      TestA obj=TestA.getInstance();
      System.out.println(TestA.val1);
      System.out.println(TestA.val2);
  }
}
class TestA {
  public static int val1;
  public static TestA instance=new TestA();
  TestA() {
      val1++;
      val2++;
  }
  public static val2=1;
  public static TestA getInstance() {
      return instance;
  }
}

輸出:11

原因:生成的靜態塊中語句順序跟定義靜態屬性的java程式碼的編寫順序是保持一致的,所以val2經過建構函式後被定義語句覆蓋回1;

程式的執行順序: 1) clint方法2)預設構造方法(執行完++後val1=1,val2=1);靜態塊(val2又被賦值為1)。

載入試驗1

public class Test {
  public static void main(String[] args) {
      System.out.printf(TestB.str);
  }
}
class TestA {
  public static String str=”A str”;
  static {
      System.out.println(“A Static Block”);
  }
}
class TestB extends TestA {
  static {
      System.out.println(“B Static Block”);
  }
}

輸出:Astatic Astr

原因:A是B的父類,會被主動載入;B沒有被使用,不會被載入

載入試驗2

public class Test {
  public static void main(String[] args) {
      System.out.printf(new TestB().str); //new了B物件
  }
}
class TestA {
  public String str=”A str”;//去掉static
  static {
      System.out.println(“A Static Block”);
  }
}
class TestB extends TestA {
  static {
        System.out.println(“B Static Block”);
  }
}

輸出:Astatic Bstatic Astr

原因:AB都被使用,都會被載入(主動使用子類,就是間接在主動使用父類)

載入試驗3

public class Test {
  public static void main(String[] args) {
      System.out.printf(new TestB().str); 
  }
}
class TestA {
  static {
      System.out.println(“A Static Block”);
  }
}
class TestB extends TestA {
  public String str=”A str”;//str從A移到B
  static {
      System.out.println(“B Static Block”);
  }
}

輸出:Astatic Bstatic Astr

原因:同上

載入試驗4

public class Test {
  public static void main(String[] args) {
      System.out.printf(testB.str); 
  }
}
class TestA {
  static {
      System.out.println(“A Static Block”);
  }
}
class TestB extends TestA {
  public static String str=”B str”; //static成員
  static {
      System.out.println(“B Static Block”);
  }
}

輸出:Astatic Bstatic Bstr

原因:靜態欄位在子類裡,子類會被載入

載入試驗5

public class Test {
  public static void main(String[] args) {
      System.out.println(TestA.str);
  }
}
class TestA {
  public static final String str=”A Str”;
  static {
      System.out.println(“A Static Block”);
  }
}

輸出:AStr

原因:雖然AStr在A類裡,但是是被final修飾的常量,此常量被寫入到Test類的常量池中

載入試驗6

public class Test {
  public static void main(String[] args) {
      System.out.println(TestA.uuid);
  }
}
class TestA {
  public static final String uuid=UUID.randomUUID().toString();
  static {
      System.out.println(“A Static Block”);
  }
}

輸出:Astatic uuid

原因:雖然uuid是final修飾,但randomUUID().toString()是動態執行的,uuid需要動態生成,不能寫入到Test的常量池。所以類A會被載入。

載入試驗7

public class Test2 {
  static {
    System.out.println(“Test2 Static Block”);
  }
  public static void main(String[] args) throws ClassNotFoundException {
    Class<?> clazz=Class.forName(“com.xxxx.Test1”);
  }
}

輸出:Test2Static Test1Static

原因:因為反射,類12都會被載入

載入試驗8

public class Test {
  public static void main(String[] args) {
      System.out.printf(B.str);
  }
}
class A {
  public static String str=”str”;
  static {
      System.out.println(“A Static Block”);
  }
}
class B extends A {
  static {
      str+=”###”;
      System.out.println(“B Static Block”);
  }
}

輸出:Astatic str

原因:JVM先判斷是否載入,後面才會有初始化動作發生。案例中B的內部沒有任何東西被使用,所以沒有載入B,B的靜態塊不會被執行。

讀取靜態變數的底層實現 涉及InstanceKlass, instanceMirrorKlass, ConstantPoolCache

實驗證明靜態屬性儲存在映象類中

public class Test {
  public static void main(String[] args) {
      System.out.printf(B.str);
      while (true) {}
  }
}
class A {
  public static String str=”A str”;
  static {
      System.out.println(“A Static Block”);
  }
}
class B extends A {
  static {
      System.out.println(“B Static Block”);
  }
}

輸出:Astatic Astr

->執行,查出程序ID,在HSDB中attach

-> HSDB classbrowser找到類A的記憶體地址,輸入到inspector

->可以在inspector類A中找到靜態屬性str是儲存在oop Klass: java.mirror中。說明jdk8靜態屬性是儲存在映象類(instanceMirrorKlass)中的。(而不是儲存在instanceKlass, jdk6之前是)

->同樣操作在inspector類B的oop Klass:java.mirror中並沒有找到靜態屬性str,說明靜態屬性str只存放在父類A。

-既然靜態屬性str存放在父類A中,main中呼叫B.str是怎麼找到它的?

-兩種實現思路:

1)先從子類B的映象類中取,如果有直接返回,沒有則沿著繼承鏈往上找;-- O(n)

2)藉助另外的資料結構,使用K-V格式儲存。-- O(1)。

Hotspot採用的就是思路2,藉助另外的資料結構ConstantPoolCache,常量池類ConstantPool有屬性_cache指向該結構,每條資料對應一個類ConstantPoolCacheEntry。

*ConstantPoolCache:用於儲存某些位元組碼指令所需的解析(resolve)好的常量項,例如給[get|put]static, [get|put]field, invoke[static|special|virtual|interface|dynamic]等指令對應的常量池項用。

ConstantPoolCacheEntry的獲取?

參考\openjdk\hotspot\src\share\vm\oop\cpCache.hpp通過ConstantPoolCache的地址加上偏移量

類載入器

JVM的類載入器包含兩種型別:

1)由C++編寫的;--啟動類載入器(Bootstrap Class Loader)

2)由Java編寫的--其他繼承java.lang.ClassLoader的類載入器

JVM也支援自定義類載入器。

各種類載入器之間邏輯上的父子關係不是真正的父子關係,沒有直接從屬關係。

啟動類載入器

啟動類載入器:JVM將C++處理類的一套邏輯定義為啟動類載入器。啟動類載入器沒有實體。

因為由C++編寫,無法被Java程式呼叫,在Java程式中顯示null。

啟動類載入器的載入路徑

URL[] urLs = Launcher.getBootstrapClassPath().getURLs();
for (URL urL : urLs) {
    System.out.println(urL);
}

openjdk原始碼

JavaMain中呼叫了LoadMainClass,啟動類載入器就是在這時載入的。

Openjdk/jdk/src/share/bin/java.c / JavaMain()

JavaMain(void * _args) {
    …
    mainClass = LoadMainClass(env, mode, what);
    …
}

LoadMainClass中需要先找到Launcherhelper類。啟動類載入器所做的事情就是載入類”sun/launcher/LauncherHelper”,checkAndLoadMain就是在LauncherHelper類裡面。

Openjdk/jdk/src/share/java.c / GetLauncherHelperClass()

GetLauncherHelperClass(JNIEnv *env) {
    …
    NULL_CHECK0(helperClass = FindBootStrapClass(env,
            "sun/launcher/LauncherHelper"));
    …
}

GetLauncherHelperClass主要呼叫FindBootStrapClass。FindBootStrapClass中用GetProcessAddress呼叫JVM動態連結庫的JVM_FindClassFromBootLoader方法。

openjdk/jdk/src/windows/bin/java_md.c / FindBootStrapClass()

jclass FindBootStrapClass(JNIEnv *env, const char *classname) {
    …
    findBootClass = (FindClassFromBootLoader_t *)GetProcAddress(hJvm,
                "JVM_FindClassFromBootLoader");
    …
} 

找到LauncherHelper類後,通過JNI執行LauncherHelper類的checkAndLoadMain方法。

用於載入main class(main方法所在的類),三種類載入器的父子鏈(->啟動擴充套件類載入器->應用類載入器)也是在這次呼叫中完成的。

LoadMainClass返回的result其實就是呼叫checkAndLoadMain的結果– main class。

Openjdk/jdk/src/share/bin/java.c / LoadMainClass()

LoadMainClass(JNIEnv *env, int mode, char *name) {
    …
    jclass cls = GetLauncherHelperClass(env);
    …
    NULL_CHECK0(mid = (*env)->GetStaticMethodID(env, cls,
                "checkAndLoadMain",
                "(ZILjava/lang/String;)Ljava/lang/Class;"));
    …
}

從checkAndLoadeMain開始都是java程式碼。checkAndLoadMain方法檢查了執行模式,如果是class就直接命名,如果是jar包則從jar包中找,其他則報錯。然後呼叫scloader.loadClass方法獲得main class來返回。

openjdk/jdk/src/share/classes/sun/launcher/LauncherHelper.java / checkAndLoadMain()

public static Class<?> checkAndLoadMain(boolean printToStderr,
                                            int mode,
                                            String what) {
    …
        switch (mode) {
            case LM_CLASS:
                cn = what;
                break;
            case LM_JAR:
                cn = getMainClassFromJar(what);
                break;
            default:
                // should never happen
                throw new InternalError("" + mode + ": Unknown launch mode");
        }
    …
    mainClass = scloader.loadClass(cn);
    …
}

scloader是由ClassLoader.getSystemClassLoader()得到的,其中呼叫了initSystemClassLoader方法。

Openjdk/jdk/src/share/classes/java/lang /ClassLoader.java / getSystemClassLoader()

public static ClassLoader getSystemClassLoader() {
    initSystemClassLoader();
    …
}

initSystemClassLoader中呼叫了Launcher.getLauncher方法。

Openjdk/jdk/src/share/classes/java/lang /ClassLoader.java / initSystemClassLoader

private static synchronized void initSystemClassLoader() {
    …
    sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
    …
}

Launcher建構函式中初始化了ExtClassLoader,再以ext為引數初始化appClassLoader。Ext其實就是parent。

執行緒上下文類載入器contextClassLoader也是在這時賦值的。

openjdk/jdk/src/share/classes/sun/misc/Launcher.java

public class Launcher {
    …
    private static Launcher launcher = new Launcher();
    …
    public static Launcher getLauncher() {
        return launcher;
    }
    public Launcher() {
        …
        // Create the extension class loader
        extcl = ExtClassLoader.getExtClassLoader();
        …
        // Now create the class loader to use to launch the application
        loader = AppClassLoader.getAppClassLoader(extcl);
        …
       Thread.currentThread().setContextClassLoader(loader);
    }
    …
}

從getAppClassLoader()和其中呼叫的AppClassLoader建構函式可看出傳入的extcl引數為parent。

openjdk/jdk/src/share/classes/sun/misc/Launcher.java /AppClassLoader

static class AppClassLoader extends URLClassLoader {
    public static ClassLoader getAppClassLoader(final ClassLoader extcl) throws IOException {
        …
        return new AppClassLoader(urls, extcl);
    }
    AppClassLoader(URL[] urls, ClassLoader parent) {…}
}

為什麼Ext的parent是null?

從ExtClassLoader建構函式看出其super建構函式傳入的就是null,而ExtClassLoader建構函式的super對應的形參就是parent。

openjdk/jdk/src/share/classes/sun/misc/Launcher.java /ExtClassLoader

static class ExtClassLoader extends URLClassLoader {
    public static ExtClassLoader getExtClassLoader() throws IOException {
        …
        return new ExtClassLoader(dirs);
    }
    public ExtClassLoader(File[] dirs) throws IOException {
        super(getExtURLs(dirs), null, factory);
        …
    }
    …
}

openjdk/jdk/share/classes/java/net/URLClassLoader.java

URLClassLoader(URL[] urls, ClassLoader parent, AccessControlContext acc) {…}

順序:jvm的目的是要去載入main所在類->啟動boot載入器->啟動ext-載入器>啟動app載入器->通過app載入器去載入main class

所以這不是繼承上的父子關係,而是載入鏈邏輯上的父子關係。邏輯上的父子關係目的就是為了雙親委派。

擴充套件類載入器

擴充套件類載入器的載入路徑是通過向System.getProperty()傳入引數”java.ext.dirs”

String[] urls = System.getProperty(“java.ext.dirs”).split(“:”);
for (String url : urls)
    System.out.println(url);
ClassLoader classLoader = ClassLoader.getSystemClassLoader().getParent();

URLClassLoader urlClassLoader = (URLClassLoader) classLoader;
URL[] urls = urlClassLoader.getURLs();
for (URL url : urls)
    System.out.println(url);

向System.getProperty()傳入引數”java.ext.dirs”的做法也可以在openjdk原始碼中看到:

openjdk/jdk/src/share/classes/sun/misc/Launcher.java /ExtClassLoader

public ExtClassLoader(File[] dirs) throws IOException {
    super(getExtURLs(dirs), null, factory);
    …
}
private static File[] getExtDirs() {
    String s = System.getProperty("java.ext.dirs");
    …
}

不同的類載入器載入同一個類,相等嗎?

不相等。方法區是按照類載入器進行分開儲存的。每個類載入器在方法區裡都有一塊獨立的區域,雖然載入的是同一份檔案,但是不會在同一個空間裡。

同一個類載入器載入同一個檔案多次,實際上會載入幾次?

一次。因為載入前會(根據全限定名)去判斷空間裡是否已經有這個類。

雙親委派

雙親委派:需要查詢某個類時,先判斷在當前類載入器是否已經載入(能在其空間中找到),如果已經載入則直接返回,沒有則向上委託給其父類載入器。

*系統已載入的class資訊儲存在SystemDictionary類中。

e.g.查詢某個類

->判斷當前最下層的自定義的類載入器是否已載入該類,是則直接返回,否則往上委託給父類AppClassLoader;

->判斷在AppClassLoader中是否已經載入,是則直接返回,否則再往上委託給父類ExtClassLoader;

-> …委託給BootstrapClassLoader…

->如果BootstrapClassLoader也沒有載入直接報錯

侷限性:無法做到不委派或向下委派

e.g.資料庫需要實現的driver介面是由啟動類載入器載入。而第三方資料(如mysql)的相關實現類需要由應用類載入器載入,啟動類載入器不能載入,需要向下委派。

什麼叫打破雙親委派?

兩種思路。a)不委派–只用當前類載入器去載入–實現方式:自定義類載入器

b)向下委派(SPI機制中的一部分)

SPI:一種服務發現機制,通過在ClassPath路徑下的META-INF/services資料夾查詢檔案,自動載入檔案裡所定義的類

e.g. SPI Demo(該案例不算打破雙親委派,因為所有類都是啟動類載入器載入的)

PayService是自定義的一個介面,有兩個實現AlipayService和WxpayService。

Public interface PayService {
    void pay();
}

main中使用執行緒上下文載入器載入PayService類。

public static void main(String[] args) {
    ServiceLoader<PayService> services=ServiceLoader.load(PayService.class);
    for (PayService service : services)
        service.pay();
}

通過介面呼叫的pay方法呼叫的是哪一個實現類是看pom.xml/<dependencies>/<artifactId>中指定載入的是哪一個模組。

Pom.xml (gateway-main)

<dependencies>
    <dependency><artifactId>pay-wx</artifactId></dependency></dependencies>

每個實現類底下的/src/main/resources/META-INF.services/目錄下的檔案中指定了實現類的全限定名。SPI從而找到需要載入的實現類。

/pay-wx/src/main/resources/META-INF.services/com.luban.common.service.PayService

    com.luban.pay.AlipayService

實現向下委派?

需要使用ServiceLoader。

e.g. Driver中的SPI機制。JDBI的底層實現用到serviceloader,也是一種SPI。driver通過向下委派來打破雙親委派。

ServiceLoader<Driver> loadedDrivers=ServiceLoader.load(Driver.class);
Iterator<Driver> driversIterator=loadedDrivers.iterator();

執行緒上下文類載入器

ServiceLoader的底層是由執行緒上下文類載入器ContextClassLoader實現的,在SPI中向下委派中有應用。

ContextClassLoader可通過Thread.currentThread().setContextClassLoader()進行設定。

在checkAndLoadMain時已經進行設定,預設為AppClassLoader。

openjdk/jdk/src/share/classes/sun/misc/Launcher.java

public Launcher() {
    …
    // Now create the class loader to use to launch the application
    loader = AppClassLoader.getAppClassLoader(extcl);
    …
    Thread.currentThread().setContextClassLoader(loader);
}

自定義類載入器

如何實現自定義類載入器?

extends ClassLoader類,重寫findClass方法。

實驗1:自定義載入器重寫findClass時返回null,能否載入成功

public class ClassLoader1 extends ClassLoader {
  public void main(String[] args) throws Exception {
      Classloader1 classloader1=new Classloader1();
      Class<?> clazz=classloader1.loadClass(“com.experiment.classloader.A”);
      System.out.println(“clazz hashcode: “+clazz.hashCode());
  }
  @Override
  protected Class<?> findClass(String className) throws ClassNotFoundException {
      return null;
  }
}

結果:可以載入

原因:雙親委派。可通過System.out.println(clazz1.getClassLoader())打印出sun.misc.Launcher$AppClassLoader證明clazz1是由AppClassLoader載入的。

實驗2

public class ClassLoader1 extends ClassLoader {
  public void main(String[] args) throws Exception {
      Classloader1 classloader1=new Classloader1(), classloader2=new Classloader1();
      Class<?> clazz1=classloader1.loadClass(“com.experiment.classloader.A”);
      Class<?> clazz2=classloader2..loadClass(“com.experiment.classloader.A”);
      System.out.println(clazz1==clazz2)
  }
  @Override
  protected Class<?> findClass(String className) throws ClassNotFoundException {…}
}

結果:true

原因:用同一個classLoader類載入同一個類,得到的Class是同一個類。可通過System.out.println(clazz1.hashCode())證明clazz1和clazz2的hashCode是一樣的。

自定義類載入器如何打破雙親委派

classLoadder原始碼中:

Class(String)底層了呼叫loadClass(String, boolean)

public Class<?> loadClass(String name) throws ClassNotFoundException {
    return this.loadClass(name, false);
}

loadClass(String, Boolean)中的雙親委派邏輯

protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
    …
    Class<?> c = this.findLoadedClass(name);
    if (c == null) { //判斷是否已經載入
        try {
            if (this.parent! = null)
                c = this.parent.loadClass(name, false); //向上委派
            else
                c = this.findBootstrapClassOrNull(name);
        } catch (ClassNotFoundException) {}    
    }
    …
    if (resolve) {//判斷有無解析,進行解析
        this.resolveClass(c);
    }
    return c;
}

可通過重寫loadClass(String, Boolean)打破雙親委派:對指定包下的類的載入做不委派處理

@Override
protected Class<?> loadClass(String name, Boolean resolve) throws ClassNotFoundException {
    …
    if (c == null) {
        if (name.startsWith(“com.experiment”))
            c = findClass(name);
        else
            c = this.getParent().loadClass(name);
    }
}

沙箱安全


checkAndLoadMain底層有呼叫到initSystemClassLoader。initClassLoader中所做的AccessController.doPrivileged判斷就是一種沙箱安全機制。

Openjdk/jdk/src/share/classes/java/lang /ClassLoader.java / initSystemClassLoader

private static synchronized void initSystemClassLoader() {
    …
    scl = AccessController.doPrivileged(
                        new SystemClassLoaderAction(scl));
    …
}

實驗:沙箱安全

在自創java.lang包下定義String類,在main中呼叫String的方法會報錯。

public class String {
  public static void main(String[] args) {
      String.show();
  }
  public static void show() {
      System.out.println(“String show function”);
  }
}

結果:報錯“在類中找不到main方法”

原因:沙箱安全防止打破雙親委派修改系統類,保護核心類庫。