咱們知道java要運行須要編譯和運行,javac將java源代碼編譯爲class文件。而虛擬機把描述類的數據從class文件中加載到內存,並對數據進行校驗、轉換解析、初始化,最終造成能夠被虛擬機直接使用的java類型,這就是類加載機制,他在運行期間完成。java
JVM加載class文件到內存有兩種方式:web
以前的我只知道在對象建立以前會先初始化靜態的東西,也知道從父類開始初始化,但一直不懂爲何會是這樣的順序,直到我瞭解了虛擬機是如何實現類加載的。在開始真正瞭解類加載以前,咱們先來看三個例子。數組
class SuperClass {
static{
System.out.println("SuperClass Init");
}
public static int value = 123;
}
class SubClass extends SuperClass{
static{
System.out.println("SubClass Init");
}
}
public class NotInitialization{
public static void main(String agrs[]){
System.out.println(SubClass.value);
}
}
複製代碼
輸出:tomcat
SuperClass Init
123
複製代碼
這道例子彷佛很簡單,他告訴咱們對於靜態字段,只有直接定義這個字段的類纔會被初始化,因此,即便這裏是經過子類來引用父類的靜態屬性,他也不會使子類發生初始化,而至於加載和驗證,虛擬機並無明確規範,各步驟的做用下文會談安全
class SuperClass {
static{
System.out.println("SuperClass Init");
}
public static int value = 123;
}
class SubClass extends SuperClass{
static{
System.out.println("SubClass Init");
}
}
public class NotInitialization{
public static void main(String agrs[]){
SuperClass[] sca = new SuperClass[10];
}
}
複製代碼
輸出:bash
//無輸出
複製代碼
是的,運行以後並無輸出,但他觸發了一個叫「[Lorg.fenixsoft.classloading.SuperClass」的類初始化,而建立動做由字節碼指令newarray觸發,從這裏,咱們也就直到建立一個對象數組的真實狀況了服務器
class ConstClass{
static{
System.out.println("ConstClass init");
}
public static final String WORD = "Hello";
}
public class NotInitialization{
public static void main(String agrs[]){
System.out.println(ConstClass.WORD);
}
}
複製代碼
輸出:網絡
Hello
複製代碼
這裏WORD做爲一個常量,他在編譯階段就已經生成,意思是說編譯階段通過常量傳播優化,已經將他存儲到了NotInitialization類的常量池中,之後全部對它的引用都是NotInitialization對常量池的引用,這就是爲何不初始化類。數據結構
下面來總結一下五種必須對類初始化的狀況:多線程
以上,都是類第一次發生初始化的狀況,而對於接口的初始化,他和類的不一樣就是隻有在真正使用到父接口的時候纔會初始化父接口。
下面來具體看一下類加載的全過程分別要作哪些事情
這個時期須要完成三件事:
這裏,非數組類的加載階段和數組類有些不一樣:
說直白加載的做用就是找到.class文件並把這個文件包含的字節碼讀取到內存中
這一步的目的是爲了確保Class文件的字節流中包含的信息符合當前虛擬機的要求,且不會危害虛擬機自身的安全,大概分爲四部驗證
爲類變量分配內存並設置類變量初始化值,在方法區進行分配,如int爲0,boolean爲false,reference爲null
將常量池內的符號引用替換爲直接引用的過程
問,什麼是符號引用,什麼是直接引用?
符號引用就是一個字符串,這個字符串有足夠的信息能夠找到相應的位置。直接引用就是偏移量,經過偏移量能夠直接在內存區域找到方法字節碼的起始位置。
解析主要包括對類、接口、字段、類方法、接口方法、方法類型、方法句柄、調用點限定符這些符號引用進行
在類中包含的靜態初始化器都被執行,在這一階段末尾靜態字段被初始化爲默認值,初始化遵照下面幾條原則(其中是類初始化的字節碼指令)
public class Test {
static {
i = 0;
//System.out.println(i);
}
static int i;
}
複製代碼
上面註釋的那一行會報錯,由於在靜態初始化塊中只能訪問到定義在靜態語句塊以前的變量;定義在他以後的變量,在前面的靜態語句塊能夠賦值,不能訪問,說明了第一條
public class Test {
static class DeadLoopClass{
static{
if (true){
System.out.println(Thread.currentThread() + "init DeadLoopClass");
while(true){
}
}
}
}
public static void main(String agrs[]){
Runnable script = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread() + "start");
DeadLoopClass dlc = new DeadLoopClass();
System.out.println(Thread.currentThread() + "run over");
}
};
Thread t1 = new Thread(script);
Thread t2 = new Thread(script);
t1.start();
t2.start();
}
}
複製代碼
輸出
Thread[Thread-0,5,main]start
Thread[Thread-1,5,main]start
Thread[Thread-0,5,main]init DeadLoopClass
複製代碼
他會打印上面的語句並會發生阻塞,這個例子說明了初始化的時候會保證類會被正確加鎖
接下來咱們具體看一下類加載器有哪些特色,它的做用就是動態加載類到Java虛擬機的內存空間中,就是上文說的「經過一個類的全限定名來獲取描述此類的二進制字節流」,而且這個動做是放到Java虛擬機外部實現的,就是說應用程序本身決定如何去獲取須要的類
在JVM中標識兩個class對象是否爲同一個類對象存在兩個必要條件
一個應用程序老是由n多個類組成,Java程序啓動時,並非一次把全部的類所有加載後再運行,它老是先把保證程序運行的基礎類一次性加載到jvm中,其它類等到jvm用到的時候再加載,這樣的好處是節省了內存的開銷
類加載器能夠大體分爲三類:
若是一個類加載器收到了類加載請求,它並不會本身先去加載,而是把這個請求委託給父類的加載器去執行,若是父類加載器還存在其父類加載器,則進一步向上委託,依次遞歸,請求最終將到達頂層的啓動類加載器,若是父類加載器能夠完成類加載任務,就成功返回,假若父類加載器沒法完成此加載任務,子加載器纔會嘗試本身去加載,這就是雙親委派模式。
注意,這裏叫雙親不是由於繼承關係而是組合關係
很容易想到,雙親委派模型的層級能夠避免重複加載,尤爲是java的核心類庫不會被替換,例如本身定義了一個java.lang.Integer,雙親委派模型不會去初始化他,而是直接返回加載過的Integer.class。固然,若是強行用defineClass()方法(這個方法將byte字節流解析成JVM可以識別的Class對象)去加載java.lang開頭的類也不會成功,會拋出安全異常
ClassLoader的loadClass(),只列出了關鍵的
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{
//首先,檢查請求的類是否已經被加載過了
Class c = findLoadedClass(name);
if (c == null){
try{
if (parent != null){
c = parent.loadClass(name,false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e){
//若是父類加載器拋出ClassNotFoundException,說明父類加載器沒法完成加載請求
}
if (c == null){
//在父類加載器沒法加載的時候
//再調用自己的findClass方法來進行類加載
c = findClass(name);
}
}
if (resolve){
//使用類的Class對象建立完成也同時被解析
resolveClass(c);
}
return c;
}
複製代碼
ClassLoader的findClass(),
//直接拋出異常
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
複製代碼
ClassLoader的defineClass
protected Class<?> findClass(String name) throws ClassNotFoundException {
//獲取類的class文件字節數組
byte[] classData = getClassData(name);
if (classData == null){
throw new ClassNotFoundException();
} else {
//直接生成class對象
return defineClass(name,classData,0,classData.length);
}
}
複製代碼
ClassLoader的resolveClass()
protected final void resolveClass(Class<?> c) {
if (c == null) {
throw new NullPointerException();
}
}
複製代碼
下面再來看一下關鍵方法的具體做用:
先看如下loadClass()方法,經過以上代碼能夠看到邏輯並不複雜:先檢查是否已經被加載過,若沒有加載則調用父加載器的loadClass(),若父加載器爲空讓啓動類加載器爲父加載器,若父類加載失敗,拋出異常,再調用本身的findClass()方法
在JDK1.2以後,若是咱們自定義類加載器的話咱們將再也不重寫loadClass(),由於ClassLoader已經實現loadClass(),而且用它來達到雙親委派的效果。咱們自定義類加載器須要重寫的是findClass(),知道findClass()方法是在loadClass()方法中被調用的,當loadClass()方法中父加載器加載失敗後,則會調用本身的findClass()方法來完成類加載,這樣就能夠保證自定義的類加載器也符合雙親委託模式。
雙親委派模型不是一個強制性的約束模型,雙親委派模型也有不太適用的時候,這時根據具體的狀況咱們就要破壞這種機制,雙親委派模型主要出現過三次被破壞的狀況
由於雙親委派模型是在JDK1.2的時候出現的,因此,在JDK1.2以前,是沒有雙親委派的,爲了向前兼容,JDK1.2以後的java.lang.ClassLoader添加了一個新的protected的findClass()方法,這個方法的惟一邏輯就是調用本身的loadClass(),前文分析代碼實現的時候咱們知道雙親委派模型就是根據loadClass()來實現的,因此爲了使用雙親委派模型,咱們應當把本身的類加載邏輯寫道findClass()中。
咱們有一些功能是java提供接口,而其餘的公司提供實現類,例如咱們的JDBC、JNDI(由多個公司提供本身的實現)因此像JDBC、JNDI這樣的SPI(服務提供者接口),就須要第三方實現,這些SPI的接口屬於核心庫,由Bootstrap類加載器加載,那麼如何去加載那些公司提供的實現類呢?這就是咱們的線程上下文類加載器,下圖是總體大概的工做流程
第三次破壞委派雙親模型就是因爲用戶追求動態性致使的,「動態性」就是指代碼熱替換、模塊熱部署等,就是但願程序不須要重啓就能夠更新class文件,最典型的例子就是SpringBoot的熱部署和OSGi。這裏拿OSGi舉例,OSGi實現模塊化熱部署的關鍵就是它自定義類加載機制的實現,每個程序模塊(OSGi中稱爲Bundle)都有本身的類加載器,當須要更換一個Bundle時,就把Bundle連同類加載器一塊兒換掉實現熱部署
因此,在OSGi環境下,類加載器再也不是層次模型,而是網狀模型,如圖
當OSGi收到一個類加載的時候會按照如下的順序進行搜索:
以上前兩點仍符合雙親委派規則,其他都是平級類加載器查找
前文咱們瞭解了Java中類加載器的運行方式;但主流的Web服務器都會有本身的一套類加載器,爲何呢?由於對於服務器來講他要本身解決一些問題:
顯然,若是Tomcat使用默認的類加載機制是沒法知足上述要求的
CommonClassLoader能加載的類均可以被Catalina ClassLoader和SharedClassLoader使用,而CatalinaClassLoader和Shared ClassLoader本身能加載的類則與對方相互隔離。WebAppClassLoader可使用SharedClassLoader加載到的類,但各個WebAppClassLoader實例之間相互隔離。而JasperLoader的加載範圍僅僅是這個JSP文件所編譯出來的那一個.Class文件,它出現的目的就是爲了被丟棄:當Web容器檢測到JSP文件被修改時,會替換掉目前的JasperLoader的實例,並經過再創建一個新的Jsp類加載器來實現JSP文件的HotSwap功能。
Tomcat 6.x把/common、/server和/shared三個目錄默認合併到一塊兒變成一個/lib目錄,這個目錄裏的類庫至關於之前/common目錄中類庫的做用
如今咱們再來看Tomcat時如何解決以前的四個問題的:
前文咱們說過破壞委託模型,這裏就是一個例子,能夠採用線程上下文加載器,讓父類加載器請求子類加載器完成加載類做用
這個錯誤是說當JVM加載指定文件的字節碼到內存時,找不到相應的字節碼。解決辦法爲在當前classpath目錄下找有沒有指定文件(this.getClass().getClassLoader().getResource("").toString()能夠查看當前classpath)
這種錯誤出現的狀況就是使用了new關鍵字、屬性引用某個類、繼承某個接口或實現某個類或某個方法參數引用了某個類,這時虛擬機隱式加載這些類發現這些類不存在的異常。解決這個錯誤的辦法就是確保每一個類引用的類都在當前的classpath下面
多是在JVM啓動的時候不當心在JVM中的某個lib刪了
沒法轉型,這個可能對於初學者來講會很常見(好比說我,哈哈),解決辦法時轉型前先用instanceof檢查是否是目標類型再轉換
這個異常是因爲類加載過程當中靜態塊初始化過程失敗所致使的。因爲它出如今負責啓動程序的主線程中,所以你最好從主類中開始分析,這裏說的主類是指你在命令行參數中指定的那個,或者說是你聲明瞭public static void main(String args[])方法的那個類。這個異常很大可能會伴隨NoClassDefFoundError,因此出現NoClassDefFoundError時咱們先看ExceptionInInitializerError出現沒。
接下來咱們要本身寫一個類加載器,在開始寫以前,咱們要知道爲何須要咱們本身寫類加載器呢?
下面咱們開始自定義類加載器吧
package SelfClassLoader;
import java.io.*;
public class FileClassLoader extends ClassLoader {
private String rootDir;
public FileClassLoader(String rootDir){
this.rootDir = rootDir;
}
/**
* 編寫findClass方法的邏輯
* @param name
* @return
* @throws ClassNotFoundException
*/
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
//獲取類的class文件字節數組
byte[] classData = getClassData(name);
if (classData == null){
throw new ClassNotFoundException();
} else {
//直接生成class對象
return defineClass(name,classData,0,classData.length);
}
}
/**
* 編寫獲取class文件並轉換爲字節碼流的邏輯
* @param className
* @return
*/
private byte[] getClassData(String className){
//讀取類文件的字節
String path = classNameToPath(className);
try {
InputStream ins = new FileInputStream(path);
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[2048];
int bytesNumRead = 0;
// 讀取類文件的字節碼
while ((bytesNumRead = ins.read(buffer)) != -1) {
baos.write(buffer, 0, bytesNumRead);
}
return baos.toByteArray();
} catch (IOException e) {
e.printStackTrace();
}
return null;
}
/**
* 類文件的完整路徑
* @param className
* @return
*/
private String classNameToPath(String className) {
return rootDir + File.separatorChar
+ className.replace('.', File.separatorChar) + ".class";
}
/**
* 讀取文件
*/
public static void main(String[] args) throws ClassNotFoundException {
String rootDir="C:\\java\\JVM\\JVMInstruction\\src";
//建立自定義文件類加載器
FileClassLoader loader = new FileClassLoader(rootDir);
try {
//加載指定的class文件,加上包名
Class<?> object1=loader.loadClass("SelfClassLoader.DemoObj");
System.out.println(object1.newInstance().toString());
//輸出結果:I am DemoObj
} catch (Exception e) {
e.printStackTrace();
}
}
}
複製代碼
咱們經過getClassData()方法找到class文件並轉換爲字節流,並重寫findClass()方法,利用defineClass()方法建立了類的class對象。在main方法中調用了loadClass()方法加載指定路徑下的class文件,因爲啓動類加載器、拓展類加載器以及系統類加載器都沒法在其路徑下找到該類,所以最終將有自定義類加載器加載,即調用findClass()方法進行加載。
還有一種方式是繼承URLClassLoader類,而後設置自定義路徑的URL來加載URL下的類,這種方式更常見
package SelfClassLoader;
import java.io.File;
import java.net.*;
public class PathClassLoader extends URLClassLoader {
private String packageName = "net.lijunfeng.classloader";
public PathClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
public PathClassLoader(URL[] urls) {
super(urls);
}
public PathClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) {
super(urls, parent, factory);
}
protected Class<?> findClass(String name) throws ClassNotFoundException{
Class<?> aClass = findLoadedClass(name);
if (aClass != null){
return aClass;
}
if (!packageName.startsWith(name)){
return super.loadClass(name);
} else {
return findClass(name);
}
}
public static void main(String[] args) throws ClassNotFoundException, MalformedURLException {
String rootDir="C:\\java\\JVM\\JVMInstruction\\src";
//建立自定義文件類加載器
File file = new File(rootDir);
//File to URI
URI uri=file.toURI();
URL[] urls={uri.toURL()};
PathClassLoader loader = new PathClassLoader(urls);
try {
//加載指定的class文件
Class<?> object1=loader.loadClass("SelfClassLoader.DemoObj");
System.out.println(object1.newInstance().toString());
//輸出結果:I am DemoObj
} catch (Exception e) {
e.printStackTrace();
}
}
}
複製代碼