要明白類加載機制是什麼,首先得知道什麼是類
我們寫的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講義》一書