深入理解JVM之類加載器

 類加載器

類的加載指的是將類的.class文件中的二進制數據讀取到內存中,並放在運行時數據區的方法區內。然後再堆內存中創建一個java.lang.Class對象(jvm規範餅沒有規定Class對象位於們哪裏)用來封裝類再方法區中的數據結構。

從jdk1.2開始,類的加載過程採用雙親委託機制。這種機制能更好的保證java平臺的安全。委託機制中。除了jvm自帶的根類加載器以外,其餘的類加載器有且只有一個父加載器,當java程序請求類加載器loader加載類的時候,loader首先委託自己父加載器去加載類,若父加載器能加載,則由父加載器完成加載任務,否則由loader本身加載。
 雙親委託模型好處
 1.可以確保java核心類庫的安全,所有的java應用都至少會引用java.lang.Object類。也就是說運行起Object類會被加載到jvm中,如果這個加載過程由自定義類加載器加載,就有可能造成jvm中存在多個名命空間的Java.lang.Object類,而且這些類之間是不兼容的相互之間不可見(名命空間所起的作用)藉助雙親委託機制,java核心類庫都是有啓動加載器統一完成。,保證java應用中所有的核心類庫使用的都是同一個版本,他們之間相互兼容
 2.可以確保java核心類庫所提供的類不會被自定義的類所代替
 3.不通的類加載器可以爲相同名稱的類常見額外的名命空間。相同名稱的類可以並存java虛擬機中,只需要不通的類加載器加載即可,不通類加載器之間的類不兼容,就相當於java虛擬機內部創建了一個又一個相互隔離的java類空間。
jvm本身定義了三種類加載器
根類加載器(bootstrap):該類沒有父加載器,他負責加載虛擬機核心庫,例如java.lang.*等。根類加載器從系統屬性 sun.boot.class.path所指定的目錄加載類庫。根類加載器的實現依賴於底層操作系統。數據虛擬機實現的一部分。沒有繼承java.lang.classLoader類
擴展類加載器(Extension):他的父加載器爲根類加載器,從java.ext.dir系統屬性所指定的目錄中加載類庫。或者從jdk安裝/jre/lib/ext子類目錄下加載類庫,如果用戶創建的jar文件放到這個目錄下,會自動由擴展加載器加載,擴展類加載器是java類是java.lang.ClassLoader的子類
系統類加載器(System): 也成爲應用類加載器,它的父加載器爲擴展類加載器,它從環境變量classpath或者java.class.path所指定的目錄中加載類。它是用戶自定義的類加載器的默認父加載器,系統類加載器是純java類,是Java.lang.ClassLoader的子類。
自定義類加載器:用戶自定義類加載器需要繼承ClassLoder類。
類加載器並不需要等到某個類被主動使用的時候加載。Jvm規範允許類加載器在預料某個類將要被使用的時候預先加載,如果在預先加載過程中遇到。class文件確實或者存在錯誤,類加載器必須在程序首次使主動使用該類的時候才報告錯誤(LinkageError)如果這個類一直沒有主動使用,那麼類加載器就不會報告錯誤
類加載器名命空間
每個類加載器都有自己的名命空間,類加載的的名命空間有當前類加載器和父類加載器構成。同一個命名空間的所有類是相互可見的,子類加載器加載的類能看到父類加載器加載的類,父類加載器不能看到子類加載器所加載的類。如果兩個類加載器沒有直接或者間接的關係,那麼他們各自加載的類相互之間不可見。
總結:子加載器所加載的類可以訪問到父加載器加載的類
父加載器所加載的類無法訪問到子加載器加載的類


案例1:
類加載器用的都是同一個自定義類加載器CustomClassLoader
1.自定義類加載器

public class CustomClassLoader extends ClassLoader {
    private  String loaderName = "";
    private final String fileExtension = ".class";
    private String path;
    //將系統類加載器當作當前加載器的父類
    public CustomClassLoader(String loaderName){
        super();
        this.loaderName = loaderName;
    }
    public String getPath() {
        return path;
    }
    public void setPath(String path) {
        this.path = path;
    }
    //指定類加載器當作當前類加載器的父類
    public CustomClassLoader(ClassLoader classLoader,String loaderName){
        super(classLoader);
        this.loaderName = loaderName;
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        System.out.println("findclass invoke:" + name);
        System.out.println("classLoaderName:" + loaderName);
        byte [] data = this.loadClassData(name);
        return this.defineClass(name,data,0,data.length);
    }
    public byte [] loadClassData(String name) throws ClassNotFoundException {
        InputStream in = null;
        byte [] data = null;
        ByteArrayOutputStream outs = null;
        try{
            String nameRel = name.replace(".","/");
            in = new FileInputStream(new File(path+nameRel+this.fileExtension));
            outs = new ByteArrayOutputStream();
            int ch = 0;
            while(-1 != (ch=in.read())){
                outs.write(ch);
            }
            data = outs.toByteArray();
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try{
                in.close();
                outs.close();
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        return data;
    }
}

public class TestClass {
    private int a = 5;
    private TestClass testClass;
    public static void doing(){
        System.out.println("static TestClass");
    }
    public void setTestClass(Object object){
        testClass = (TestClass) object;
    }
}

public class ValidSpace {
    public static void main(String[] args) throws Exception{
        CustomClassLoader classLoader1 = new CustomClassLoader("loader1");
        //CustomClassLoader classLoader2 = new CustomClassLoader(classLoader1,"loader2");
        CustomClassLoader classLoader2 = new CustomClassLoader("loader2");
        classLoader1.setPath("D:/");
        classLoader2.setPath("D:/");
        Class clazz1 = classLoader1.loadClass("com.namespace.TestClass");
        Class clazz2 = classLoader2.loadClass("com.namespace.TestClass");
        Object object1 = clazz1.newInstance();
        Object object2 = clazz2.newInstance();
        Method method = clazz1.getMethod("setTestClass",Object.class);
        method.invoke(object1,object2);
    }
}


執行結果:
findclass invoke:com.namespace.TestClass
classLoaderName:loader1
Exception in thread "main" java.lang.reflect.InvocationTargetException
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at com.namespace.ValidSpace.main(ValidSpace.java:22)
Caused by: java.lang.ClassCastException: com.namespace.TestClass cannot be cast to com.namespace.TestClass
    at com.namespace.TestClass.setTestClass(TestClass.java:15)
    ... 5 more
原因:
雖然logdr1和loader2雖然都是同一個自定義類加載器,但是從類加載器的雙親委託機制上來說看他們沒有存在任何關係。loader1加載TestClass類的時候首先會讓委託給父類加載器系統類加載器加載,系統類加載器會委託給拓展類加載器,拓展類加載器會委託給根類加載器,但是無論哪個類加載器都不能加載TestClass類。所以最終還是有loader1類加載器加載。同上loader2也一樣。因爲loader1和loader2兩個不存在任何關係,最終在內存中會形成兩個名命空空間,相互之間不可見。所以會報錯。如果將 //CustomClassLoader classLoader2 = new CustomClassLoader(classLoader1,"loader2"); 這行代碼放開那麼loader2的父類加載器就是loader1,就不會存在上面問題,因爲最終都會有loader1加載。


案例2:

public class MyGe {
    public MyGe(){
        System.out.println("Myge is load by:" + this.getClass().getClassLoader());
        new MyYi();
    }
}

public class MyYi {
    public MyYi(){
        System.out.println("MyYi is load by : " + this.getClass().getClassLoader());
    }
}

public class ValidSpace {
    public static void main(String[] args) throws Exception{
        CustomClassLoader classLoader1 = new CustomClassLoader("loader1");
        classLoader1.setPath("D:");
        Class clazz1 = classLoader1.loadClass("com.namespace.MyGe");
        System.out.println("class:" + clazz1.hashCode());
        Object object = clazz1.newInstance();
    }
}


public class ValidSpace {
    public static void main(String[] args) throws Exception{
        CustomClassLoader classLoader1 = new CustomClassLoader("loader1");
        classLoader1.setPath("D:");
        Class clazz1 = classLoader1.loadClass("com.namespace.MyGe");
        System.out.println("class:" + clazz1.hashCode());
        Object object = clazz1.newInstance();
    }
}
運行結果
class:1735600054
Myge is load by:sun.misc.Launcher$AppClassLoader@18b4aac2
Exception in thread "main" java.lang.NoClassDefFoundError: com/namespace/MyYi
    at com.namespace.MyGe.<init>(MyGe.java:13)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
    at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
    at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
    at java.lang.Class.newInstance(Class.java:442)
    at com.namespace.ValidSpace.main(ValidSpace.java:16)
Caused by: java.lang.ClassNotFoundException: com.namespace.MyYi
    at java.net.URLClassLoader.findClass(URLClassLoader.java:381)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:335)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    ... 7 more
在idea中如果把target下面的MyYi.class移動到到D:/com/namespace下面。運行程序就會報錯,原因如下 Class clazz1 = classLoader1.loadClass("com.namespace.MyGe"); 因爲只是刪除了target下面的MyYi.class所以MyGe類還是由應用類加載器加載。new MyYi()類的時候,它是有加載了MyGe類的加載器根據雙親委託機制加載,因爲target下面我們已經刪除了,所以根本加載不到。因此報錯。
相反如果把MyGe類移動到D:/com/namespace目錄下
findclass invoke:com.namespace.MyGe
classLoaderName:loader1
class:2133927002
Myge is load by:com.namespace.CustomClassLoader@677327b6
MyYi is load by : sun.misc.Launcher$AppClassLoader@18b4aac2
程序是可以正常加載的,原因Myge由自定義CustomClassLoader類加載器加載,根據雙親委託AppClassLoader系統類加載器直接能從classpath(target)中加載MyYi。因爲父父類加載器加載的類是可以看到父類加載器加載的類。因此程序能夠正常執行。

線程上下文類加載器


子類加載器可以看到父類加載器所加載的類,父類加載器不可以看到子類加載器所加載的類,因此線程類上下文類加載器的出現就是打破這種雙親委託機制在某種場景下不適用的問題。
每個類都會使用當前類加載器加載了自身的類加載器去加載自身所依賴的類(前提依賴的類沒有被加載)
線程上下文類加載器
類Thread中Thread.currentThread().getContextClassLoader();Thread.currentThread().setContextClassLoader(ClassLoader cl1);分別用來獲取設置類加載器
如果沒有設置上下文類加載器,線程將繼承該父線程的上下文類加載器,java應用運行是的初始化線程類加載器是系統類加載器。在線程中運行的代碼可以通過該類加載器加載類和資源。
線程類加載器的重要性
問題延申:

Class.forName(name);
Connection connection = DriverManager.getConnection“jdbc:mysql://localhost:3306/customer”username, password);
Statement preparedStatement = connection.prepareStatement(sql);


上面是普通的鏈接jdbc的初始代碼,其中Connection、Statement 都是java定義的接口位於rt.jar,並且是由根類加載器加載或者應用類加載器加載。具體的實現是有具體廠商實現。
如果需要將具體的實現融合到應用當中,那麼就需要把這些實現放到環境的classpath下面。顯然廠商的實現就就不會被根類加載器加載,是由應用類加載器加載。
根據雙親委託原則,這樣就存在類加載器名命空間問題。這種問題在SPI(service provider interface)場合下都會出現問題。
而線程上下文類加載器就是解決這種雙親委託原則造成的侷限,父classloader可以通過Thread.currentThread().getContextClassLoader()所指定的classloader去加載類
就可以改變父類加載器加載的類不能訪問子類加載器加載的類的情況了,線程上下文類加載器就是Current Classloader。
雙親委託模型,由下至上,下層的類加載器是可以訪問上層類加載器加載的類。但對於SPI來說,有些核心接口java核心庫所提供的,核心庫是由根類加載器加載的。而這些接口的實現來自不通的jar。java啓動類加載器不會加載這些jar,這樣雙親委託模型就無法滿足SPI的要求,而通過線程上下文類加載器,就可以實現由線程類加載器來實現對接口實現類的加載。這種場景在項目中很少遇到,但是在框架中肯定會用到。
ContextClassLoader就是打破雙親委託模型的限制。當高層提供統一的接口讓低層實現時候,就必須通過線程上下文類加載器來幫助高層ClassLoader找到並加載該類

類的聲明週期

在java代碼中,類的加載、連接、初始化都是在程序運行期間完成的(類型指的是類本身,不是類對象)。
 類的生命週期分爲加載、連接、初始化、使用、卸載。連接又分爲驗證、準備、解析。

 

1.類的加載
類的加載指的是將類的.class文件中的二進制數據讀取到內存中,並放在運行時數據區的方法區內。然後再堆內存中創建一個java.lang.Class對象(jvm規範餅沒有規定Class對象位於們哪裏)用來封裝類再方法區中的數據結構。java編譯器爲他編譯的每一個類至少生成一個實例初始化方法,在java的class文件中,這個實例初始化方法被稱之爲<init>,針對源代碼中的每一個類的構造方法,java編譯器都會產生一個<init>方法。類的加載最終是位於內存中的Class對象,Class對象封裝了類在方法區內的數據結構,並向java程序員提供了訪問方法區內的數據結構的接口。
加載的方式
 通過網絡文件下載.class文件
 從本地系統中直接加載
 從zip、jar等歸檔文件加載
 從系統編譯的文件中加載等
2.鏈接
1)類的驗證:類文件結構檢查、語義監察、字節碼驗證、二進制兼容性驗證
2)準備:爲類變量分配內存、設置默認值,但是在到達初始化之前,類變量都沒有初始化爲正確的值爲類的靜態變量分配內存,並將其初始化爲默認值。
     eg:public static int a = 5; 準備階段會爲其分配內存,並初始化值爲0。
3)解析:把類中的符號引用轉換成直接引用。解析過程就是在類型的常量池中尋找類、接口、字段和方法的付好引用,把付好引用替換成直接引用
3.初始化:爲類變量賦予正確的初始值。 eg:此時會將a的值賦予爲5。在初始化階段,java虛擬機執行類的初始化語句,爲類的靜態變量賦予初始值。在程序中,靜態變量的初始化有兩種途徑,1.靜態變量生命處初始化,2.靜態代碼塊中初始化.靜態變量的聲明語句,以及靜態代碼塊都被看做類的初始化語句,java虛擬機會按照初始化語句在文件中的先後順序來一次執行他們。
4.使用:類實例化爲新的對象分配內存,爲實例變量賦予默認值,爲實例變量賦予爭取的初始值。在程序中使用類
5.卸載:字節碼加載到內存以後形成自己的數據結構駐留在內存中,還會從內存中消除。這就是卸載。卸載以後這個類就不能使用。必須重新加載。

類的使用

java程序對類的使用分爲兩種
    —主動使用
    —被動使用
所有的java虛擬機實現必須在每個類或者接口被java程序“首次主動使用”時才初始化他們
主動使用分爲7種
 1)創建類的實例
2)訪問某個類或者接口的靜態變量或者對該靜態變量賦值(getstatic得到靜態變量、putstatic爲靜態變量賦值、invokestatic調用靜態方法)
3)調用類的靜態方法
4)反射(Class.forName("com.mysql.jdbc"))
5)初始化一個類的子類
6)一個類啓動的時候標明是啓動類例如main、javaTest
7)1.7以後動態語言支持
除了上面的7種情況被稱爲主動調用,其他情況都成爲被動調用。被動條用不會初始化類但是並不代表不會加載類。

類的初始化步驟


假如這個類還沒有被加載和鏈接,那就閒進行加載和鏈接
假如類存在直接父類,父類還沒有初始化,那就先初始化父類
假如類中存在初始化語句,那麼一次執行這些初始化語句
當java虛擬機初始化一個類的時候,要求所有的父類都已經被初始化,但是並不適合接口
當初始化一個類時,並不會先初始化他所實現的接口
當初始化一個接口時,並不會先初始化父接口
因此一個父接口並不會因爲他的子接口或者實現類的初始化而初始化,只有當程序首次使用特定接口的靜態變量時,纔會導致該接口的初始化
 

案例1

對於靜態字段來說,只有直接定義了該類的字段纔會初始化, 一個子類再初始化的時候,要求它的父類已經初始化完畢

public class MyTest1 {
    public static void main(String[] args) {
        //System.out.println(Child.str);
        System.out.println(Child.str2);
    }
}
class Parent1{
    public static String str = "hello word";
    static{
        System.out.println("parent靜態代碼塊1");
    }
}
class Child extends Parent1{
    public static String str2 = "welcome";
    static {
        System.out.println("child靜態代碼塊1");
    }
}

結果:
Child.str1
  parent靜態代碼塊1
  hello word
Child.str2
  parent靜態代碼塊1
  child靜態代碼塊1
  welcome
原因:
Child.str1調用的是父類中的類變量,因此根據類的主動使用不會造成Child的初始化
Child.str2調用的是自己的類變量,因此根據類的主動使用類初始化會造成所有的父類初始化
 

案例2

常量在編譯階段會放入該類的常量池中

**
 * 常量再編譯階段會將常量放入調用該常量方法所在類的常量池中
 * 本質上調用類並沒用直接引用定義常量的類,因此沒有觸發定義常量類的初始化
 * 這裏會將常量放入MyTest2類的常量池中與Parent2沒有任何關係,甚至可以將Parent2類的.class刪除
 */
public class MyTest2 {
    public static void main(String[] args) {
        System.out.println(Parent2.str);
    }
}
class Parent2{
    public static final String str = "hello Parent2";
    static{
        System.out.println("parent2 str");
    }
}

結果:
hello Parent2
原因:
常量再編譯階段會將常量放入調用該常量方法所在類的常量池中
本質上調用類並沒用直接引用定義常量的類,因此沒有觸發定義常量類的初始化
這裏會將常量放入MyTest2類的常量池中與Parent2沒有任何關係,甚至可以將Parent2類的.class刪除
 

案例3

public static final String str 對應隨機數

/**
 * UUID.randomUUID().toString()只有在運行期才知道的會初始化類
 * 當一個常量在編譯期間可以確定的,那麼其值就會放入該類的常量池中
 * 如果一個常量在編譯期間不能確定,那麼就會導致主動使用常量所在的類 ,所以這個類會被初始化
 */
public class MyTest3 {
    public static void main(String[] args) {
        System.out.println(Parent3.str);
    }
}
class Parent3{
    public static final String str = UUID.randomUUID().toString();
    static {
        System.out.println("parent3 static block");
    }
}

結果:
parent3 static block
9394bf16-2f8a-4d7e-a73c-114e0dff1d77
原因:
 UUID.randomUUID().toString()只有在運行期才知道的會初始化類
 當一個常量在編譯期間可以確定的,那麼其值就會放入該類的常量池中
  如果一個常量在編譯期間不能確定,那麼就會導致主動使用常量所在的類 ,所以這個類會被初始化

 

案例3

初始化順序

public class MyTest6 {
    public static void main(String[] args) {
        Singleton singleton = Singleton.getSingleton();
        System.out.println(singleton.a);
        System.out.println(singleton.b);
    }
}
class Singleton{
    public static int a;
    private static Singleton singleton = new Singleton();
    private Singleton(){
        a++;
        b++;
    }
    public static int b = 0;

    public static Singleton getSingleton(){
        return singleton;
    }
}

結果:
1
0
原因:
初始化順序由上至下執行,    private static Singleton singleton = new Singleton();
時候a = 1 b = 1。 public static int b = 0; 因此爲1.0
 

案例4

接口初始化

public class MyTest7 {
    public static void main(String[] args) {
        System.out.println(Parent7.thread);
        System.out.println("====================");
        System.out.println(Child7.b);
    }
}
interface GrandPa7{
    public static int a = 5;
    public static Thread thread = new Thread(){
        {
            System.out.println("GrandPa block");
        }
    };
}
interface Parent7 extends GrandPa7{
    public static int a = 5;
    public static Thread thread = new Thread(){
        {
            System.out.println("MyParent7 block");
        }
    };
}
class Child7 implements Parent7{
    public static int b = 5;
}

結果:
MyParent7 block
Thread[Thread-0,5,main]
====================
5
原因:
當初始化一個類時,並不會先初始化他所實現的接口
當初始化一個接口時,並不會先初始化父接口

案例5

父類初始化

public class MyTest8 {
    public static void main(String[] args) {
        System.out.println(Child8.a);
        Child8.doSoming();
    }
}
class Parent8{
    public static  int a = 5;
    static {
        System.out.println("Parent8 block");
    }
    static void doSoming(){
        System.out.println("do someing");
    }
}
class Child8 extends Parent8{
    static {
        System.out.println("Parent8 block");
    }
}

結果:
Parent8 block
5
do someing
原因:
當初始化一個類時,並首先初始父類
 

案例6

類的主動使用

public class MyTest9 {
    public static void main(String[] args) throws Exception{
        ClassLoader loader = ClassLoader.getSystemClassLoader();
        Class clazz = loader.loadClass("com.jvmsource.classload.CL");
        System.out.println(clazz);
        System.out.println("==================");
        clazz = Class.forName("com.jvmsource.classload.CL");
        System.out.println(clazz);

    }
}

class CL{
    public static  int a = 5;
    static {
        System.out.println("CL static");
    }
}

結果:
class com.jvmsource.classload.CL
==================
CL static
class com.jvmsource.classload.CL
原因:
 調用ClassLoader類的loadClass方法並不會造成類的主動使用,不會導致類的初始化。
  Class.forName會造成類的主動使用,導致類的初始化


案例7_1

數組的初始化


/**
 * 數組的創建並不會導致具體引用類型的初始化
 * 對於數組實例來說,其類型是在jvm在運行期間生成的,表示class [Lcom.jvmsource.classload.MyTest4
 * 這種形式,動態生成的類型,其父類就是Object
 * 對於數組來說,JavaDoc經常將構成數組的元素稱之爲Component,實際上就是將數組降低一個維度後的類型
 * anewarray 表示創建一個引用類型的(類、接口、數組)數組,並將其引用值壓入棧頂
 * newarray表示創建一個原始類型的(int float double)數組,並將其壓入棧頂
 */
public class MyTest4 {
    public static void main(String[] args) {
        MyTest4[] myTests = new MyTest4[1];
        System.out.println(myTests.getClass());
    }
}
class Parent4{
    static{
        System.out.println("parent4 static bolck");
    }
}

結果:
class [Lcom.jvmsource.classload.MyTest4;
原因:
數組的創建並不會導致具體引用類型的初始化
對於數組實例來說,其類型是在jvm在運行期間生成的

案例7_2

數組的初始化


public class MyTest11 {
    public static void main(String[] args) {
        /**
         * ClassLoader負責加載一個類的對象,每一個Class對象都包含一個定義他的ClassLoader
         * 數組類的Classd對象不是由類加載器創建,而是有java運行時根據需要自動創建,
         * 數組類的類加載器有Class.getClassLoader返回,該加載器與其元素的類型的類加載器是相同的
         * 如果該元素類型爲基本類型,則數組類沒有類加載器爲null
         */
        //類加載器爲根類加載器
        String [] strArray = new String[2];
        System.out.println(strArray.getClass().getClassLoader());
        //類加載器爲根類加載器
        MyTest11 [] myArray = new MyTest11[2];
        System.out.println(myArray.getClass().getClassLoader());
        //沒有類加載器
        int [] intArray = new int[2];
        System.out.println(intArray.getClass().getClassLoader());

    }
}

 

結果:
null
sun.misc.Launcher$AppClassLoader@18b4aac2
null

原因:
兩個null不同,第一個null是根類加載器,第二個null是基本類型沒有類加載器
 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章