JVM類加載器是否可以加載自定義的String

前言

曾經有一次,面試官問到類加載機制,相信大多數小夥伴都可以答上來雙親委派機制,也都知道JVM出於安全性的考慮,全限定類名相同的String是不能被加載的。但是如果加載了,會出現什麼樣的結果呢?異常?那是什麼樣的異常。如果包名不相同呢?自定義類加載器是否可以加載呢?相信面試官從各種不同的角度出擊,很快就會答出漏洞,畢竟咱沒有深入研究過虛擬機…

接下來筆者就針對上述問題進行一一驗證。該篇文章抱着求證答案的方向出發,並無太多理論方面的詳解。如有理解上的偏差,還望大家不吝賜教。

JVM都有哪些類加載器

首先我們放上一張節選自網絡的JVM類加載機制示意圖

JVM 中內置了三個重要的 ClassLoader,除了 BootstrapClassLoader 其他類加載器均由 Java 實現且全部繼承自java.lang.ClassLoader:

  • BootstrapClassLoader(啓動類加載器) :最頂層的加載類,由C++實現,負責加載 %JAVA_HOME%/lib目錄下的jar包和類或者或被 -Xbootclasspath參數指定的路徑中的所有類。

  • ExtensionClassLoader(擴展類加載器) :主要負責加載目錄 %JRE_HOME%/lib/ext 目錄下的jar包和類,或被 java.ext.dirs 系統變量所指定的路徑下的jar包。

  • AppClassLoader(應用程序類加載器) :面向我們用戶的加載器,負責加載當前應用classpath下的所有jar包和類。

JVM類加載方式

類加載有三種方式:

  • 1、命令行啓動應用時候由JVM初始化加載
  • 2、通過Class.forName()方法動態加載
  • 3、通過ClassLoader.loadClass()方法動態加載

Class.forName()和ClassLoader.loadClass()區別

  • Class.forName():將類的.class文件加載到jvm中之外,還會對類進行解釋,執行類中的static塊;
  • ClassLoader.loadClass():只幹一件事情,就是將.class文件加載到jvm中,不會執行static中的內容,只有在newInstance纔會去執行static塊。
  • Class.forName(name,initialize,loader)帶參函數也可控制是否加載static塊。並且只有調用了newInstance()方法採用調用構造函數,創建類的對象 。

JVM類加載機制

  • 全盤負責,當一個類加載器負責加載某個Class時,該Class所依賴的和引用的其他Class也將由該類加載器負責載入,除非顯示使用另外一個類加載器來載入

  • 父類委託,先讓父類加載器試圖加載該類,只有在父類加載器無法加載該類時才嘗試從自己的類路徑中加載該類

  • 緩存機制,緩存機制將會保證所有加載過的Class都會被緩存,當程序中需要使用某個Class時,類加載器先從緩存區尋找該Class,只有緩存區不存在,系統纔會讀取該類對應的二進制數據,並將其轉換成Class對象,存入緩存區。這就是爲什麼修改了Class後,必須重啓JVM,程序的修改纔會生效

JVM類加載機制源碼

雙親委派模型實現源碼分析

private final ClassLoader parent; 
protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
        synchronized (getClassLoadingLock(name)) {
            // 首先,檢查請求的類是否已經被加載過
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {//父加載器不爲空,調用父加載器loadClass()方法處理
                        c = parent.loadClass(name, false);
                    } else {//父加載器爲空,使用啓動類加載器 BootstrapClassLoader 加載
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                   //拋出異常說明父類加載器無法完成加載請求
                }

                if (c == null) {
                    long t1 = System.nanoTime();
                    //自己嘗試加載
                    c = findClass(name);

                    // this is the defining class loader; record the stats
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }

雙親委派模型的好處

雙親委派模型保證了Java程序的穩定運行,可以避免類的重複加載(JVM 區分不同類的方式不僅僅根據類名,相同的類文件被不同的類加載器加載產生的是兩個不同的類),也保證了 Java 的核心 API 不被篡改。如果沒有使用雙親委派模型,而是每個類加載器加載自己的話就會出現一些問題,比如我們編寫一個稱爲 java.lang.Object 類的話,那麼程序運行的時候,系統就會出現多個不同的 Object 類。

如果我們不想用雙親委派模型怎麼辦?

爲了避免雙親委託機制,我們可以自己定義一個類加載器,然後重寫 loadClass() 即可。

系統類加載器加載自定義String

1. 首先我們看下普通的類加載過程

package com.example.demojava.loadclass;
public class ClassLoaderDemo{

    public static void main(String[] args) {
        System.out.println("ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader());
        System.out.println("The Parent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent());
        System.out.println("The GrandParent of ClassLodarDemo's ClassLoader is " + ClassLoaderDemo.class.getClassLoader().getParent().getParent());
    }
}

結果輸出

ClassLodarDemo's ClassLoader is sun.misc.Launcher$AppClassLoader@18b4aac2
The Parent of ClassLodarDemo's ClassLoader is sun.misc.Launcher$ExtClassLoader@75bd9247
The GrandParent of ClassLodarDemo's ClassLoader is null

AppClassLoader的父類加載器爲ExtClassLoader
ExtClassLoader的父類加載器爲null,null並不代表ExtClassLoader沒有父類加載器,而是 BootstrapClassLoader

2. 我們自己定義一個String類,看下會發生什麼

package com.example.demojava.loadclass;

public class String {
    public static void main(String[] args) {
        System.out.println("我是自定義的String");
    }
}

結果輸出

➜  demo-java javac src/main/java/com/example/demojava/loadclass/String.java 
➜  demo-java java src.main.java.com.example.demojava.loadclass.String 
錯誤: 找不到或無法加載主類 src.main.java.com.example.demojava.loadclass.String

這裏分明有main方法,全限定類名又和jdk的String不在同一個package(不會造成衝突),爲什麼會輸出找不到或無法加載主類呢?

細心的小夥伴一定會發現該類’沒有導入’系統的String類,會不會因爲JVM的類加載機制,AppClassLoader加載類的時候,由於自定義的String被加載,攔截了上層的String類呢?String對象是自定義的,不符合main()方法的定義方式,故系統拋找不到main()方法。

我們反過來驗證下剛纔的推測,再次運行剛纔的ClassLoaderDemo會發生什麼呢?what?IDE中的main()方法去哪裏了?還是手動編譯運行下吧

➜  demo-java javac src/main/java/com/example/demojava/loadclass/ClassLoaderDemo.java 
➜  demo-java java src.main.java.com.example.demojava.loadclass.ClassLoaderDemo 
錯誤: 找不到或無法加載主類 src.main.java.com.example.demojava.loadclass.ClassLoaderDemo

結果顯示: 之前正常運行的java類也找不到主類了。

我們導入正確的String類再來驗證下

package com.example.demojava.loadclass;

public class String {
    public static void main(java.lang.String[] args) {
        System.out.println("我是自定義的String");
    }
}

結果輸出

我是自定義的String

3. 能否覆寫lang包下的String類?

上邊的案例修改包路徑即可

package java.lang;

public class String {
    public static void main(java.lang.String[] args) {
        System.out.println("我是自定義的String");
    }
}

輸出報錯

Connected to the target VM, address: '127.0.0.1:63569', transport: 'socket'
錯誤: 在類 java.lang.String 中找不到 main 方法, 請將 main 方法定義爲:
   public static void main(String[] args)
否則 JavaFX 應用程序類必須擴展javafx.application.Application

**分析:**首先由於全限定類名java.lang.String等於jdk中的String類,根據上邊類加載源碼可知,當AppClassLoader加載該String時,判斷java.lang.String已經加載,便不會再次加載。所以執行的依舊是jdk中的String,但是系統的java.lang.String中沒有main()方法,所以會報錯。這是一種安全機制。

然後驗證下默認的類加載器能否加載自定義的java.lang.String。==,默認的AppClassLoader能加載Everything?

public class LoadStringDemo {

    public static void main(String[] args) {
        URLClassLoader systemClassLoader = (URLClassLoader)ClassLoader.getSystemClassLoader();
        URL[] urLs = systemClassLoader.getURLs();
        for (URL url: urLs) {
            System.out.println(url);
        }
    }
}

輸出日誌如下

...
file:/Users/cuishiying/work/demo-java/target/classes/
...

日誌太多,但是絕對沒有其他的包路徑(當前包下的java.lang.String默認只能時jdk中的)

自定義類加載器

爲什麼會存在自定義類加載器呢

自定義類加載器的核心在於對字節碼文件的獲取,如果是加密的字節碼則需要在該類中對文件進行解密。

因爲實際項目中,會有多種加載.class文件的方式,

  • 從本地系統中直接加載
  • 通過網絡下載.class文件
  • 從zip,jar等歸檔文件中加載.class文件
  • 從專有數據庫中提取.class文件
  • 將Java源文件動態編譯爲.class文件

如何自定義類加載器

package com.example.demojava.loadclass;

import com.demo.ClassLoaderDemo;

import java.io.*;
import java.lang.reflect.Method;


public class MyClassLoader extends ClassLoader {

    private String root;


    /**
     * @param name 全限定類名
     * @return
     * @throws ClassNotFoundException
     */

    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        byte[] classData = loadClassData(name);

        if (classData == null) {
            throw new ClassNotFoundException();
        } else {
            return defineClass(name, classData, 0, classData.length);
        }
    }


    private byte[] loadClassData(String className) {
        String fileName = root + File.separatorChar +
                className.replace('.', File.separatorChar) + ".class";

        try {
            InputStream ins = new FileInputStream(fileName);

            ByteArrayOutputStream baos = new ByteArrayOutputStream();

            int bufferSize = 1024;

            byte[] buffer = new byte[bufferSize];

            int length = 0;

            while ((length = ins.read(buffer)) != -1) {
                baos.write(buffer, 0, length);
            }

            return baos.toByteArray();
        } catch (IOException e) {
            e.printStackTrace();
        }

        return null;
    }

    public String getRoot() {
        return root;
    }

    public void setRoot(String root) {
        this.root = root;
    }

    public static void main(String[] args) throws Exception {

        MyClassLoader classLoader = new MyClassLoader();
        classLoader.setRoot("/Users/cuishiying/Desktop/demo");

        Class<?> clz = Class.forName("LoadDemo", true, classLoader);
        Object  instance = clz.newInstance();
        Method test = clz.getDeclaredMethod("test");
        test.setAccessible(true);
        test.invoke(instance); 

        System.out.println(instance.getClass().getClassLoader());

    }
}

結果輸出

test
com.example.demojava.loadclass.MyClassLoader@75bd9247

由此可知,自定義類加載器已可以正常工作。這裏我們不能把LoadDemo放在類路徑下,由於雙親委託機制的存在,會直接導致該類由 AppClassLoader加載,而不會通過我們自定義類加載器來加載。

自定義類加載器加載手寫java.lang.String

改寫自定義類加載器的main()方法

    public static void main(String[] args) throws Exception {

        MyClassLoader classLoader = new MyClassLoader();
        classLoader.setRoot("/Users/cuishiying/Desktop/demo");

        Class<?> clz = classLoader.findClass("java.lang.String");
        Object  instance = clz.newInstance();

        System.out.println(instance.getClass().getClassLoader());
    }

JVM由於安全機制拋出了SecurityException

/Users/cuishiying/Desktop/demo/java/lang/String.class
Exception in thread "main" java.lang.SecurityException: Prohibited package name: java.lang
    at java.lang.ClassLoader.preDefineClass(ClassLoader.java:662)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:761)
    at java.lang.ClassLoader.defineClass(ClassLoader.java:642)
    at com.example.demojava.loadclass.MyClassLoader.findClass(MyClassLoader.java:25)
    at com.example.demojava.loadclass.MyClassLoader.main(MyClassLoader.java:71)

公衆號 【當我遇上你】

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章