深入理解和探究Java類載入機制 1.java.lang.ClassLoader類介紹 java.lang.ClassLoader類的基本職責就是根據一個指定的類的名稱,找到或者生成其對應的位元組代碼,然後從這些位元組代碼中定義出一個Java 類,即 java.lang.Class類的一個實例。 Cla ...
深入理解和探究Java類載入機制----
1.java.lang.ClassLoader類介紹
java.lang.ClassLoader類的基本職責就是根據一個指定的類的名稱,找到或者生成其對應的位元組代碼,然後從這些位元組代碼中定義出一個Java 類,即 java.lang.Class類的一個實例。
ClassLoader提供了一系列的方法,比較重要的方法如:
2.JVM中類載入器的樹狀層次結構
Java 中的類載入器大致可以分成兩類,一類是系統提供的,另外一類則是由 Java 應用開發人員編寫的。
引導類載入器(bootstrap class loader):
它用來載入 Java 的核心庫(jre/lib/rt.jar),是用原生C++代碼來實現的,並不繼承自java.lang.ClassLoader。
載入擴展類和應用程式類載入器,並指定他們的父類載入器,在java中獲取不到。
擴展類載入器(extensions class loader):
它用來載入 Java 的擴展庫(jre/ext/*.jar)。Java 虛擬機的實現會提供一個擴展庫目錄。該類載入器在此目錄裡面查找並載入 Java 類。
系統類載入器(system class loader):
它根據 Java 應用的類路徑(CLASSPATH)來載入 Java 類。一般來說,Java 應用的類都是由它來完成載入的。可以通過 ClassLoader.getSystemClassLoader()來獲取它。
自定義類載入器(custom class loader):
除了系統提供的類載入器以外,開發人員可以通過繼承 java.lang.ClassLoader類的方式實現自己的類載入器,以滿足一些特殊的需求。
以下測試代碼可以證明此層次結構:
public class testClassLoader { @Test public void test(){ //application class loader System.out.println(ClassLoader.getSystemClassLoader()); //extensions class loader System.out.println(ClassLoader.getSystemClassLoader().getParent()); //bootstrap class loader System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent()); } }
輸出為:
可以看出ClassLoader類是由AppClassLoader載入的。他的父親是ExtClassLoader,ExtClassLoader的父親無法獲取是因為它是用C++實現的。
3.雙親委派機制
某個特定的類載入器在接到載入類的請求時,首先將載入任務委托交給父類載入器,父類載入器又將載入任務向上委托,直到最父類載入器,如果最父類載入器可以完成類載入任務,就成功返回,如果不行就向下傳遞委托任務,由其子類載入器進行載入。
雙親委派機制的好處:
保證java核心庫的安全性(例如:如果用戶自己寫了一個java.lang.String類就會因為雙親委派機制不能被載入,不會破壞原生的String類的載入)
代理模式
與雙親委派機制相反,代理模式是先自己嘗試載入,如果無法載入則向上傳遞。tomcat就是代理模式。
4.自定義類載入器
public class MyClassLoader extends ClassLoader{ private String rootPath; public MyClassLoader(String rootPath){ this.rootPath = rootPath; } @Override protected Class<?> findClass(String name) throws ClassNotFoundException { //check if the class have been loaded Class<?> c = findLoadedClass(name); if(c!=null){ return c; } //load the class byte[] classData = getClassData(name); if(classData==null){ throw new ClassNotFoundException(); } else{ c = defineClass(name,classData, 0, classData.length); return c; } } private byte[] getClassData(String className){ String path = rootPath+"/"+className.replace('.', '/')+".class"; InputStream is = null; ByteArrayOutputStream bos = null; try { is = new FileInputStream(path); bos = new ByteArrayOutputStream(); byte[] buffer = new byte[1024]; int temp = 0; while((temp = is.read(buffer))!=-1){ bos.write(buffer,0,temp); } return bos.toByteArray(); } catch (Exception e) { e.printStackTrace(); }finally{ try { is.close(); bos.close(); } catch (Exception e) { e.printStackTrace(); } } return null; } }
測試自定義的類載入器
創建一個測試類HelloWorld
package testOthers; public class HelloWorld { }
在D盤根目錄創建一個testOthers文件夾,編譯HelloWorld.java,將得到的class文件放到testOthers文件夾下。
利用如下代碼進行測試
public class testMyClassLoader { @Test public void test() throws Exception{ MyClassLoader loader = new MyClassLoader("D:"); Class<?> c = loader.loadClass("testOthers.HelloWorld"); System.out.println(c.getClassLoader()); } }
輸出:
說明HelloWorld類是被我們的自定義類載入器MyClassLoader載入的
5.類載入過程詳解
JVM將類載入過程分為三個步驟:裝載(Load),鏈接(Link)和初始化(Initialize)
1) 裝載:
查找並載入類的二進位數據;
2)鏈接:
驗證:確保被載入類信息符合JVM規範、沒有安全方面的問題。
準備:為類的靜態變數分配記憶體,並將其初始化為預設值。
解析:把虛擬機常量池中的符號引用轉換為直接引用。
3)初始化:
為類的靜態變數賦予正確的初始值。
ps:解析部分需要說明一下,Java 中,虛擬機會為每個載入的類維護一個常量池【不同於字元串常量池,這個常量池只是該類的字面值(例如類名、方法名)和符號引用的有序集合。 而字元串常量池,是整個JVM共用的】這些符號(如int a = 5;中的a)就是符號引用,而解析過程就是把它轉換成指向堆中的對象地址的相對地址。
類的初始化步驟:
1)如果這個類還沒有被載入和鏈接,那先進行載入和鏈接
2)假如這個類存在直接父類,並且這個類還沒有被初始化(註意:在一個類載入器中,類只能初始化一次),那就初始化直接的父類(不適用於介面)
3)如果類中存在static標識的塊,那就依次執行這些初始化語句。