JVM類加載過程詳細分析

雙親委派加載模型

爲何須要雙親委派加載模型

主要是爲了安全,避免用戶惡意加載破壞JVM正常運行的字節碼文件,好比說加載一個本身寫的java.util.HashMap.class。這樣就有可能形成包衝突問題。java

類加載器種類

file

  • 啓動類加載器:用於加載jdkrt.jar的字節碼文件
  • 擴展類加載器:用於加載jdk/jre/lib/ext文件夾下的字節碼文件
  • 應用程序類加載器:加載classPath下的字節碼文件
  • 自定義類加載器:用戶在程序中本身定義的加載器

源碼分析

一、ClassLoader.loadClass()git

protected Class<?> loadClass(String name, boolean resolve)
        throws ClassNotFoundException
    {
    	// 加鎖
        synchronized (getClassLoadingLock(name)) {
            // First, check if the class has already been loaded
            Class<?> c = findLoadedClass(name);
            // 若是這個Class對象尚未被加載,下面就準備加載
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                	// 查看當前類加載器有沒有父類加載器
                    if (parent != null) {
                    	// 父類加載器來加載字節碼文件
                        c = parent.loadClass(name, false);
                    } else {
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // ClassNotFoundException thrown if class not found
                    // from the non-null parent class loader
                }
				// 若是父類加載器也沒有加載這個Class對象,就由本身來加載
                if (c == null) {
                    // If still not found, then invoke findClass in order
                    // to find the class.
                    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;
        }
    }

不遵照雙親委派加載模型的例子

雙親委派加載模型僅僅是一個約定,後面實現類加載器時,是能夠不遵照這個約定。ClassLoader是在JDK1.0的時候就設計好的,而雙親委派加載模型在JDK1.2引入的。因此,有些機制是沒有遵照這個約定的。好比:Service Provider Interface機制的JDBC就沒有遵照這個約定。github

一、爲何JDBC沒法遵照這個約定?
JDBCSPI機制的一個例子,JDK定義了java.sql.Connection核心接口,後續MySQLOracle爲其提供實現類。在運行中是經過java.sql.DriverManager來獲取指定實現類的實例。這裏須要明白三個問題:sql

  • java.sql.DriverManager是在rt.jar中,由核心類加載器加載的;
  • 第三方所提供Collection的實現類都是在classpath中;
  • 類中方法想加載新的字節碼文件時,其初始類加載器就是當前這個類的定義類加載器;

也就是說當JVMjava.sql.DriverManager類的getConnection()方法中獲取Collection實現類的字節碼時,當前類的定義類加載器是啓動類加載器,而按照約定啓動類加載器是不容許加載classpath下的字節碼。因此,JDBC就沒法遵照這個約定。安全

二、JDBC是如何解決上面的問題的?
爲了解決這個,java在線程中放入一個類加載器Thread.currentThread().getContextClassLoader();而這個類加載器能夠是隨意的。好比你想加載classpath包下的字節碼文件,只須要設置當前線程的類加載器爲應用程序類加載器便可。網絡

JVM類加載過程

JVM本質的工做就是讀取字節碼文件、執行字節碼文件中的指令。其中JVM將讀取字節碼文件的過程稱爲JVM類加載過程。app

JVM讀取的字節碼文件將放在方法區裏;jvm

JVM類加載機制分爲五個部分:加載、驗證、準備、解析、初始化。以下圖所示:
fileide

1、Loading:加載

這一步是將JVM外的字節碼文件加載到JVM內部方法區中的Class對象。源碼分析

JVM能夠經過幾種方式來加載外部的字節碼文件?

  • 從本地讀字節碼文件;
  • 從網絡讀取字節碼文件;
  • 經過動態生成的字節碼文件;

初始類加載器和定義類加載器

因爲雙親委派加載模型的存在,一個Class對象的初始類加載器initiating class loader和定義類加載器defining class loader有可能不是同一個。

  • 初始類加載器:它是指讓JVM加載這個字節碼文件
  • 定義類加載器:它是真正調用defineClass方法,將字節碼轉換成Class對象

java在判斷instanceof時,只有類名、defining class loader都相等,才表示是同一個類的實例。

Class.getClassLoader()獲得的是定義類加載器

相關實驗代碼

一、驗證使用不一樣ClassLoader加載字節碼文件

// 這種方法是不遵照雙親委派加載模型的約定
public class ClassLoaderLoading {
    public static void main(String[] args) throws ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        // 這個Class對象是由當前方法的類加載器加載
        Class c1 = MiniJVM.class;
        Class c2 = new MyClassLoader().loadClass("com.github.hcsp.MiniJVM");
        // 使用c2建立一個MiniJVM實例
        Object o = c2.getConstructor().newInstance();
        System.out.println(o instanceof MiniJVM);
        MiniJVM demo = (MiniJVM) o;
    }

    private static class MyClassLoader extends ClassLoader {
        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            if (name.contains("MiniJVM")) {
                try {
                    byte[] bytes = Files.readAllBytes(new File("target/classes/com/github/hcsp/MiniJVM.class").toPath());
                    return defineClass(name, bytes, 0, bytes.length);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            } else {
                return super.loadClass(name);
            }
        }
    }
}

二、實現一個遵照雙親委派加載模型的類加載器

public class ClassLoaderLoading {
    public static void main(String[] args) throws ClassNotFoundException {
        Class c1 = MiniJVM.class;
        Class c2 = new MyClassLoader(ClassLoader.getSystemClassLoader()).loadClass("com.github.hcsp.MiniJVM");
        System.out.println("c2 = " + c2);
    }

    private static class MyClassLoader extends ClassLoader {
        public MyClassLoader(ClassLoader systemClassLoader) {
            super(systemClassLoader);
        }
        
        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            // 加載你想讓這個類加載器加載的字節碼文件
            if (name.contains("MiniJVM")) {
                try {
                    byte[] bytes = Files.readAllBytes(new File("target/classes/com/github/hcsp/MiniJVM.class").toPath());
                    return defineClass(name, bytes, 0, bytes.length);
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            } else {
                // 其餘的字節碼文件交由父類加載器加載
                return super.loadClass(name);
            }
        }
    }
}

2、Linking:連接

當一個.java文件編譯成.class文件時,裏面含有一個符號引用,好比/java/utils/HashMapLinking是指將這符號引用與具體的class對象連接起來。

每一個字節碼結構都有一個運行時常量池,它會存儲每一個符號引用和所對應的具體對象,以此實現連接。

  • Verification:驗證字節碼的正確性
  • Preparation:爲static成員賦默認初始值
  • Resolution:解析當前字節碼裏包含的其餘符號引用

3、Initializing

執行初始化方法。好比下面的四個虛擬機指令:newgetstaticputstaticinvokestatic

原博客地址

相關文章
相關標籤/搜索