面试干货 | Java 能否自定义一个类叫 java.lang.System?
本文由讀者 apdoer 投稿,apdoer 是一個極具鉆研精神的 Java 猿,技術牛X頭發茂盛!?
博客地址:https://blog.csdn.net/m0_43452671
緣起:一個面試題
最近在上下班地鐵刷博客,無意刷到一個面試題,號稱很多程序員的烈士公墓:
java 能否自己寫一個類叫 java.lang.System
博主也提供了相關的答案:
一般情況下是不可以的,但是可以通過特殊的處理來達到目的,這個特殊的處理就是自己寫個類加載器來加載自己寫的這個 java.lang.System 類。
然后隨手又刷了幾個,基本雷同,看到的博客都是在講 java 類加載的雙親委托機制, 一個類在需要加載時,會向上委托,直到最上層的 bootstrapClassLoader ,然后最上層的 bootstrapClassLoader 如果能在自己對應的目錄加載就加載,不能就向下查找。
而 bootstrapClassLoader 會加載系統默認的 System 類,所以我們自定義的就不會被加載。
但是我們自定義一個類加載器加載特定路徑的,避開 jvm 默認的三個類加載器的加載路徑,就可以使我們的自定義 System 類被加載。
可是真的是這樣嗎?
為了弄清楚這個問題,我又看了下類加載。
什么是類加載
類加載指的是將類 Class 文件讀入內存,并為之創建一個 java.lang.Class 對象, class 文件被載入到了內存之后,才能被其它 class 所引用
jvm 啟動的時候,并不會一次性加載所有的 class 文件,而是根據需要去動態加載
java 類加載器是 jre 的一部分,負責動態加載 java 類到 java 虛擬機的內存
類的唯一性由類加載器和類共同決定
還了解到系統的三種類加載器:
AppClassLoader?: 也稱為 SystemAppClass 加載當前應用的 classpath 的所有類。
ExtClassLoader?: 擴展的類加載器,加載目錄 %JRE_HOME%\lib\ext 目錄下的 jar 包和 class 文件。還可以加載?-D java.ext.dirs?選項指定的目錄。
BoostrapClassLoader?: 最頂層的加載類,主要加載核心類庫, %JRE_HOME%\lib 下的 rt.jar、resources.jar、charsets.jar 和 class 等。另外需要注意的是可以通過啟動 jvm 時指定 -Xbootclasspath 和路徑來改變 Bootstrap ClassLoader 的加載目錄。比如 java -Xbootclasspath/a:path 被指定的文件追加到默認的 bootstrap 路徑中。
瞄一眼源碼,在Launcher類中
public class Launcher {private static URLStreamHandlerFactory factory = new Launcher.Factory();private static Launcher launcher = new Launcher();private static String bootClassPath = System.getProperty("sun.boot.class.path");private ClassLoader loader;private static URLStreamHandler fileHandler;public static Launcher getLauncher() {return launcher;}public Launcher() {// 創建ExtClassLoaderLauncher.ExtClassLoader var1;var1 = Launcher.ExtClassLoader.getExtClassLoader();//創建AppClassLoaderthis.loader = Launcher.AppClassLoader.getAppClassLoader(var1);//設置AppClassLoader為線程上下文類加載器Thread.currentThread().setContextClassLoader(this.loader);}public ClassLoader getClassLoader() {return this.loader;}public static URLClassPath getBootstrapClassPath() {return Launcher.BootClassPathHolder.bcp;}//AppClassLoaderstatic class AppClassLoader extends URLClassLoader {public static ClassLoader getAppClassLoader(final ClassLoader var0) throws IOException {final String var1 = System.getProperty("java.class.path");public Class<?> loadClass(String var1, boolean var2) throws ClassNotFoundException {}//ExtClassLoaderstatic class ExtClassLoader extends URLClassLoader {private static volatile Launcher.ExtClassLoader instance;public static Launcher.ExtClassLoader getExtClassLoader() throws IOException {}//創建ExtClassLoaderprivate static Launcher.ExtClassLoader createExtClassLoader() throws IOException {}private static File[] getExtDirs() {String var0 = System.getProperty("java.ext.dirs");File[] var1;這段源碼有以下幾點
Launcher?類在構造函數初始化了?ExtClassLoader?和?AppClassLoader?并設置?AppClassLoader?為線程上下文類加載器。
代碼里面沒有告訴我們?BoostrapClassLoader?從哪里來的,但卻為其指定了要加載 class 文件的路徑?sun.boot.class.path?。
BoostrapClassLoader 是由 c++ 編寫的,內嵌在 jvm 中,所以不能顯示的看到他的存在【這個不是從源碼中得到】。
實踐出真知
我們通過代碼來檢驗下上面的理論。
類加載器的父子關系
public class Test {public static void main(String[] args) {System.out.println(Test.class.getClassLoader());System.out.println(Test.class.getClassLoader().getParent());System.out.println(Test.class.getClassLoader().getParent().getParent());} }這段代碼我們可以看到類加載器的父子關系, APPClassLoader->ExtClassLoader->BoostrapClassLoader , 但是 BoostrapClassLoader 無法顯示的獲取到,只能看到是個 null 。
源碼中的路徑到底加載哪些目錄
sun.boot.class.path
可以看到是 jre/lib 目錄下一些核心 jar
java.ext.dirs
java.class.path
可以看到,各個加載器加載的對應路徑和前面的介紹是吻合的
類加載的雙親委托機制
這里直接來一張圖(processon 圖庫滿了,這個先將就下):
如果看不太懂可以看下以下解釋
一個 class 文件發送請求加載,會先找到自定義的類加載器,當然這里沒畫出來。
APPClassLoader 得到加載器請求后,向上委托交給 ExtClassLoader , ExtClassLoader 同理會交給 BoostrapClassLoader ,這是向上委托方向。
最終到達 BoostrapClassLoader ,會先在緩存中找,沒有就嘗試在自己能加載的路徑去加載,找不到就交給 ExtClassLoader ,同理一直到用戶自定義的 ClassLoader ,這就是向下查找方向。
前面說的類的唯一性由類和類加載器共同決定, 這樣保證了確保了類的唯一性。
弄清楚這些,我們可以開始驗證自定義的類加載器是否可以加載我們自定義的這個System類了
自定義類加載器
新建一個?MyClassLoader?繼承?ClassLoader?,并重寫 loadclass 方法
這里的代碼很容易看懂,就不贅述了。
測試
由于 System 需要用于打印獲取結果,這里就用同屬 lang 包的 Long 類:
public class Long {public void testClassLoader(){System.out.println("自定義Long類被"+Long.class.getClassLoader()+"加載了");}public static void main(String[] args) {System.out.println("Long");} }運行自定義 Long 類中 main 方法 報錯如下:
出錯原因很簡單,這個自定義的 Long 類申請加載后,會被委托到 BoostrapClassLoader,BoostrapClassLoader 會在向下查找的過程中找到 rt.jar 中的 java.lang.Long 類并加載,執行 main 方法時,找不到 main 方法,所以報找不到 main 方法。
public class MyLong {public void testClassLoader(){System.out.println("自定義Math類被"+MyLong.class.getClassLoader()+"加載了");}public static void main(String[] args) {System.out.println("mylong");} }我們再定義一個自定義的 java.lang.MyLong 類,執行 main 方法,報錯如下
很明顯的堆棧信息,禁止使用的包名 java.lang ,我們點進去 preDefineClass 看看:
private ProtectionDomain preDefineClass(String name,ProtectionDomain pd){if (!checkName(name))throw new NoClassDefFoundError("IllegalName: " + name);if ((name != null) && name.startsWith("java.")) {throw new SecurityException("Prohibited package name: " + name.substring(0, name.lastIndexOf('.')));}if (pd == null) {pd = defaultDomain;}if (name != null) checkCerts(name, pd.getCodeSource());return pd; }可以看到,當如果類的全路徑名以 java. 開頭時,就會報錯,看到這里,開頭的答案你是否有了結果呢?
我們梳理一下過程,如果用自定義的類加載器加載我們自定義的類
會調用自定義類加載器的?loadClass?方法。
而我們自定義的 classLoader 必須繼承 ClassLoader,loadClass 方法會調用父類的 defineClass 方法。
而父類的這個 defineClass 是一個 final 方法,無法被重寫
所以自定義的 classLoader 是無論如何也不可能加載到以?java.?開頭的類的。
到這里,最開始的問題已經有了答案。我們無法自定義一個叫 java.lang.System 的類。
思考
如果我把 MyLong 打成 jar 放到 BoostrapClassLoader 的加載路徑呢?讓 BoostrapclassLoader 去加載,具體操作如下,在 jdk 的 jre 目錄下創建 classes 目錄,然后把 MyLong.jar 復制進去,再通過 vmOptions 追加這個 classes 目錄以使 BoostrapClassLoader 加載:
可以看到仍然加載不了,如果能加載,在控制臺是會有 load 信息的,如果不是 java.lang.Long ,是可以跨過 APPClassLoader 和 ExtClassLoader 來讓 boostraPClassloader 來加載的,這里就不演示了,操作很簡單。
下面是vm參數
-Xbootclasspath/a:c:\classloader.jar -verbose由一個面試題引起的類加載器思考,既然已經寫到這里,干脆把線程上下文類加載器也一并學習了。
拓展線程上下文類加載器
為什么不和前面三種類加載器放在一起說呢,這個線程上下文類加載器只是一個概念,是一個成員變量,而前三種是確切存在的,是一個類,我們來看一下 Thread 的源碼:
public class Thread implements Runnable {private ClassLoader contextClassLoader;public void setContextClassLoader(ClassLoader cl) {SecurityManager sm = System.getSecurityManager();if (sm != null) {sm.checkPermission(new RuntimePermission("setContextClassLoader"));}contextClassLoader = cl;}@CallerSensitivepublic ClassLoader getContextClassLoader() {if (contextClassLoader == null)return null;SecurityManager sm = System.getSecurityManager();if (sm != null) {ClassLoader.checkClassLoaderPermission(contextClassLoader,Reflection.getCallerClass());}return contextClassLoader;} }特點
線程上下文類加載器是一個成員變量,可以通過相應的方法來設置和獲取。
每個線程都有一個線程類加載器,默認是?AppClassLoader?。
子線程默認使用父線程的?ClassLoader?,除非子線程通過上面的?setContextClassLoader?來設置。
測試
針對以上兩點簡單測試一下:
public class Test {public static void main(String[] args) {Thread thread = new Thread(()->{});System.out.println(thread.getContextClassLoader());thread.setContextClassLoader(Test.class.getClassLoader().getParent());System.out.println(thread.getContextClassLoader());} } public class Test {public static void main(String[] args) {Thread thread = new Thread(()->{});Thread.currentThread().setContextClassLoader(Test.class.getClassLoader().getParent());thread.setContextClassLoader(Test.class.getClassLoader().getParent());System.out.println(thread.getContextClassLoader());} }可以證明以上三點
總結
java 三種類加載器
一條主線-----路徑
一個機制->雙親委托
兩個方向->向上委托,向下查找
好了,本文就先介紹到這里,有問題歡迎留言討論。
【End】
查看更多面試題內容,請訪問《Java最常見200+面試題全解析》,它包含的模塊有:
Java、JVM?最常見面試題解析
Spring、Spring?MVC、MyBatis、Hibernate?面試題解析
MySQL、Redis?面試題解析
RabbitMQ、Kafka、Zookeeper?面試解析
微服務?Spring?Boot、Spring?Cloud?面試解析
掃描下面二維碼付費閱讀
關注下方二維碼,訂閱更多精彩內容。
轉發朋友圈,是對我最大的支持。
創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎總結
以上是生活随笔為你收集整理的面试干货 | Java 能否自定义一个类叫 java.lang.System?的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 程序员精美简历Top榜—面试必备
- 下一篇: 面试官 | 讲一下如何给高并发系统做限流