【Java基礎】類加載機制

要明白類加載機制是什麼,首先得知道什麼是類

我們寫的java代碼都是以.java爲後綴的java源碼文件,想要讓計算機識別,需要將.java文件編譯成.class二進制文件,那麼這個class就是我們今天講的類,有.class文件還不行,java程序是運行在JVM上的,所以還需要將.class文件加載到JVM中,所有就引出了咱們今天的話題,類加載機制,那麼類是如何加載的呢

爲了避免開發工具爲我們做的自動化操作,我們就用最原始的寫代碼的方式來看一下,用記事本
測試代碼如下

public class Demo1{
	static {
        System.out.println("Demo1初始化了..");
    }

    public static void main(String[] args) {

        Class clazz = Object.class;
        System.out.println("Object類的加載器爲:" + clazz.getClassLoader());
        System.out.println("Demo1類的加載器爲:" + Demo1.class.getClassLoader());
    }
	
}

看一下運行結果
在這裏插入圖片描述
運行環境有問題的可以看博主這篇文章一看就懂的jdk環境變量配置

輸出第一行Demo1初始化了..說明Demo1這個類加載到了JVM中,換句話說,如果不加載Demo1,程序也沒法跑起來,這個是毋庸置疑的

輸出第三行Demo1類的加載器爲:sun.misc.Launcher$AppClassLoader@73d16e93通過Class對象的getClassLoader方法即可拿到該類的類加載器

那有人會問Object、System類我並沒有去編譯、也沒有去加載,爲何能在代碼中執行呢?

Object、System類大家都知道是在rt.jar包中的,在JVM啓動的時候根加載器會加載java程序中的基本類庫,所以這些都是預加載好了的,就可以直接調用。

那我們能不能拿到根加載器呢,從第二行輸出可以看到是不行的,根加載器不是用java實現的,所以是不可能拿到的

那麼類加載器分別有哪些呢?
當JVM啓動時,會形成由3個類加載器組成的初始類加載器層次結構。

  • Bootstrap ClassLoader:根加載器
  • Extension ClassLoader:擴展類加載器
  • System ClassLoader:系統加載器

也就是這3個類加載器會幫我們加載一些類,那麼加載的類都在哪裏呢?

public class BootstrapTest {

    public static void main(String[] args) {
        URL[] urls = Launcher.getBootstrapClassPath().getURLs();
        for (int i=0; i<urls.length; i++){
            System.out.println(urls[i].toExternalForm());
        }
    }
}

運行這段代碼可以看到結果如下

file:/E:/software/jdk1.8.0_121/jre/lib/resources.jar
file:/E:/software/jdk1.8.0_121/jre/lib/rt.jar
file:/E:/software/jdk1.8.0_121/jre/lib/sunrsasign.jar
file:/E:/software/jdk1.8.0_121/jre/lib/jsse.jar
file:/E:/software/jdk1.8.0_121/jre/lib/jce.jar
file:/E:/software/jdk1.8.0_121/jre/lib/charsets.jar
file:/E:/software/jdk1.8.0_121/jre/lib/jfr.jar
file:/E:/software/jdk1.8.0_121/jre/classes

由運行結果可知,根加載器加載了咱們程序中標準的類庫如rt.jar

再來個Demo看看系統加載器加載類的路徑在哪

import java.net.URL;
import java.util.Enumeration;

public class Demo1 {
    public static void main(String[] args) throws Exception{
        Enumeration<URL> urls = Demo1.class.getClassLoader().getResources("");
        while (urls.hasMoreElements()){
            System.out.println(urls.nextElement());
        }

    }
}

在這裏插入圖片描述
可以看到輸出結果爲
file:/F:/study_demo/
file:/D:/
再看看我的系統環境變量
在這裏插入圖片描述
大家明白了爲什麼要配置classpath了吧

那有人要說了,我在IDE開發工具中沒有配置classpath這個東西

拿intellij idea爲例,它是在下圖所示配置了在哪裏去加載我們自己寫的class文件
在這裏插入圖片描述
除了以上3種類加載器外,我們還可以自定義類加載器,只要繼承了ClassLoader即可

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.lang.reflect.Method;

public class CompileClassLoader extends ClassLoader {

    //讀取一個文件的內容
    private byte[] getBytes(String filename) throws IOException {
        File file = new File(filename);
        long len = file.length();
        byte[] raw = new byte[(int) len];
        try(FileInputStream fin = new FileInputStream(file)) {
            //一次讀取Class文件的全部二進制數據
            int r = fin.read(raw);
            if (r != len) throw new IOException("無法讀取全部文件:" + r + " !=" + len);
            return raw;
        }
    }
    //定義編譯指定Java文件的方法
    private boolean compile(String javaFile) throws IOException{
        System.out.println("CompileClassLoader:正在編譯" + javaFile + "...");
        //調用系統的javac命令
        Process p = Runtime.getRuntime().exec("javac " + javaFile);
        try {
            //其他線程都等待這個線程完成
            p.waitFor();
        }catch (InterruptedException ie){
            System.out.println(ie);
        }
        //獲取javac線程的退出值
        int ret = p.exitValue();
        //返回編譯是否成功
        return ret == 0;
    }

    //重寫ClassLoader的findClass方法
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException{
        Class clazz = null;
        //將包路徑中的點(.)替換成斜線(/)
        String fileStub = name.replace(".", "/");
        String javaFilename = fileStub + ".java";
        String classFilename = fileStub + ".class";
        File javaFile = new File(javaFilename);
        File classFile = new File(classFilename);
        //當指定Java源文件存在,且Class文件不存在,或者Java源文件的修改時間比Class文件的修改時間更晚時,重新編譯
        if (javaFile.exists() && (!classFile.exists() || javaFile.lastModified() > classFile.lastModified())){
            try {
                //如果編譯失敗,或者該Class文件不存在
                if (!this.compile(javaFilename) || !classFile.exists()){
                    throw new ClassNotFoundException("ClassNotFoundException:" + javaFilename);
                }
            }catch (IOException e){
                e.printStackTrace();
            }
        }
        //如果Class文件存在,系統負責將該文件轉換成Class對象
        if (classFile.exists()){
            try {
                //將Class文件的二進制數據讀入數組
                byte[] raw = this.getBytes(classFilename);
                //調用ClassLoader的defineClass方法將二進制數據轉換成Class對象
                clazz = this.defineClass(name, raw, 0, raw.length);
            }catch (IOException e){
                e.printStackTrace();
            }
        }
        //如果clazz爲null,表明加載失敗,則拋出異常
        if (clazz == null){
            throw new ClassNotFoundException(name);
        }
        return clazz;
    }

    //定義一個主方法
    public static void main(String[] args) throws Exception{
        //如果運行該程序時沒有參數,即沒有目標類
        if (args.length < 1){
            System.out.println("缺少目標類,請按如下格式運行Java源文件:");
            System.out.println("java CompileClassLoader ClassName");
        }
        //第一個參數是需要運行的類
        String progClass = args[0];
        //剩下的參數將作爲目標類時的參數,將這些參數複製到一個新數組中
        String[] progArgs = new String[args.length - 1];
        System.arraycopy(args, 1, progArgs, 0, progArgs.length);
        CompileClassLoader ccl = new CompileClassLoader();
        //加載需要運行的類
        Class<?> clazz = ccl.loadClass(progClass);
        //獲取需要運行的類的主方法
        Method main = clazz.getMethod("main", (new String[0]).getClass());
        Object argsArray[] = {progArgs};
        main.invoke(null, argsArray);
    }
}

Hello類如下

public class Hello {
	public static void main(String[] args){
		for(String arg : args){
			System.out.println("運行Hello的參數" + arg);
		}
	}
}

查看結果
在這裏插入圖片描述
我們無須手動編譯Hello.java類,我們自定義的ClassLoder幫我們完成了,當然這裏只是爲了演示功能,真正的編譯工作肯定不需要我們自己去做。

爲了驗證Hello類的加載器是我們自定義的CompileClassLoader,我們可以在Hello類中打印System.out.println(Hello.class.getClassLoader());
結果如下
在這裏插入圖片描述
bingo!
多說幾句,其實從上面的代碼中就可以看到類加載的過程,先要編譯java源文件,然後讀取class二進制文件到內存中,最後將二進制數據轉換成Class對象,我們就可以通過Class對象去實例化對象了。

    //調用系統的javac命令
    Process p = Runtime.getRuntime().exec("javac " + javaFile);

這行代碼就相當於我們在DOS窗口輸入的javac命令

ps:本文部分示例來自李剛老師《瘋狂java講義》一書

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