Java的核心是 JVM 版述,了解并熟悉JVM對(duì)于我們理解Java語言非常重要。
一寞冯、類加載機(jī)制
當(dāng)程序主動(dòng)使用某個(gè)類時(shí)渴析,如果該類還未被加載到內(nèi)存中,則系統(tǒng)會(huì)通過加載吮龄、連接俭茧、初始化三個(gè)步驟來對(duì)該類進(jìn)行初始化。
JVM把描述類的數(shù)據(jù)從class文件加載到內(nèi)存漓帚,并對(duì)數(shù)據(jù)進(jìn)行校驗(yàn)母债,轉(zhuǎn)換解析和初始化,最終形成可以被虛擬機(jī)直接使用的Java類型尝抖,這就是JVM的類加載機(jī)制毡们。
1、JVM和類
當(dāng)調(diào)用Java命令運(yùn)行某個(gè)Java程序時(shí)昧辽,該命令將會(huì)啟動(dòng)一個(gè)Java虛擬機(jī)進(jìn)程衙熔,不管該Java程序有多么復(fù)雜,該程序啟動(dòng)了多少個(gè)線程搅荞,它們都處于該Java虛擬機(jī)進(jìn)程里红氯。
同一個(gè)JVM的所有線程、所有變量都處于同一個(gè)進(jìn)程里咕痛,它們都使用該JVM進(jìn)程的內(nèi)存區(qū)痢甘。
當(dāng)系統(tǒng)出現(xiàn)以下幾種情況時(shí),JVM進(jìn)程將被終止:
- 程序運(yùn)行到最后正常結(jié)束暇检。
- 程序運(yùn)行到使用
System.exit()
或Runtime.getRuntime().exit()
處程序結(jié)束产阱。 - 程序執(zhí)行過程中遇到未捕獲的異常或錯(cuò)誤而結(jié)束块仆。
- 程序所在平臺(tái)強(qiáng)制結(jié)束了JVM進(jìn)程构蹬。
從上面介紹可以知道,當(dāng)Java程序運(yùn)行結(jié)束時(shí)悔据,JVM進(jìn)程結(jié)束庄敛,該進(jìn)程在內(nèi)存中的狀態(tài)將會(huì)丟失。
2科汗、類的加載
類加載:將類的class文件讀入內(nèi)存藻烤,并為之創(chuàng)建一個(gè)java.lang.Class 對(duì)象,也就是說,當(dāng)程序中使用任何類時(shí)怖亭,系統(tǒng)都會(huì)為之建立一個(gè)java.lang.Class對(duì)象涎显。
類的加載由類加載器完成,類加載器通常由JVM提供兴猩,這些類加載器也是前面所有程序運(yùn)行的基礎(chǔ)期吓,JVM提供的這些類加載器通常被稱為系統(tǒng)類加載器。除此之外倾芝,開發(fā)者可以通過繼承 ClassLoader 基類來創(chuàng)建自己的類加載器讨勤。
通過使用不同的類加載器,可以從不同來源加載類的二進(jìn)制數(shù)據(jù)晨另,通常有如下幾種來源:
- 從本地文件系統(tǒng)加載class文件潭千。
- 從 JAR 包加載class文件。
- 通過網(wǎng)絡(luò)加載class文件借尿。
- 把一個(gè) java 源文件動(dòng)態(tài)編譯刨晴,并進(jìn)行加載。
在加載階段虛擬機(jī)需要完成以下三件事:
- 通過一個(gè)類的全限定名稱來獲取此類的二進(jìn)制字節(jié)流垛玻,并加載到內(nèi)存中(需要使用類加載器)
- 將這個(gè)字節(jié)流所代表的靜態(tài)存儲(chǔ)結(jié)構(gòu)轉(zhuǎn)化為方法區(qū)的運(yùn)行時(shí)數(shù)據(jù)結(jié)構(gòu)
- 在堆中生成一個(gè)代表這個(gè)類的java.lang.Class對(duì)象割捅,作為方法區(qū)這個(gè)類的各種數(shù)據(jù)的訪問入口
3、類的鏈接
將Java類的二進(jìn)制數(shù)據(jù)合并到 JVM 的運(yùn)行狀態(tài)之中帚桩。
類的連接分為三個(gè)階段:
驗(yàn)證:驗(yàn)證被加載后的類是否有正確的結(jié)構(gòu)亿驾,類數(shù)據(jù)是否符合虛擬機(jī)的要求,確保不會(huì)危害虛擬機(jī)安全账嚎。
包含四個(gè)階段的校驗(yàn)動(dòng)作:a.文件格式驗(yàn)證莫瞬;b.原數(shù)據(jù)信息進(jìn)行語義校驗(yàn);c.字節(jié)碼驗(yàn)證郭蕉;d.符號(hào)引用驗(yàn)證疼邀。準(zhǔn)備:為類的靜態(tài)變量(static filed)在方法區(qū)分配內(nèi)存,并設(shè)置默認(rèn)初始值(0值或null值)召锈,這些內(nèi)存都將在方法區(qū)分配旁振。對(duì)于一般的成員變量是在類實(shí)例化時(shí)候,隨對(duì)象一起分配在堆內(nèi)存中涨岁。
另外拐袜,靜態(tài)常量(static final filed)會(huì)在準(zhǔn)備階段賦程序設(shè)定的初值,對(duì)于靜態(tài)變量梢薪,這個(gè)操作是在初始化階段進(jìn)行的蹬铺。解析:將類的二進(jìn)制數(shù)據(jù)內(nèi)的符號(hào)引用替換為直接引用。
4秉撇、類的初始化
在該階段甜攀,虛擬機(jī)負(fù)責(zé)對(duì)類進(jìn)行初始化秋泄,主要是對(duì)類變量進(jìn)行初始化。
在Java類中规阀,對(duì)類變量指定初始值有兩種方式:
(1)在聲明類變量時(shí)指定初始值恒序。
(2)在使用靜態(tài)初始化塊時(shí),為類變量指定初始值谁撼。
JVM會(huì)按照這些語句在程序中的排列順序依次執(zhí)行他們奸焙。
JVM初始化一個(gè)類的步驟:
- 假如這個(gè)類還沒有被加載和連接,則程序先加載并連接該類彤敛。
- 假如該類的直接父類還沒有被初始化,則先初始化其直接父類了赌。若該直接父類又有直接父類墨榄,依次類推。所以JVM最先初始化的總是 java.lang.Object 類勿她。
當(dāng)程序主動(dòng)使用任何一個(gè)類時(shí)袄秩,系統(tǒng)會(huì)保證該類以及所有父類(包括直接父類和間接父類)都會(huì)被初始化。 - 假如類中有初始化語句逢并,則系統(tǒng)依次執(zhí)行這些初始化語句之剧。
5、類初始化的時(shí)機(jī)
當(dāng)Java程序首次通過下面的 6種 方式來使用某個(gè)類或接口時(shí)砍聊,系統(tǒng)就會(huì)初始化該類或接口背稼,也稱為主動(dòng)初始化。
觸發(fā)類加載的條件:
- 創(chuàng)建類的實(shí)例玻蝌。
- 使用 new 來創(chuàng)建實(shí)例蟹肘。
- 通過反射創(chuàng)建實(shí)例。
- 通過反序列化來創(chuàng)建實(shí)例俯树。
- 調(diào)用類的類變量(靜態(tài)屬性)帘腹,或?yàn)樵擃愖兞抠x值。
- 調(diào)用類的靜態(tài)方法许饿。
- 通過class文件反射創(chuàng)建對(duì)象阳欲。
例如:Class.forName("Person");
- 初始化一個(gè)子類的時(shí)候,該子類的所有父類都會(huì)被初始化陋率。
- java虛擬機(jī)啟動(dòng)時(shí)被標(biāo)記為啟動(dòng)類的類球化,就是 main 方法所在的類。
同時(shí)還需要注意幾點(diǎn):
- 在同一個(gè)類加載器下面只能初始化類一次翘贮,如果已近初始化了就不要初始化了赊窥。
因?yàn)槔奂虞d的最終結(jié)果就是在堆中存有唯一的一個(gè)Class對(duì)象,這樣通過Class對(duì)象就能找到類的相關(guān)信息狸页。 - 在編譯時(shí)能夠確定下來的 final修飾的靜態(tài)變量(編譯常量)不會(huì)對(duì)類進(jìn)行初始化锨能。
- 在編譯時(shí)無法確定下來的 final修飾的靜態(tài)變量(運(yùn)行時(shí)常量)會(huì)對(duì)類進(jìn)行初始化扯再。
- 如果這個(gè)類沒有被加載和連接,那就需要進(jìn)行加載和連接址遇。
- 如果這個(gè)類有父類并且這個(gè)父類沒有被初始化熄阻,則先初始化父類。
- 如果類中存在初始化語句倔约,依次執(zhí)行初始化語句秃殉。
一個(gè)有關(guān)的小例子:
public class Single {
private static Single single = new Single();
public static int counter1;
public static int counter2 = 0;
private Single () {
counter1++;
counter2++;
}
public static Single getSingle() {
return single;
}
}
public class SingleTest {
public static void main(String[] args) {
Single single = Single.getSingle();
System.out.println("counter1=" + single.counter1);
System.out.println("counter2=" + single.counter2);
}
}
輸出是:
counter1=1
counter2=0
例子分析:
- 在執(zhí)行SIngleTest第一句的時(shí)候,還沒有對(duì)Single類進(jìn)行加載和連接浸剩,所以首先需要對(duì)它進(jìn)行加載和連接钾军。
在連接——準(zhǔn)備階段,要給靜態(tài)變量賦默認(rèn)的初始值绢要。
singel=null
counter1=0
counter2=0
- 加載和連接完畢之后吏恭,再進(jìn)行初始化工作。這時(shí)會(huì)依次執(zhí)行重罪。
首先第一個(gè)靜態(tài)屬性single = new Single();
會(huì)執(zhí)行構(gòu)造方法內(nèi)部的邏輯操作樱哼,此時(shí)
counter1=1
counter2=1
接下來第二個(gè)靜態(tài)屬性counter1,程序并沒有對(duì)它進(jìn)行初始化賦值剿配,所以它沒辦法進(jìn)行初始化搅幅。
第三個(gè)屬性counter2我們初始化復(fù)制為0,因此可以初始化為 counter2=1呼胚。
- 初始化完畢之后茄唐,就調(diào)用了靜態(tài)方法
Single.getSingle();
放回的single
已經(jīng)初始化了。
輸出的內(nèi)容也理所當(dāng)然就是counter1=1蝇更,counter2=0
二琢融、類加載器
類加載器負(fù)責(zé)將 .class 文件加載到內(nèi)存中,并為之生成對(duì)應(yīng)的 Class 對(duì)象簿寂。
在JVM中漾抬,一個(gè)類用其全限定類名和其類加載器作為唯一的標(biāo)識(shí)。這樣保證同一個(gè)類不會(huì)再次被載入常遂。
1纳令、類加載器的層級(jí)結(jié)構(gòu)
引導(dǎo)類加載器(Bootstrap ClassLoader)
它用來加載Java的核心庫(JAVA_HOME/jre/lib/rt.jar或sun.boot.class.Path路徑下的內(nèi)容),是用C++代碼來實(shí)現(xiàn)的克胳,并不繼承自java.lang.Classloader平绩。
加載擴(kuò)展類和應(yīng)用程序類加載器,并指定他們的父類加載器漠另。
啟動(dòng)類加載器無法被Java程序直接引用
擴(kuò)展類加載器(Extension ClassLoader)
- 用來加載Java的擴(kuò)展庫(JAVA_HOME/jre/ext/*.jar或java.ext.dirs路徑下的內(nèi)容)捏雌。 Java虛擬機(jī)的實(shí)現(xiàn)會(huì)提供一個(gè)擴(kuò)展庫目錄。該類加載器在此目錄里面查找并加載Java類笆搓。
- 由sun.misc.Launcher$ExtClassLoader實(shí)現(xiàn)性湿。
應(yīng)用程序類加載器(Application ClassLoader)
- 它根據(jù)Java應(yīng)用的類路徑(classpath纬傲,java.class.path類。 一般來說肤频,Java應(yīng)用的類都是由它來完成加載的叹括。
- 由sun.misc.Launcher$AppClassLoader實(shí)現(xiàn)。
自定義類加載器
開發(fā)人員可以用過繼承java.lang.ClassLoader類的方式實(shí)現(xiàn)自己的類加載器宵荒,以滿足一些特殊的要求
2汁雷、類加載機(jī)制——雙親委派模式
幾個(gè)類加載器實(shí)現(xiàn)類加載過程時(shí)相互配合協(xié)作的流程。
從JDK1.2開始报咳,java虛擬機(jī)規(guī)范就推薦開發(fā)者使用雙親委派模式(ParentsDelegation Model)進(jìn)行類加載侠讯,其加載過程如下:
- 如果一個(gè)類加載器收到了類加載請(qǐng)求,它首先不會(huì)自己去嘗試加載這個(gè)類暑刃,而是把類加載請(qǐng)求委派給父類加載器去完成继低。
- 每一層的類加載器都把類加載請(qǐng)求委派給父類加載器,依次向上稍走,直到所有的類加載請(qǐng)求都傳遞給頂層的啟動(dòng)類加載器。
- 如果頂層的啟動(dòng)類加載器無法完成加載請(qǐng)求柴底,子類加載器才會(huì)嘗試自己去加載該類婿脸,如果連最初發(fā)起類加載請(qǐng)求的類加載器也無法完成加載請(qǐng)求時(shí),將會(huì)拋出ClassNotFoundException柄驻,而不再調(diào)用其子類加載器去進(jìn)行類加載狐树。
雙親委派模式的類加載機(jī)制的優(yōu)點(diǎn):
不同層次的類加載器具有不同優(yōu)先級(jí),比如所有Java對(duì)象的超級(jí)父類java.lang.Object鸿脓,位于rt.jar抑钟,無論哪個(gè)類加載器加載該類,最終都是由啟動(dòng)類加載器進(jìn)行加載野哭,保證安全在塔。即使用戶自己編寫一個(gè)java.lang.Object類并放入程序中,雖能正常編譯拨黔,但不會(huì)被加載運(yùn)行蛔溃,保證不會(huì)出現(xiàn)混亂。
注意:
- 并不是所有的類加載器都采用雙親委托機(jī)制篱蝇。
- tomcat服務(wù)器類加載器也是用代理模式贺待,所不同的是它首先嘗試去加載某個(gè)類,如果找不到再找代理給父類加載器零截。這與一般類加載器的順序是相反的麸塞。
雙親委派模型的代碼實(shí)現(xiàn)
ClassLoader中l(wèi)oadClass方法實(shí)現(xiàn)了雙親委派模型
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
//檢查該類是否已經(jīng)加載過
Class c = findLoadedClass(name);
if (c == null) {
//如果該類沒有加載,則進(jìn)入該分支
long t0 = System.nanoTime();
try {
if (parent != null) {
//當(dāng)父類的加載器不為空涧衙,則通過父類的loadClass來加載該類
c = parent.loadClass(name, false);
} else {
//當(dāng)父類的加載器為空哪工,則調(diào)用啟動(dòng)類加載器來加載該類
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//非空父類的類加載器無法找到相應(yīng)的類奥此,則拋出異常
}
if (c == null) {
//當(dāng)父類加載器無法加載時(shí),則調(diào)用findClass方法來加載該類
long t1 = System.nanoTime();
c = findClass(name); //用戶可通過覆寫該方法正勒,來自定義類加載器
//用于統(tǒng)計(jì)類加載器相關(guān)的信息
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
//對(duì)類進(jìn)行l(wèi)ink操作
resolveClass(c);
}
return c;
}
}
整個(gè)流程大致如下:
a.首先得院,檢查一下指定名稱的類是否已經(jīng)加載過,如果加載過了章贞,就不需要再加載祥绞,直接返回。
b.如果此類沒有加載過鸭限,那么蜕径,再判斷一下是否有父加載器;如果有父加載器败京,則由父加載器加載(即調(diào)用parent.loadClass(name, false);).或者是調(diào)用bootstrap類加載器來加載兜喻。
c.如果父加載器及bootstrap類加載器都沒有找到指定的類,那么調(diào)用當(dāng)前類加載器的findClass方法來完成類加載赡麦。
3朴皆、自定義類加載器
通過擴(kuò)展 ClassLoader 的子類,重寫 ClassLoader 所包含的方法來實(shí)現(xiàn)自定義的類加載器泛粹。
ClassLoader 類有如下兩個(gè)關(guān)鍵方法:
-
loadClass(String name, boolean resolve)
:該方法為ClassLoader的入口點(diǎn)遂铡,根據(jù)指定名稱來加載類,系統(tǒng)就是調(diào)用 ClassLoader 的該方法來獲取指定類對(duì)應(yīng)的 Class 對(duì)象晶姊。 -
findClass(String name)
:根據(jù)指定名稱來查找類扒接。
通常推薦重寫 findClass() 方法。
在 ClassLoader 類中還有一個(gè)核心方法:
-
Class defineClass(String name, byte[] b, int off, int len)
:該方法負(fù)責(zé)將指定類的字節(jié)碼文件(即Class文件们衙,如:Hello.class)讀入字節(jié)數(shù)組byte[] b
內(nèi)钾怔,并把它轉(zhuǎn)換為 Class 對(duì)象。
無須重寫該方法蒙挑,因?yàn)樵摲椒ㄊ?final 的宗侦。
除此之外,ClassLoader 類還有一些普通方法:
-
findSystemClass(String name)
:從本地系統(tǒng)裝入文件忆蚀。 -
static getSystemClassLoader()
:用于返回系統(tǒng)類加載器凝垛。 -
getParent()
:獲取該類加載器的父類加載器。 -
resolveClass(Class<?> c)
:鏈接指定的類蜓谋。 -
findLoadClass(String name)
:如果Java虛擬機(jī)已經(jīng)加載了名為 name 的類梦皮,則直接返回該類對(duì)應(yīng)的 Class 實(shí)例,否則返回 null 桃焕。該方法是 Java 類加載緩存機(jī)制的體現(xiàn)剑肯。
整個(gè)的函數(shù)調(diào)用流程:
我們可以簡單地自定義一個(gè)類加載器,用于加載某個(gè)class
public class FileSystemClassLoader extends ClassLoader {
//文件的根目錄
private String rootDir;
public FileSystemClassLoader(String rootDir){
this.rootDir=rootDir;
}
//重寫findClass方法
@Override
protected Class<?> findClass(String s) throws ClassNotFoundException {
Class c=findLoadedClass(s);
if (c!=null){
return c;
}else {
ClassLoader parent=this.getParent();
//parent獲取不到class時(shí)會(huì)拋出異常观堂,為了繼續(xù)執(zhí)行使用try catch包裹
try{
c=parent.loadClass(s);
}catch (Exception e){
}
if (c!=null){
return c;
}else {
byte[] classData=getClassData(s);
if (classData==null){
throw new ClassNotFoundException();
}else {
//將字節(jié)數(shù)組轉(zhuǎn)為Class
c=defineClass(s,classData,0,classData.length);
}
}
}
return c;
}
//將文件轉(zhuǎn)為字節(jié)數(shù)組
private byte[] getClassData(String className) {
//改為文件地址
String path=rootDir+"/"+className.replace(".","/")+".class";
System.out.println(path);
ByteArrayOutputStream byteArrayOutputStream=new ByteArrayOutputStream();
InputStream inputStream=null;
try {
inputStream=new FileInputStream(path);
byte[] buffer=new byte[1024];
int temp=0;
while ((temp=inputStream.read(buffer))!=-1){
byteArrayOutputStream.write(buffer,0,temp);
}
return byteArrayOutputStream.toByteArray();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
return null;
} finally {
if (inputStream!=null){
try {
inputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
if (byteArrayOutputStream!=null){
try {
byteArrayOutputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
return null;
}
}
public class UseCustomClassLoader {
public static void main(String[]args){
FileSystemClassLoader loader=new FileSystemClassLoader("/home/xjk");
FileSystemClassLoader loader2=new FileSystemClassLoader("/home/xjk");
try {
Class clazz1=loader.findClass("com.jk.bean.Emp");//本項(xiàng)目自定義的類調(diào)用AppClassLoader
System.out.println(clazz1.getClassLoader());
Class clazz2=loader.findClass("java.lang.String");//rt.jar里的類調(diào)用BootstrapClassLoader
System.out.println(clazz2.getClassLoader());
Class clazz3=loader.findClass("com.company.Main");//項(xiàng)目外的類調(diào)用自定義的FileSystemClassLoader
System.out.println(clazz3.getClassLoader());
Class clazz4=loader2.findClass("com.company.Main");//使用不同類加載器让网,Class對(duì)象不一致
System.out.println(clazz4.getClassLoader());
System.out.println(clazz3==clazz4);
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
輸出結(jié)果
sun.misc.Launcher$AppClassLoader@18b4aac2
null
com.jk.jvm.FileSystemClassLoader@1d44bcfa
com.jk.jvm.FileSystemClassLoader@6f94fa3e
false
因?yàn)锽ootstrapClassLoader無法被Java程序直接引用呀忧,所以顯示為空。
使用自定義的類加載器溃睹,可以實(shí)現(xiàn)如下常見的功能:
- 執(zhí)行代碼前自動(dòng)驗(yàn)證數(shù)字簽名而账。
- 根據(jù)用戶提供的密碼解密代碼,從而可以實(shí)現(xiàn)代碼混淆器來避免反編譯 *.class 文件因篇。
- 根據(jù)用戶需求來動(dòng)態(tài)的加載類泞辐。
- 根據(jù)用戶需求把其他數(shù)據(jù)以字節(jié)碼的形式加載到應(yīng)用中。