1 前言:
在上一篇文章一文讓你明白Java字節(jié)碼中,
我們了解了java字節(jié)碼的解析過程昂验,那么在接下來的內(nèi)容中逛艰,我們來了解一下類的加載機(jī)制蘑志。
2 題外話
Java的核心是什么金砍?當(dāng)然是JVM了址否,所以說了解并熟悉JVM對于我們理解Java語言非常重要餐蔬,不管你是做Java還是Android,熟悉JVM是我們每個(gè)Java在张、Android開發(fā)者必不可少的技能用含。如果你現(xiàn)在覺得Android的開發(fā)到了天花板的地步,那不妨往下走走帮匾,一起探索JAVA層面的內(nèi)容啄骇。如果我們不了解自己寫的代碼是如何被執(zhí)行的,那么我們只是一個(gè)會寫代碼的程序員瘟斜,我們知其然不知其所以然缸夹。看到很多人說現(xiàn)在工作難找螺句,真是這樣嗎虽惭?如果我們足夠優(yōu)秀,工作還難找嗎蛇尚?如果我們底子足夠深芽唇,需要找工作嗎?找不到工作多想想自己的原因取劫,總是抱怨環(huán)境是沒有用的匆笤,因?yàn)槟銢]辦法去改變壞境。如果我們一直停留在框架層面谱邪,停留在新的功能層面炮捧,那么我們的優(yōu)勢在哪里呢?所以說惦银,我們不僅要學(xué)會寫代碼咆课,還要知道為什么這樣寫代碼末誓,這才是我們的核心競爭力之一。這樣我們的差異化才能夠體現(xiàn)出來书蚪,不信喇澡?我們走著瞧......我們第一個(gè)差異化就是對JVM的掌握,而今天的內(nèi)容類加載機(jī)制是JVM比較核心的部分善炫,如果你想和別人不一樣撩幽,那就一起仔細(xì)研究研究這次的內(nèi)容吧。
3 引子
為了看看自己是否掌握了類加載機(jī)制箩艺,我們看看一道題:
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;
}
}
上面是一個(gè)Singleton類窜醉,有3個(gè)靜態(tài)變量,下面是一個(gè)測試類艺谆,打印出靜態(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);
}
}
在往下看之前静汤,大家先看看這道題的輸出是啥琅催?如果你清楚知道為什么,那么說明你掌握了類的加載機(jī)制虫给,往下看或許有不一樣的收獲藤抡;如果你不懂,那就更要往下看了抹估。我們先不講這道題缠黍,待我們了解了類的加載機(jī)制之后,回過頭看看這道題药蜻,或許有恍然大悟的感覺瓷式,或許講完之后你會懷疑自己是否真正了解Java,或許你寫了這么多年的Java都不了解它的執(zhí)行機(jī)制语泽,是不是很丟人呢贸典?不過沒關(guān)系,馬上你就不丟人了踱卵。
4 正題
下面我們具體了解類的加載機(jī)制廊驼。
1)加載
2)連接(驗(yàn)證-準(zhǔn)備-解析)
3)初始化
JVM就是按照上面的順序一步一步的將字節(jié)碼文件加載到內(nèi)存中并生成相應(yīng)的對象的。首先將字節(jié)碼加載到內(nèi)存中惋砂,然后對字節(jié)碼進(jìn)行連接妒挎,連接階段包括了驗(yàn)證準(zhǔn)備解析這3個(gè)步驟,連接完畢之后再進(jìn)行初始化工作班利。下面我們一一了解:
5 首先我們了解一下加載
5.1 什么是類的加載?
類的加載指的是將類的.class文件中的二進(jìn)制數(shù)據(jù)讀入內(nèi)存中,將其放在運(yùn)行時(shí)數(shù)據(jù)區(qū)域的方法去內(nèi),然后在堆中創(chuàng)建java.lang.Class對象,用來封裝類在方法區(qū)的數(shù)據(jù)結(jié)構(gòu).只有java虛擬機(jī)才會創(chuàng)建class對象,并且是一一對應(yīng)關(guān)系.這樣才能通過反射找到相應(yīng)的類信息.
我們上面提到過Class這個(gè)類榨呆,這個(gè)類我們并沒有new過罗标,這個(gè)類是由java虛擬機(jī)創(chuàng)建的庸队。通過它可以找到類的信息,我們來看下源碼:
/*
* Constructor. Only the Java Virtual Machine creates Class
* objects.
*/
private Class() {}
從上面貼出的Class類的構(gòu)造方法源碼中闯割,我們知道這個(gè)構(gòu)造器是私有的彻消,并且只有虛擬機(jī)才能創(chuàng)建這個(gè)類的對象。
5.2 什么時(shí)候?qū)︻愡M(jìn)行加載呢宙拉?
Java虛擬機(jī)有預(yù)加載功能宾尚。類加載器并不需要等到某個(gè)類被"首次主動使用"時(shí)再加載它,JVM規(guī)范規(guī)定JVM可以預(yù)測加載某一個(gè)類,如果這個(gè)類出錯(cuò)谢澈,但是應(yīng)用程序沒有調(diào)用這個(gè)類煌贴, JVM也不會報(bào)錯(cuò);如果調(diào)用這個(gè)類的話锥忿,JVM才會報(bào)錯(cuò)牛郑,(LinkAgeError錯(cuò)誤)。其實(shí)就是一句話敬鬓,Java虛擬機(jī)有預(yù)加載功能淹朋。
6 類加載器
講到類加載,我們不得不了解類加載器.
6.1 什么是類加載器钉答?
類加載器負(fù)責(zé)對類的加載础芍。
6.2 Java自帶有3種類加載器
1)根類加載器,使用c++編寫(BootStrap),負(fù)責(zé)加載rt.jar
2)擴(kuò)展類加載器,java實(shí)現(xiàn)(ExtClassLoader)
3)應(yīng)用加載器,java實(shí)現(xiàn)(AppClassLoader) classpath
根類加載器,是用c++實(shí)現(xiàn)的数尿,我們沒有辦法在java層面看到仑性;我們接下來看看ExtClassLoader的代碼,它是在Launcher類中,
static class ExtClassLoader extends URLClassLoader
同時(shí)我們看看AppClassLoader砌创,它也是在Launcher中虏缸,
static class AppClassLoader extends URLClassLoader
他們同時(shí)繼承一個(gè)類URLClassLoader。
關(guān)于這種層次關(guān)系嫩实,看起來像繼承刽辙,其實(shí)不是的。我們看到上面的代碼就知道ExtClassLoader和AppClassLoader同時(shí)繼承同一個(gè)類甲献。同時(shí)我們來看下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)加載那就直接拿出,否則再進(jìn)行加載球及。其中有一個(gè)parent屬性氧骤,就是表示父加載器。這點(diǎn)正好說明了加載器之間的關(guān)系并不是繼承關(guān)系吃引。
6.3 雙親委派機(jī)制
關(guān)于類加載器筹陵,我們不得不說一下雙親委派機(jī)制刽锤。聽著很高大上,其實(shí)很簡單朦佩。比如A類的加載器是AppClassLoader(其實(shí)我們自己寫的類的加載器都是AppClassLoader)并思,AppClassLoader不會自己去加載類,而會委ExtClassLoader進(jìn)行加載语稠,那么到了ExtClassLoader類加載器的時(shí)候宋彼,它也不會自己去加載,而是委托BootStrap類加載器進(jìn)行加載仙畦,就這樣一層一層往上委托输涕,如果Bootstrap類加載器無法進(jìn)行加載的話,再一層層往下走议泵。
上面的源碼也說明了這點(diǎn)占贫。
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
6.4 為何要雙親委派機(jī)制
對于我們技術(shù)來講,我們不但要知其然先口,還要知其所以然型奥。為何要采用雙親委派機(jī)制呢?了解為何之前碉京,我們先來說明一個(gè)知識點(diǎn):
判斷兩個(gè)類相同的前提是這兩個(gè)類都是同一個(gè)加載器進(jìn)行加載的厢汹,如果使用不同的類加載器進(jìn)行加載同一個(gè)類,也會有不同的結(jié)果谐宙。
如果沒有雙親委派機(jī)制烫葬,會出現(xiàn)什么樣的結(jié)果呢?比如我們在rt.jar中隨便找一個(gè)類凡蜻,如java.util.HashMap,那么我們同樣也可以寫一個(gè)一樣的類搭综,也叫java.util.HashMap存放在我們自己的路徑下(ClassPath).那樣這兩個(gè)相同的類采用的是不同的類加載器,系統(tǒng)中就會出現(xiàn)兩個(gè)不同的HashMap類划栓,這樣引用程序就會出現(xiàn)一片混亂兑巾。
我們看一個(gè)例子:
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);
}
}
大家可以看看輸出的是什么?我們自己定義了一個(gè)類加載器忠荞,讓它去加載我們自己寫的一個(gè)類蒋歌,然后判斷由我們寫的類加載器加載的類是否是MyClassLoader的一個(gè)實(shí)例。
答案是否定的委煤。為什么堂油?因?yàn)閖vm.classloader.MyClassLoader是在classpath下面,是由AppClassLoader加載器加載的碧绞,而我們卻指定了自己的加載器府框,當(dāng)然加載出來的類就不相同了。不信讥邻,我們將他的父類加載器都打印出來迫靖。在上面代碼中加入下面代碼:
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
第一個(gè)是我們自己加載器加載的類癣诱,第二個(gè)是直接new的一個(gè)對象,是由App類加載器進(jìn)行加載的袜香,我們把它們的父類加載器打印出來了,可以看出他們的加載器是不一樣的鲫惶。很奇怪為何會執(zhí)行classloader==null這句話蜈首。其實(shí)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 類的連接
講完了類的加載之后赏淌,我們需要了解一下類的連接踩寇。類的連接有三步,分別是驗(yàn)證碳竟,準(zhǔn)備喂柒,解析搬俊。下面讓我們一一了解
7.1 首先我們看看驗(yàn)證階段。
驗(yàn)證階段主要做了以下工作
-將已經(jīng)讀入到內(nèi)存類的二進(jìn)制數(shù)據(jù)合并到虛擬機(jī)運(yùn)行時(shí)環(huán)境中去睛榄。
-類文件結(jié)構(gòu)檢查:格式符合jvm規(guī)范-語義檢查:符合java語言規(guī)范,final類沒有子類,final類型方法沒有被覆蓋
-字節(jié)碼驗(yàn)證:確保字節(jié)碼可以安全的被java虛擬機(jī)執(zhí)行.
二進(jìn)制兼容性檢查:確保互相引用的類的一致性.如A類的a方法會調(diào)用B類的b方法.那么java虛擬機(jī)在驗(yàn)證A類的時(shí)候會檢查B類的b方法是否存在并檢查版本兼容性.因?yàn)橛锌赡蹵類是由jdk1.7編譯的想帅,而B類是由1.8編譯的场靴。那根據(jù)向下兼容的性質(zhì),A類引用B類可能會出錯(cuò)港准,注意是可能旨剥。
7.2 準(zhǔn)備階段
java虛擬機(jī)為類的靜態(tài)變量分配內(nèi)存并賦予默認(rèn)的初始值.如int分配4個(gè)字節(jié)并賦值為0,long分配8字節(jié)并賦值為0;
7.3 解析階段
解析階段主要是將符號引用轉(zhuǎn)化為直接引用的過程。比如 A類中的a方法引用了B類中的b方法浅缸,那么它會找到B類的b方法的內(nèi)存地址轨帜,將符號引用替換為直接引用(內(nèi)存地址)。
到這里為止疗杉,我們知道了類的加載阵谚,類加載器,雙親委派機(jī)制烟具,類的連接等等操作梢什。那么接下來需要講的是類的初始化,初始化內(nèi)容較多朝聋,另開一篇文章深入理解Java類加載機(jī)制(二)講嗡午,這樣大家就不會疲勞和畏懼了。