1 前言:
在上一篇文章一文讓你明白Java字節(jié)碼中,
我們了解了java字節(jié)碼的解析過程,那么在接下來的內(nèi)容中,我們來了解一下類的加載機制。
2 題外話
Java的核心是什么?當然是JVM了,所以說了解并熟悉JVM對于我們理解Java語言非常重要,不管你是做Java還是Android,熟悉JVM是我們每個Java、Android開發(fā)者必不可少的技能。如果你現(xiàn)在覺得Android的開發(fā)到了天花板的地步,那不妨往下走走,一起探索JAVA層面的內(nèi)容。如果我們不了解自己寫的代碼是如何被執(zhí)行的,那么我們只是一個會寫代碼的程序員,我們知其然不知其所以然。看到很多人說現(xiàn)在工作難找,真是這樣嗎?如果我們足夠優(yōu)秀,工作還難找嗎?如果我們底子足夠深,需要找工作嗎?找不到工作多想想自己的原因,總是抱怨環(huán)境是沒有用的,因為你沒辦法去改變壞境。如果我們一直停留在框架層面,停留在新的功能層面,那么我們的優(yōu)勢在哪里呢?所以說,我們不僅要學(xué)會寫代碼,還要知道為什么這樣寫代碼,這才是我們的核心競爭力之一。這樣我們的差異化才能夠體現(xiàn)出來,不信?我們走著瞧......我們第一個差異化就是對JVM的掌握,而今天的內(nèi)容類加載機制是JVM比較核心的部分,如果你想和別人不一樣,那就一起仔細研究研究這次的內(nèi)容吧。
3 引子
為了看看自己是否掌握了類加載機制,我們看看一道題:
public class Singleton {
private static Singleton singleton = new Singleton();
public static int counter1;
public static int counter2 = 0;
private Singleton() {
counter1++;
counter2++;
}
public static Singleton getSingleton() {
return singleton;
}
}
上面是一個Singleton類,有3個靜態(tài)變量,下面是一個測試類,打印出靜態(tài)屬性的值,就是這么簡單。
public class TestSingleton {
public static void main(String args[]){
Singleton singleton = Singleton.getSingleton();
System.out.println("counter1="+singleton.counter1);
System.out.println("counter2="+singleton.counter2);
}
}
在往下看之前,大家先看看這道題的輸出是啥?如果你清楚知道為什么,那么說明你掌握了類的加載機制,往下看或許有不一樣的收獲;如果你不懂,那就更要往下看了。我們先不講這道題,待我們了解了類的加載機制之后,回過頭看看這道題,或許有恍然大悟的感覺,或許講完之后你會懷疑自己是否真正了解Java,或許你寫了這么多年的Java都不了解它的執(zhí)行機制,是不是很丟人呢?不過沒關(guān)系,馬上你就不丟人了。
4 正題
下面我們具體了解類的加載機制。
1)加載
2)連接(驗證-準備-解析)
3)初始化
JVM就是按照上面的順序一步一步的將字節(jié)碼文件加載到內(nèi)存中并生成相應(yīng)的對象的。首先將字節(jié)碼加載到內(nèi)存中,然后對字節(jié)碼進行連接,連接階段包括了驗證準備解析這3個步驟,連接完畢之后再進行初始化工作。下面我們一一了解:
5 首先我們了解一下加載
5.1 什么是類的加載?
類的加載指的是將類的.class文件中的二進制數(shù)據(jù)讀入內(nèi)存中,將其放在運行時數(shù)據(jù)區(qū)域的方法去內(nèi),然后在堆中創(chuàng)建java.lang.Class對象,用來封裝類在方法區(qū)的數(shù)據(jù)結(jié)構(gòu).只有java虛擬機才會創(chuàng)建class對象,并且是一一對應(yīng)關(guān)系.這樣才能通過反射找到相應(yīng)的類信息.
我們上面提到過Class這個類,這個類我們并沒有new過,這個類是由java虛擬機創(chuàng)建的。通過它可以找到類的信息,我們來看下源碼:
/*
* Constructor. Only the Java Virtual Machine creates Class
* objects.
*/
private Class() {}
從上面貼出的Class類的構(gòu)造方法源碼中,我們知道這個構(gòu)造器是私有的,并且只有虛擬機才能創(chuàng)建這個類的對象。
5.2 什么時候?qū)︻愡M行加載呢?
Java虛擬機有預(yù)加載功能。類加載器并不需要等到某個類被"首次主動使用"時再加載它,JVM規(guī)范規(guī)定JVM可以預(yù)測加載某一個類,如果這個類出錯,但是應(yīng)用程序沒有調(diào)用這個類, JVM也不會報錯;如果調(diào)用這個類的話,JVM才會報錯,(LinkAgeError錯誤)。其實就是一句話,Java虛擬機有預(yù)加載功能。
6 類加載器
講到類加載,我們不得不了解類加載器.
6.1 什么是類加載器?
類加載器負責對類的加載。
6.2 Java自帶有3種類加載器
1)根類加載器,使用c++編寫(BootStrap),負責加載rt.jar
2)擴展類加載器,java實現(xiàn)(ExtClassLoader)
3)應(yīng)用加載器,java實現(xiàn)(AppClassLoader) classpath
根類加載器,是用c++實現(xiàn)的,我們沒有辦法在java層面看到;我們接下來看看ExtClassLoader的代碼,它是在Launcher類中,
static class ExtClassLoader extends URLClassLoader
同時我們看看AppClassLoader,它也是在Launcher中,
static class AppClassLoader extends URLClassLoader
他們同時繼承一個類URLClassLoader。
關(guān)于這種層次關(guān)系,看起來像繼承,其實不是的。我們看到上面的代碼就知道ExtClassLoader和AppClassLoader同時繼承同一個類。同時我們來看下ClassLoader的loadClass方法也可以知道,下面貼出源代碼:
private final ClassLoader parent;
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
}
return c;
}
}
源碼沒有全部貼出,只是貼出關(guān)鍵代碼。從上面代碼我們知道首先會檢查class是否已經(jīng)加載了,如果已經(jīng)加載那就直接拿出,否則再進行加載。其中有一個parent屬性,就是表示父加載器。這點正好說明了加載器之間的關(guān)系并不是繼承關(guān)系。
6.3 雙親委派機制
關(guān)于類加載器,我們不得不說一下雙親委派機制。聽著很高大上,其實很簡單。比如A類的加載器是AppClassLoader(其實我們自己寫的類的加載器都是AppClassLoader),AppClassLoader不會自己去加載類,而會委ExtClassLoader進行加載,那么到了ExtClassLoader類加載器的時候,它也不會自己去加載,而是委托BootStrap類加載器進行加載,就這樣一層一層往上委托,如果Bootstrap類加載器無法進行加載的話,再一層層往下走。
上面的源碼也說明了這點。
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
6.4 為何要雙親委派機制
對于我們技術(shù)來講,我們不但要知其然,還要知其所以然。為何要采用雙親委派機制呢?了解為何之前,我們先來說明一個知識點:
判斷兩個類相同的前提是這兩個類都是同一個加載器進行加載的,如果使用不同的類加載器進行加載同一個類,也會有不同的結(jié)果。
如果沒有雙親委派機制,會出現(xiàn)什么樣的結(jié)果呢?比如我們在rt.jar中隨便找一個類,如java.util.HashMap,那么我們同樣也可以寫一個一樣的類,也叫java.util.HashMap存放在我們自己的路徑下(ClassPath).那樣這兩個相同的類采用的是不同的類加載器,系統(tǒng)中就會出現(xiàn)兩個不同的HashMap類,這樣引用程序就會出現(xiàn)一片混亂。
我們看一個例子:
public class MyClassLoader {
public static void main(String args[]) throws ClassNotFoundException, IllegalAccessException, InstantiationException {
ClassLoader loader = new ClassLoader() {
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
String fileName = name.substring(name.lastIndexOf(".")+1)+".class";
InputStream inputStream = getClass().getResourceAsStream(fileName);
if (inputStream==null)
return super.loadClass(name);
try {
byte[] bytes = new byte[inputStream.available()];
inputStream.read(bytes);
return defineClass(name,bytes,0,bytes.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException(name);
}
}
};
Object object = loader.loadClass("jvm.classloader.MyClassLoader").newInstance();
System.out.println(object instanceof jvm.classloader.MyClassLoader);
}
}
大家可以看看輸出的是什么?我們自己定義了一個類加載器,讓它去加載我們自己寫的一個類,然后判斷由我們寫的類加載器加載的類是否是MyClassLoader的一個實例。
答案是否定的。為什么?因為jvm.classloader.MyClassLoader是在classpath下面,是由AppClassLoader加載器加載的,而我們卻指定了自己的加載器,當然加載出來的類就不相同了。不信,我們將他的父類加載器都打印出來。在上面代碼中加入下面代碼:
ClassLoader classLoader = object.getClass().getClassLoader();
while (classLoader!=null){
System.out.println(classLoader);
classLoader = classLoader.getParent();
}
if (classLoader==null){
System.out.println("classLoader == null");
}
輸出內(nèi)容 :
jvm.classloader.MyClassLoader$1@60172ec6
sun.misc.Launcher$AppClassLoader@338bd37a
sun.misc.Launcher$ExtClassLoader@20e90906
classLoader == null
對比一下下面的代碼:
Object object2 = new MyClassLoader();
ClassLoader classLoader2 = object2.getClass().getClassLoader();
while (classLoader2!=null){
System.out.println(classLoader2);
classLoader2 = classLoader2.getParent();
}
if (classLoader2==null){
System.out.println("classLoader2 == null");
}
輸出內(nèi)容:
sun.misc.Launcher$AppClassLoader@20e90906
sun.misc.Launcher$ExtClassLoader@234f79cb
classLoader == null
第一個是我們自己加載器加載的類,第二個是直接new的一個對象,是由App類加載器進行加載的,我們把它們的父類加載器打印出來了,可以看出他們的加載器是不一樣的。很奇怪為何會執(zhí)行classloader==null這句話。其實classloader==null表示的就是根類加載器。我們看看Class.getClassLoader()方法源碼:
/**
* Returns the class loader for the class. Some implementations may use
* null to represent the bootstrap class loader. This method will return
* null in such implementations if this class was loaded by the bootstrap
* class loader.
**/
@CallerSensitive
public ClassLoader getClassLoader() {
ClassLoader cl = getClassLoader0();
if (cl == null)
return null;
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
ClassLoader.checkClassLoaderPermission(cl, Reflection.getCallerClass());
}
return cl;
}
從注釋中我們知道了,如果返回了null,表示的是bootstrap類加載器。
7 類的連接
講完了類的加載之后,我們需要了解一下類的連接。類的連接有三步,分別是驗證,準備,解析。下面讓我們一一了解
7.1 首先我們看看驗證階段。
驗證階段主要做了以下工作
-將已經(jīng)讀入到內(nèi)存類的二進制數(shù)據(jù)合并到虛擬機運行時環(huán)境中去。
-類文件結(jié)構(gòu)檢查:格式符合jvm規(guī)范-語義檢查:符合java語言規(guī)范,final類沒有子類,final類型方法沒有被覆蓋
-字節(jié)碼驗證:確保字節(jié)碼可以安全的被java虛擬機執(zhí)行.
二進制兼容性檢查:確保互相引用的類的一致性.如A類的a方法會調(diào)用B類的b方法.那么java虛擬機在驗證A類的時候會檢查B類的b方法是否存在并檢查版本兼容性.因為有可能A類是由jdk1.7編譯的,而B類是由1.8編譯的。那根據(jù)向下兼容的性質(zhì),A類引用B類可能會出錯,注意是可能。
7.2 準備階段
java虛擬機為類的靜態(tài)變量分配內(nèi)存并賦予默認的初始值.如int分配4個字節(jié)并賦值為0,long分配8字節(jié)并賦值為0;
7.3 解析階段
解析階段主要是將符號引用轉(zhuǎn)化為直接引用的過程。比如 A類中的a方法引用了B類中的b方法,那么它會找到B類的b方法的內(nèi)存地址,將符號引用替換為直接引用(內(nèi)存地址)。
到這里為止,我們知道了類的加載,類加載器,雙親委派機制,類的連接等等操作。那么接下來需要講的是類的初始化,初始化內(nèi)容較多,另開一篇文章深入理解Java類加載機制(二)講,這樣大家就不會疲勞和畏懼了。