作一個積極的人編碼、改bug、提高本身java
我有一個樂園,面向編程,春暖花開!編程
推薦閱讀segmentfault
第一季
0、Java的線程安全、單例模式、JVM內存結構等知識梳理
一、Java內存管理-程序運行過程(一)
二、Java內存管理-初始JVM和JVM啓動流程(二)
三、Java內存管理-JVM內存模型以及JDK7和JDK8內存模型對比總結(三)
四、Java內存管理-掌握虛擬機類加載機制(四)
五、Java內存管理-掌握虛擬機類加載器(五)
六、Java內存管理-類加載器的核心源碼和設計模式(六)
七、Java內存管理-掌握自定義類加載器的實現(七)
第一季總結:由淺入深JAVA內存管理 Core Storywindows第二季
八、Java內存管理-愚人節new一個對象送給你(八)
【福利】JVM系列學習資源無套路贈送
九、Java內存管理-」一文掌握虛擬機建立對象的祕密」(九)
十、Java內存管理-你真的理解Java中的數據類型嗎(十)
十一、Java內存管理-Stackoverflow問答-Java是傳值仍是傳引用?(十一)
十二、Java內存管理-探索Java中字符串String(十二)設計模式實戰
分享一位老師的人工智能教程。零基礎!通俗易懂!風趣幽默!
你們能夠看看是否對本身有幫助,點擊這裏查看【人工智能教程】。接下來進入正文。安全
<font color='blue'>勿在流沙築高臺,出來混早晚要還的。</font>服務器
上一篇分析了ClassLoader的類加載相關的核心源碼,也簡單介紹了ClassLoader的設計思想,讀源碼相對來講是比較枯燥的,仍是這個是必需要走的過程,學習源碼中的一些思想,一些精髓,看一下大神級人物是怎麼寫出那麼牛逼的代碼。咱們可以從中學到一點點東西,那也是一種進步和成長了。本文基於上一篇文章內容,手把手寫一個自定義類加載器,而且經過一些簡單的案例(場景)讓咱們更加細緻和靜距離的體驗類加載器的神奇之處。網絡
<font color='red'>本文地圖:</font>app
上一篇介紹了ClassLoader中loadClass()
內的一些源碼,也介紹了一些核心的API,其中有一個getParent()
是沒有作說明的,這裏簡單說明一下,方便快速理解後續的內容。
// 返回委託的父類加載器。 ClassLoader getParent()
這個方法是獲取父類加載器,那麼父類加載器是怎麼初始化的。上一文也提到了,雖然類加載器的加載模式爲雙親委派模型,可是真正在實現上並非使用繼承方式。
看下面源碼,sun.misc.Launcher
類是java的入口,在啓動java應用的時候會首先建立Launcher類,建立Launcher類的時候回準備應用程序運行中須要的類加載器。
/** * 刪除了一些其餘代碼,方便閱讀 **/ public class Launcher { // 可自行打印一下 bootClassPath ,看輸入內容是什麼? private static String bootClassPath = System.getProperty("sun.boot.class.path");// ① private static Launcher launcher = new Launcher(); // ② private ClassLoader loader; public Launcher() { Launcher.ExtClassLoader var1; try { var1 = Launcher.ExtClassLoader.getExtClassLoader(); } catch (IOException var10) { throw new InternalError("Could not create extension class loader", var10); } try { this.loader = Launcher.AppClassLoader.getAppClassLoader(var1); } catch (IOException var9) { throw new InternalError("Could not create application class loader", var9); } Thread.currentThread().setContextClassLoader(this.loader);// ③ // 省略其餘代碼.... } }
第一:Launcher做爲JAVA應用的入口,根據咱們以前所學的雙親委派模型,Laucher是由JVM建立的,它類加載器應該是BootStrapClassLoader
, 這是一個C++編寫的類加載器,是Java應用體系中最頂層的類加載器,負責加載JVM須要的一些類庫,classpath配置 (%JAVA_HOME%/jre/lib
)。下面經過簡單例子進行說明:
public class TestClassLoader { public static void main(String[] args) { // 能夠獲取是哪一個類加載器加載 Launcher 這個類 ClassLoader classLoader = Launcher.class.getClassLoader(); System.out.println("classLoader : " + classLoader); System.out.println("-------------------"); String bootClassPath = System.getProperty("sun.boot.class.path"); String[] split = bootClassPath.split(";"); for (int i = 0; i < split.length; i++) { System.out.println(split[i]); } }
輸出的結果,我本地機器上路徑:
classLoader : null ------------------- C:\Program Files\Java\jdk1.8.0_121\jre\lib\resources.jar C:\Program Files\Java\jdk1.8.0_121\jre\lib\rt.jar C:\Program Files\Java\jdk1.8.0_121\jre\lib\sunrsasign.jar C:\Program Files\Java\jdk1.8.0_121\jre\lib\jsse.jar C:\Program Files\Java\jdk1.8.0_121\jre\lib\jce.jar C:\Program Files\Java\jdk1.8.0_121\jre\lib\charsets.jar C:\Program Files\Java\jdk1.8.0_121\jre\lib\jfr.jar C:\Program Files\Java\jdk1.8.0_121\jre\classes
第二:當初始化Launcher
類的時候,遇到關鍵字new 進行初始化,調用構造方法。先獲取到ExtClassLoader
類加載器,而後在獲取AppClassLoader
類加載器器,而後設置ExtClassLoader
作爲它父類加載器。具體設置代碼以下:
public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException { //刪掉其餘代碼... return new Launcher.AppClassLoader(var1x, var0); } AppClassLoader(URL[] var1, ClassLoader var2) { // 看 super的實現 ,var2 就是 ExtClassLoader super(var1, var2, Launcher.factory); this.ucp.initLookupCache(this); } /** * 在AppClassLoader的父類中,java.net.URLClassLoader#URLClassLoader * super(parent); 設置父類加載器,最後能夠經過 getParent() 獲取父類加載器 */ public URLClassLoader(URL[] urls, ClassLoader parent, URLStreamHandlerFactory factory) { super(parent); // 這裏就不在繼續往上跟蹤了,在往上代碼相對簡單,可自行查閱 SecurityManager security = System.getSecurityManager(); //刪掉其餘代碼... }
第三:設置上下文類加載器的思考?
在Java中爲何須要上下文類加載器呢,這個就是一個很是有意思的問題。 Java中有兩種方式獲取類加載器:
第一種:一個類被加載的時候使用哪一個類加載器來加載,也就是類.class.getClassLoader()
或者對象.getClass().getClassLoader()
。
String str = new String(); ClassLoader classLoader1 = String.class.getClassLoader(); ClassLoader classLoader2 = str.getClass().getClassLoader(); System.out.println("String loader1 : " + classLoader1); System.out.println("String loader2 : " + classLoader2); -----輸出爲null,啓動類加載器進行加載-------- String loader1 : null String loader2 : null
第二種:經過Thread的上限文獲取類加載器。爲何要經過上下文加載呢?
雖然咱們都知道Java類加載的雙親委派模型,在加載一個類的時候,會優先委派給父類加載器,這樣保證不會出現類被重複加載,也保證了Java一些基礎類(如String類)能夠穩定的存在,不會被用戶自定義類頂替掉。
可是雙親委派模型並非完美的,在一些場景下會出現一些比較難解決的問題,舉個例子,在使用SPI的時候,java.util.ServiceLoader
是經過BootStrapClassLoader
類加載器加載的,在執行到加載用戶編寫的擴展類的時候,若是使用當前類的類加載器,是確定沒法加載到用戶編寫的類的,這個時候就沒法繼續執行了,因此這個時候就須要使用Thread的上下文類加載器,查看源碼的時候咱們就發現,在用戶不主動傳遞ClassLoader
的時候,會獲取當前上下文類加載器,這樣應用程序才能正常的執行。
public static <S> ServiceLoader<S> load(Class<S> var0) { ClassLoader var1 = Thread.currentThread().getContextClassLoader(); return load(var0, var1); }
<font color='blue'>小總結:</font>
上面的內容是在上一篇的基礎上繼續的擴展,若是對尚未看過上一篇內容,請先閱讀上一篇內容後,在來看這段內容。整個ClassLoader源碼的的部分就分析這麼多了,後面在自定義類加載器中有遇到須要分析源碼的地方,仍是會繼續進行說明和講解。
前面的兩篇文章一直在爲自定義加載器作鋪墊,本文終於來引來這個神祕的嘉賓了,下面就咱們用"熱戀"的掌聲歡迎它的出場(活躍一下氣氛,由於剛纔看源碼太安靜了)!
首先看一下JDK API中如何教咱們實現從現從網絡加載類的類加載器的簡單示例。看下面代碼:
例如,應用程序能夠建立一個網絡類加載器,從服務器中下載類文件。示例代碼以下所示: ClassLoader loader = new NetworkClassLoader(host, port); Object main = loader.loadClass("Main", true).newInstance(); . . . 網絡類加載器子類必須定義方法 findClass 和 loadClassData,以實現從網絡加載類。下載組成該類的字節後,它應該使用方法 defineClass 來建立類實例。示例實現以下: class NetworkClassLoader extends ClassLoader { String host; int port; public Class findClass(String name) { byte[] b = loadClassData(name); return defineClass(name, b, 0, b.length); } private byte[] loadClassData(String name) { // load the class data from the connection . . . } }
下面就「手把手」一步步教你如何實現一個自定義類加載器!
第一步: 新建一個MyClassLoader
繼承 ClassLoader
public class MyClassLoader extends ClassLoader {}
第二步:添加自定義的類加載器屬性和 構造函數
public class MyClassLoader extends ClassLoader { /** * 類加載器名稱 */ private String name; /** * 自定義加載路徑 */ private String path; /** * 自定義加載文件後綴類型 */ private final String fileType = ".class"; public MyClassLoader(String name,String path){ //讓系統類加載器(AppClassLoader)成爲該類加載器的父類加載器 super(); this.name = name; this.path = path; } public MyClassLoader(ClassLoader parent,String name,String path){ //顯示指定該類的父類加載器 super(parent); this.name = name; this.path = path; } @Override public String toString() { return this.name; } }
第三步:重寫findClass
方法,自定義咱們本身的查詢類的方式,而後經過defineClass
方法將一個 byte 數組轉換爲 Class 類的實例。
/** * 加載咱們本身定義的類,經過咱們本身定義的類加載器 * @param name 二進制的文件名稱 * @return Class實例對象 * @throws ClassNotFoundException */ @Override protected Class<?> findClass(String name) throws ClassNotFoundException { //獲取class文件的字節數組 byte[] resultData = this.loadByteClassData(name); return super.defineClass(name, resultData, 0, resultData.length); } /** * 加載指定路徑下面的class文件的字節數組 * @param name 二進制文件名稱 ,例如:com.learn.classloader.Demo * @return 二進制字節數組 */ private byte[] loadByteClassData(String name) { byte[] classData = null; InputStream in = null; ByteArrayOutputStream os = null; try { // 好比 有包名 二進制文件名:com.learn.classloader.Demo // 轉換爲本地路徑 com/learnclassloader/Demo.class name = this.path + name.replaceAll("\\.", "/") + fileType; File file = new File(name); os = new ByteArrayOutputStream(); in = new FileInputStream(file); int tmp = 0; while ((tmp = in.read()) != -1){ os.write(tmp); } // 文件流轉爲二進制字節流 classData = os.toByteArray(); }catch (Exception e){ e.printStackTrace(); }finally { try { // 關閉流 if(in != null){ in.close(); } if(os != null){ os.close(); } }catch (Exception e){ e.printStackTrace(); } } return classData; }
上面三步整合在一塊兒,就實現了一個簡單的類加載! 查看自定義類加載器的所有代碼。
/** * 自定義類加載器 * * @author:dufyun * @version:1.0.0 * @date 2019/3/28 */ public class MyClassLoader extends ClassLoader { /** * 類加載器名稱 */ private String name; /** * 自定義加載路徑 */ private String path; /** * 自定義加載文件後綴類型 */ private final String fileType = ".class"; public MyClassLoader(String name,String path){ //讓系統類加載器(AppClassLoader)成爲該類加載器的父類加載器 super(); this.name = name; this.path = path; } public MyClassLoader(ClassLoader parent,String name,String path){ //顯示指定該類的父類加載器 super(parent); this.name = name; this.path = path; } /** * 加載咱們本身定義的類,經過咱們本身定義的類加載器 * @param name 二進制的文件名稱 * @return Class實例對象 * @throws ClassNotFoundException */ @Override protected Class<?> findClass(String name) throws ClassNotFoundException { //獲取class文件的字節數組 byte[] resultData = this.loadByteClassData(name); return super.defineClass(name, resultData, 0, resultData.length); } /** * 加載指定路徑下面的class文件的字節數組 * @param name 二進制文件名稱 ,例如:com.learn.classloader.Demo * @return 二進制字節數組 */ private byte[] loadByteClassData(String name) { byte[] classData = null; InputStream in = null; ByteArrayOutputStream os = null; try { // 好比 有包名 二進制文件名:com.learn.classloader.Demo // 轉換爲本地路徑 com/learn/classloader/Demo.class name = this.path + name.replaceAll("\\.", "/") + fileType; File file = new File(name); os = new ByteArrayOutputStream(); in = new FileInputStream(file); int tmp = 0; while ((tmp = in.read()) != -1){ os.write(tmp); } // 文件流轉爲二進制字節流 classData = os.toByteArray(); }catch (Exception e){ e.printStackTrace(); }finally { try { // 關閉流 if(in != null){ in.close(); } if(os != null){ os.close(); } }catch (Exception e){ e.printStackTrace(); } } return classData; } @Override public String toString() { return this.name; } }
第五: 簡單的測試自定義類加載器!
MyClassLoader myClassLoaderA = new MyClassLoader("aflyun", "F:\\tmp\\"); System.out.println("myClassLoaderA :" + myClassLoaderA.getParent().getClass().getName()); // 設置父類加載器 爲null ,啓動類加載器 MyClassLoader myClassLoaderB = new MyClassLoader(null, "aflyun", "F:\\tmp\\"); System.out.println("myClassLoaderB :" + myClassLoaderB.getParent()); ------------ myClassLoaderA :sun.misc.Launcher$AppClassLoader myClassLoaderB :null
看到這裏,你若是對本篇第一小節理解的話,這裏確定會好奇,MyClassLoader
是怎麼設置AppClassLoader
爲父類加載器的,在代碼中只寫了一個 super();
系統類加載器(AppClassLoader)成爲該類加載器的父類加載器。仍是看源碼 ,使用super();
在初始化的時候調用CLassLoader
的構造函數,在此構造函數中有一個getSystemClassLoader()
獲取的就是 AppClassLoader
。
protected ClassLoader() { this(checkCreateClassLoader(), getSystemClassLoader()); }
咱們在看一下 getSystemClassLoader()
的實現,其中有一個initSystemClassLoader()
方法獲取到Launcher
初始化加載的ClassLoader
,而後將此ClassLoader
賦值給 ClassLoader
類 中的 scl
!具體源碼以下:
// The class loader for the system private static ClassLoader scl; public static ClassLoader getSystemClassLoader() { initSystemClassLoader(); // 獲取系統初始化的ClassLoader if (scl == null) { return null; } SecurityManager sm = System.getSecurityManager(); if (sm != null) { checkClassLoaderPermission(scl, Reflection.getCallerClass()); } return scl; } private static synchronized void initSystemClassLoader() { if (!sclSet) { // 省略其餘代碼..... sun.misc.Launcher l = sun.misc.Launcher.getLauncher(); if (l != null) { Throwable oops = null; //從Launcher 中獲取ClassLoader也就是 AppClassLoader scl = l.getClassLoader(); // 省略其餘代碼..... } sclSet = true; } }
最後 this(checkCreateClassLoader(), getSystemClassLoader());
調用了ClassLoader
的另外一個構造函數,具體看下面源碼,比較簡單,就不作太多說明了。
// 這段代碼上一篇文章就介紹過 private ClassLoader(Void unused, ClassLoader parent) { this.parent = parent; // 將獲取的系統類加載器做爲父類加載器,經過getParent()獲取! if (ParallelLoaders.isRegistered(this.getClass())) { parallelLockMap = new ConcurrentHashMap<>(); // 省略其餘代碼..... } else { // no finer-grained lock; lock on the classloader instance parallelLockMap = null; // 省略其餘代碼..... } } /** * 獲取父類加載器 */ public final ClassLoader getParent() { if (parent == null) return null; SecurityManager sm = System.getSecurityManager(); if (sm != null) { checkClassLoaderPermission(parent, Reflection.getCallerClass()); } return parent; }
<font color='blue'>一張圖在此說明:當前的類加載器就是自定義類加載器MyClassLoader
!</font>
MyClassLoader myClassLoaderA = new MyClassLoader("aflyun", "F:\\tmp\\");
<font color='blue'>tips1 :自定義類加載說明</font>
在實現自定義類加載器過程當中能夠重寫findClass
也能夠重寫loadClass
,但通常建議重寫findClass
!
<font color='blue'>tips2 :自定義類加載器設置了加載路徑path,其實以前介紹過的類加載器也有對應的加載路徑。</font>
// BootStrapClassLoader String bootClassPath = System.getProperty("sun.boot.class.path"); // ExtClassLoader String var0 = System.getProperty("java.ext.dirs"); // AppClassLoader String var1 = System.getProperty("java.class.path");
F:\tmp\
下(無包名)一個工程中(有包名)F:盤中代碼
public class HelloWorld { public HelloWorld() { System.out.println("--F: --HelloWorld--" + this.getClass().getClassLoader()); } }
工程中代碼
:
package com.learn.classloader; public class HelloWorld { public HelloWorld() { System.out.println("--IDEA --HelloWorld--" + this.getClass().getClassLoader()); } }
執行類加載代碼
:
MyClassLoader myClassLoader = new MyClassLoader("myClassLoader","F:/tmp/"); Class<?> cls = null; try { //加載類文件 cls = myClassLoader.loadClass("HelloWorld"); cls.newInstance();//實例化類。調用構造方法 } catch (Exception e) { e.printStackTrace(); }
思考一下打印的結果是什麼呢?
答案:--F: --HelloWorld--myClassLoader
緣由分析:HelloWorld
類的class文件只有在F:/tmp/
下存在,因此就加載的是F盤下的HelloWorld
類文件!
F:\tmp\
,一個在工程中F:盤中代碼
package com.learn.classloader; public class HelloWorld { public HelloWorld() { System.out.println("--F:com.learn.classloader --HelloWorld--" + this.getClass().getClassLoader()); } }
工程中的代碼
:
package com.learn.classloader; public class HelloWorld { public HelloWorld() { System.out.println("--IDEA --HelloWorld--" + this.getClass().getClassLoader()); } }
執行類加載代碼
:
MyClassLoader myClassLoader = new MyClassLoader("myClassLoader","F:/tmp/"); Class<?> cls = null; try { cls = myClassLoader.loadClass("com.learn.classloader.HelloWorld"); cls.newInstance(); } catch (Exception e) { e.printStackTrace(); }
思考一下打印的結果是什麼呢?
答案:--IDEA --HelloWorld--sun.misc.Launcher$AppClassLoader@18b4aac2
緣由分析:F盤中HelloWorld
和工程中的HelloWorld
具備同樣的包名,而且MyClassLoader
沒有設置父類加載器,那麼默認的父類加載類就是AppClassLoader
,根據以前所學的雙親委派模型,HelloWorld
類文件會首先被父類加載器加載,也就是被AppClassLoader
加載,只要父類加載器加載成功,子類加載器就不會在進行加載!
F: 盤 MyHelloWorld
package com.learn.classloader; public class MyHelloWorld { public MyHelloWorld() { System.out.println("--F:com.learn.classloader --MyHelloWorld--" + this.getClass().getClassLoader()); } }
D:盤 MyHelloWorld
package com.learn.classloader; public class MyHelloWorld { public MyHelloWorld() { System.out.println("--D:com.learn.classloader --MyHelloWorld--" + this.getClass().getClassLoader()); } }
此時新增一個myClassLoaderB,加載MyHelloWorld
!
加載的代碼以下:
MyClassLoader myClassLoader = new MyClassLoader("myClassLoader","F:/tmp/"); MyClassLoader myClassLoaderB = new MyClassLoader(myClassLoader,"myClassLoader","D:/tmp/"); Class<?> cls = null; try { cls = myClassLoaderB.loadClass("com.learn.classloader.MyHelloWorld"); cls.newInstance(); } catch (Exception e) { e.printStackTrace(); }
思考一下打印的結果是什麼呢?
答案:--F:com.learn.classloader --MyHelloWorld--myClassLoader
緣由分析:myClassLoaderB
父類加載器是myClassLoader
,此時在加載 MyHelloWorld
,首先會被父類加載器myClassLoader
加載!
代碼示例和示例3同樣!只是修改類加載器!代碼以下:
MyClassLoader myClassLoaderC = new MyClassLoader(null,"myClassLoaderC","D:/tmp/"); Class<?> cls = null; try { cls = myClassLoaderC.loadClass("com.learn.classloader.MyHelloWorld"); cls.newInstance(); } catch (Exception e) { e.printStackTrace(); }
思考一下打印的結果是什麼呢?
答案:--D:com.learn.classloader --MyHelloWorld--myClassLoaderC
緣由分析:myClassLoaderC
父類加載器是啓動類加載器
,在啓動類加載器
中找不到MyHelloWorld
,轉一圈回來仍是須要myClassLoaderC
本身去加載!
本文實現了一個自定義的類加載器,而且經過簡單的案例進行講解和說明,讓咱們更加深刻的瞭解類加載器的雙親委派模式和實現原理。
對類加載器有了比較深刻的學習和思考以後,會對咱們之後寫Java代碼會有必定幫助,而且在遇到一些Java的異常如ClassNotFoundException
可以快速知道緣由。 其實類加載的知識還有不少,在這裏先拋出兩個問題:
<font color='blue'>問題一、Java熱部署如何實現 ? </font>修改一個Java文件後,不須要啓動服務,就能夠動態生效! (目前的主流開發工具都支持,如IDEA 在windows下 Ctrl+Shirt+F9,動態編譯,動態加載!或者 SpringBoot經過配置devtools實現熱部署的原理是什麼?)
本質上是更新clas文件內容! 不須要從新啓動服務!
<font color='blue'>問題二、以前一直提的類加載模式: 雙親模式模式!可是在Tomcat中你知道 WebappClassLoader 的加載機制嗎?</font>
說明:WebappClassLoader 會先加載本身的Class ,找不到在委託給parent,破壞雙親委派模式!
後續有時間會去整理,若是你對這兩個問題感興趣,也歡迎在文末留言,一塊兒探討!
《JDK API 文檔》
《深刻理解Java虛擬機》
<font color='red'>備註: 因爲本人能力有限,文中如有錯誤之處,歡迎指正。</font>
謝謝你的閱讀,若是您以爲這篇博文對你有幫助,請點贊或者喜歡,讓更多的人看到!祝你天天開心愉快!
<center><font color='red'>Java編程技術樂園</font>:一個分享編程知識的公衆號。跟着老司機一塊兒學習乾貨技術知識,天天進步一點點,讓小的積累,帶來大的改變!</center>
<p/>
<center><font color='blue'>掃描關注,後臺回覆【資源】,獲取珍藏乾貨! 99.9%的夥伴都很喜歡</font></center>
<p/>
<center>© 天天都在變得更好的阿飛雲</center>