咱們知道class文件中存儲了類的描述信息和各類細節的數據,在運行Java程序時,虛擬機須要先將類的這些數據加載到內存中,並通過校驗、轉換、解析和初始化事後,最終造成能夠直接使用的Java類型。java
類從被加載到虛擬機內存中開始,到卸載出內存爲止,它的整個生命週期包括:加載、驗證、準備、解析、初始化、使用和卸載7個階段。其中驗證、準備、解析3個部分統稱爲鏈接。
android
類的加載機制實際上就是類的生命週期中加載、驗證、準備、解析、初始化5個過程。c++
加載是類的加載過程的第一個階段,在加載階段,虛擬機須要完成如下3件事情:程序員
java.lang.Class
對象,做爲方法區這個類的各類數據的訪問入口。經過全限定名來獲取二進制流能夠有不少種方式,好比從JAR、EAR、WAR文件包中讀取,從網絡獲取,也能夠由其餘文件來生成(jsp文件生成對應的Servlet類),甚至還能夠經過運行時動態生成(Java動態代理)。安全
相比類加載過程的其餘階段,加載階段是可控性最強的。由於開發者既能夠利用系統提供的啓動類加載器來完成,也能夠經過自定義類加載去完成(重寫loadClass
方法,控制字節流的獲取方式)。bash
關於類加載器的詳細介紹將放在文章最後。網絡
加載階段完成後,虛擬機外部的二進制字節流就按照虛擬機所需的格式存儲在方法區之中。而後在內存中實例化一個java.lang.Class
類的對象,這樣就能夠經過這個對象來訪問方法區中的這些數據。數據結構
驗證是鏈接階段的第一步,這一階段的目的是爲了確保class文件的字節流中包含的信息符合當前虛擬機的要求,而且不會危害虛擬機自身的安全。驗證階段大體上會完成下面4個階段的檢驗動做:文件格式驗證、元數據驗證、字節碼驗證、符號引用驗證。多線程
準備階段是正式爲類變量(靜態變量)分配內存並設置初始值的階段,這些類變量所使用的內存都將在方法區中進行分配。jsp
這裏有兩點須要注意:
好比:
public class Test {
public int number = 111;
public static int sNumber = 111;
}複製代碼
成員變量number
在這個階段就不會進行內存分配和初始化。而類變量sNunber
會在方法區中分配內存,並設置爲int類型的零值0而不是111,賦值爲111是在初始化階段纔會執行。
好比:
public class Test {
public static final int NUMBER = 111;
}複製代碼
此時,就會在準備階段將NUMBER
的值設置爲111。
解析階段是虛擬機將常量池內的符號引用替換爲直接引用的過程。
解析動做主要就是在常量池中尋找類或接口、字段、類方法、接口方法、方法類型、方法句柄、調用點限定符等7類符號引用,把這些符號引用替換爲直接引用。下面主要介紹下類或接口、字段、類方法、接口方法的解析:
A
經過符號X引用了類B
,虛擬機會把表明類B
的全限定名傳遞給A
的類加載器去加載B
,B
通過加載、驗證、準備過程,在解析過程又可能會觸發B
引用的其餘的類的加載過程,至關於一個類引用鏈的遞歸加載過程,整個過程只要不出現異常,B
的就是一個加載成功的類或接口了,也就是能夠獲取到表明B
的java.lang.Class
對象。在驗證了A
具有對B
的訪問權限後,就將符號引用X替換爲B
的直接引用。類的初始化類加載過程的最後一步,在前面的過中,除了在加載階段開發者能夠自定義加載器以外,其他的動做都是徹底有虛擬機主導和控制完成。到了初始化階段,才真正開始執行類中定義的Java代碼。
在準備階段,類變量已經設置了系統要求的零值,而在初始化階段,則根據程序員經過程序制定的主觀計劃去初始化類變量和其餘資源,或者能夠從另一個角度來表達:初始化階段是執行類構造器<clinit>()
方法的過程。
<clinit>()
方法是由編譯器自動收集類中全部的類變量(static
變量)和靜態代碼塊(static{}
塊)中的語句合併生成的。編譯器收集的順序是由語句在源文件中出現的順序所決定的,靜態代碼塊中只能訪問到定義在靜態代碼塊以前的變量,定義在它以後的變量,在前面的靜態代碼塊能夠賦值,可是不能訪問。
public class Test {
static {
number = 111; // 能夠賦值
System.out.println(number); // 不能讀取,編輯器或報錯Illegal forward reference
}
static int number;
}複製代碼
<clinit>()
方法與類的構造函數(或者說實例構造器<init>()
方法)不一樣,它不須要顯式地調用父類的<clinit>()
方法,虛擬機會保證在子類的<clinit>()
方法執行以前,父類的<clinit>()
方法已經執行完畢。因此,父類定義的靜態代碼塊要先與子類的賦值操做。
class Parent {
public static int A = 1;
static {
A = 2;
}
}
class Sub extends Parent {
public static int B = A;
public static void main(String[] args) {
System.out.println(Sub.B);
}
}複製代碼
<clinit>()
方法對於類或接口來講並非必需的,若是一個類中沒有靜態語句塊,也沒有對變量的賦值操做,那麼編譯器能夠不爲這個類生成<clinit>()
方法。
接口中不能使用靜態語句塊,但仍然有變量初始化的賦值操做,所以接口與類同樣都會生成<clinit>()
方法。但接口與類不一樣的是,執行接口的<clinit>()
方法不須要先執行父接口的<clinit>()
方法。只有當父接口中定義的變量使用時,父接口才會初始化。另外,接口的實現類在初始化時也同樣不會執行接口的<clinit>()
方法。
虛擬機會保證一個類的<clinit>()
方法在多線程環境中被正確地加鎖、同步,若是多個線程同時去初始化一個類,那麼只會有一個線程去執行這個類的<clinit>()
方法,其餘線程都須要阻塞等待,直到活動線程執行<clinit>()
方法完畢。若是在一個類的<clinit>()
方法中有耗時很長的操做,就可能形成多個進程阻塞。
在以前的加載過程當中,提到了類加載器經過一個類的全限定名來獲取描述此類的二進制字節流,這個過程可讓開發中自定義類加載器來決定如何獲取須要的字節流。那麼,什麼是類加載器呢?
對於任意一個Java類,都必須經過類加載器加載到方法區,並生成java.lang.Class
對象才能使用類的各個功能,因此咱們能夠把類加載器理解爲一個將class
類文件轉換爲java.lang.Class
對象的工具。
對於任意一個類,都須要由加載它的類加載器和這個類自己一同確立其在Java虛擬機中的惟一性,每個類加載器,都擁有一個獨立的類名稱空間。也就是說,若是兩個類「相等」,那麼這兩個類必須是被同一個虛擬機中的同一個類加載器加載,而且來自同一個class
文件。
在Java當中,已經有3個預製的類加載器,分別是BootStrapClassLoader
、ExtClassLoader、AppClassLoader
。
ExtClassLoader
做爲類加載器,但它也是一個Java類,是由BootStrapClassLoader
來加載的,因此,ExtClassLoader
的parent是BootStrapClassLoader
。可是因爲BootStrapClassLoader
是c++
實現的,咱們經過ExtClassLoader.getParent
獲取到的是null
。一樣地,AppClassLoader
是由ExtClassLoader
加載,AppClassLoader
的parent是ExtClassLoader
。
public class Test {
public static void main(String[] args) {
ClassLoader cl = Test.class.getClassLoader();
while (cl != null) {
System.out.println(cl);
cl = cl.getParent();
}
}
}複製代碼
打印結果:
sun.misc.Launcher$AppClassLoader@232204a1
sun.misc.Launcher$ExtClassLoader@74a14482複製代碼
同時咱們能夠定義本身的類加載器CustomClassLoader
,那麼它的parent確定就是AppClassLoader
了。類加載器的這種層次關係稱爲雙親委派模型。
雙親委派模型要求除了頂層的啓動類加載器外,其他的類加載器都應當有本身的父類加載器。這裏類加載器之間的父子關係不是以繼承的關係來實現,而是都使用遞歸的方式來調用父加載器的代碼。
雙親委派模型的工做過程是:若是一個類加載器收到了類加載的請求,它首先不會本身去嘗試加載這個類,而是把這個請求委派給父類加載器去完成,每個層次的類加載器都是如此,所以全部的加載請求最終都應該傳送到頂層的啓動類加載器中,只有當父加載器反饋本身沒法完成這個加載請求(它的搜索範圍中沒有找到所需的類)時,子加載器纔會嘗試本身去加載。
ClassLoader的源碼:
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;
}
}複製代碼
先檢查是否已經被加載過,若沒有加載則調用父類加載器的loadClass()
方法,依次向上遞歸。若父類加載器爲空則說明遞歸到啓動類加載器了。若是從父類加載器到啓動類加載器的上層次的全部加載器都加載失敗,則調用本身的findClass()
方法進行加載。
使用雙親委派模型能使Java類隨着加載器一塊兒具有一種優先級的層次關係,保證同一個類只加載一次,避免了重複加載,同時也能阻止有人惡意替換加載系統類。
通常地,在ClassLoader
方法的loadClass
方法中已經給開發者實現了雙親委派模型,在自定義類加載器的時候,只須要複寫findClass
方法便可。
public class CustomClassLoader extends ClassLoader {
private String root;
public CustomClassLoader(String root) {
this.root = root;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = loadClassData(name);
if (classData == null) {
throw new ClassNotFoundException();
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] loadClassData(String name) {
String fileName = root + File.separatorChar
+ name.replace('.', File.separatorChar)
+ ".class";
try {
InputStream ins = new FileInputStream(fileName);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
int bufferSize = 1024;
byte[] buffer = new byte[bufferSize];
int length;
while ((length = ins.read(buffer)) != -1) {
baos.write(buffer, 0, length);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
}複製代碼
新建一個類com.xiao.U
,編譯成class文件,放到桌面,來測試一下:
public class Test {
public static void main(String[] args) {
CustomClassLoader customClassLoader = new CustomClassLoader("C:\\Users\\PC\\Desktop");
try {
Class clazz = customClassLoader.loadClass("com.xiao.U");
Object o = clazz.newInstance();
System.out.println(o.getClass().getClassLoader());
} catch (ClassNotFoundException | IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
}
}複製代碼
打印結果:
CustomClassLoader@1540e19d複製代碼
自定義類加載器在能夠實現服務端的熱部署,在移動端好比android也能夠實現熱更新。
參考: