天天看點

從JDK源碼級别剖析JVM類加載機制

main方法的執行流程

從JDK源碼級别剖析JVM類加載機制

image-20230224180340262

1:先把檔案編譯成class檔案

2:window下,java.exe調用jvm.dll建立虛拟機,dll檔案相當于jar包。

3:c++建立引導類加載器。

4:執行個體化Launcher,由引導類加載器加載。

5:加載JvmTest.class檔案

6:執行main方法

loadClass加載流程,類是如何加載到java虛拟機的?

static int a = 10;
int b = 5;
           

1:加載:将位元組碼檔案丢到記憶體

2:驗證:需要驗證位元組碼格式是否正确。Cafe baba标準的位元組碼檔案開頭。

3:準備:對靜态變量進行指派,賦上預設值,将a賦為0。

4:解析:将符号引用轉變成直接引用,隻是将靜态的能夠确定被誰調用的才會被轉變,這個叫做靜态解析,類加載時候完成。

動态解析,在編譯的時候沒有辦法确定被誰調用,隻能在運作的時候才能判斷出來,比如多态。類運作時候完成。

​ 符号引用包括三種:類、接口的符号引用。字段的符号引用。方法的符号引用。

​ 直接引用:符号的記憶體位址。

5:初始話:将靜态遍變量真正指派,将a指派為10,靜态代碼塊也會在這裡執行 。

6:将class檔案加載到JVM虛拟機中。

類的加載機制一般是懶加載,在隻有用到的時候才會真正的加載。

public class JvmTest {
    static int b = 10;
    static {
        System.out.println("jvmTest靜态方法");
        System.out.println("b的值 = " + b);
    }
    public static void main(String[] args) {
        System.out.println("執行main方法");
        new A();
    }
}
class A {
    static {
        System.out.println("A的靜态方法");
    }
    A() {
        System.out.println("A的構造方法");
    }
}
class B {
    static {
        System.out.println("B的靜态方法");
    }
    B(){
        System.out.println("B的構造方法");
    }
}
           

執行結果:

jvmTest靜态方法
b的值 = 10
執行main方法
A的靜态方法
A的構造方法
           

A的靜态代碼塊輸出是在main方法之後,也就是在真正使用的時候才會被加載。然後B沒有被使用,是以不會被加載。

類加載器和雙親委派機制

類加載器分為:

​ 1:引導類加載器:是由c++建立,在jvm建立之後加載,它負責加載JRE下lib目錄的jar。

​ 2:擴充類加載器:負責加載JER下lib的ext,extClassLoader

​ 3:應用程式加載器:負責加載應用程式,appClassLoader

​ 4:自定義加載器:自己定義的

System.out.println("String加載類:" + String.class.getClassLoader());
System.out.println("DESKeyFactory加載類:" + com.sun.crypto.provider.DESCipher.class.getClassLoader());
System.out.println("JvmTest加載類:" + JvmTest.class.getClassLoader());
           

輸出:

String加載類:null 
DESKeyFactory加載類:sun.misc.Launcher$ExtClassLoader@372f7a8d
JvmTest加載類:sun.misc.Launcher$AppClassLoader@18b4aac2
           

String的加載類是引導類加載器加載的,引導類是由c++生成的,是以看不到,是個null。

extClassLoader是擴充類加載器。

AppClassLoader引用程式加載器。

Launcher

C++引導類加載完之後會執行個體化Launcher

class Launcher{
private ClassLoader loader;
private static Launcher launcher = new Launcher();
public static Launcher getLauncher() {
     return launcher;
}
    ... ...
}
           

launcher是個靜态變量,是個單例,在加載的時候就初始話好了。

初始話的時候執行個體化ext和app ClassLoader

public Launcher() {
//執行個體化擴充類加載器,它的父類沒有設定值
Launcher.ExtClassLoader var1 = 
        Launcher.ExtClassLoader.getExtClassLoader();
//初始化APPclassLoader,并且将ext傳入設定為父類加載器    
this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
}
           

loader指派為appClassLoader,預設先用appClassLoader加載

雙親委派機制

從JDK源碼級别剖析JVM類加載機制

image-20230224132150805

C++在加載的時候調用

Launcher.getLauncher().getClassLoader().loadClass("JvmTest.class")
           

Launcher的classLoader是在執行個體化的時候指派了AppClassLoader。

1:AppClassLoader去自己已經加載的類裡邊找,如果沒有向上委托

2:ExtClassLoader去自己已經加載的類裡邊找,如果沒有向上委托。

3:引導類去自己加載的類裡邊找,如果沒有,

4:引導類嘗試加載,判斷這個類是不是應該由自己加載,判斷是不是JRE包路徑下的,如果是加載并且傳回,不是向下傳播

5:ExtClassLoader判斷這個類是不是ext包路徑下的,如果是自己加載并傳回,不是向下傳播

6:AppClassLoader判斷這個類是不是classPath路徑下的,如果是的化加載。不是的話,ClassNotFound

為什麼先由AppClassLoader,而不是引導類開始?

因為大部分類都是我們自己寫的,百分之95都是存放到AppClassLoader,隻有第一次加載的時候會多走一步,之後大部分都直接從AppClassLoader裡邊擷取。

loadClass("JvmTest.class")源碼

Class<?> loadClass(String name, boolean resolve){
    //從自己加載的類裡邊找
    Class<?> c = findLoadedClass(name);
  if (c == null) {
     if (parent != null) {
            //父加載器加載
            c = parent.loadClass(name, false);
        } else {
            //ext的父是null,這是最後一層,引導類類加載器
            c = findBootstrapClassOrNull(name);
      }
      if (c == null) {
              //由URLClassLoader實作
            c = findClass(name);
       }
   }
    return c;
}
           

URLClassLoader.findClass源碼

Class<?> findClass(final String name){
    String path = name.replace('.', '/').concat(".class");
    //判斷是不是自己要加載的。jrt/  ext/
    Resource res = ucp.getResource(path, false);
    if (res != null) {
        //真正加載class的方法
        return defineClass(name, res);
    }
    if (result == null) {
        throw new ClassNotFoundException(name);
    }
    return result;
}
           

新增一個String類

package java.lang;
public class String {
    public static void main(String[] args) {
        System.out.println(111);
    }
}
           

執行報錯資訊:

錯誤: 在類 java.lang.String 中找不到 main 方法, 請将 main 方法定義為:
   public static void main(String[] args)
否則 JavaFX 應用程式類必須擴充javafx.application.Application
           

我們新增的 java.lang.String,是能夠在引導類加載器的JRE包下邊找到的,并且傳回String類,這個傳回的是jdk自帶的類,并不是我們自己寫的類,是以會報找不到main方法。

jdk為什麼要用雙親委派機制?

jdk不讓修改自己内部的類,沙箱安全機制,防止核心API被篡改。

避免類的重複加載,父加載器已經加載完了,自己就不用再加載了。

全盤負責:

public class JvmTest {
    static User user;
}
           

當加載JvmTest類的時候,也會加載User類,這兩個類都會由同一個類加載器加載,不會由亂七八糟的加載器加載。

自定義加載器

我們要自定義加載器,隻需要實作ClassLoader,重寫findClass就可以。

public class MyClassLoader extends ClassLoader {
    private String classPath;

    public MyClassLoader(String classPath) {
        this.classPath = classPath;
    }
    private byte[] loadByte(String name) throws Exception {
        name = name.replaceAll("\\.", "/");
        FileInputStream fis = new FileInputStream(classPath + "/" + name + ".class");
        int len = fis.available();
        byte[] data = new byte[len];
        fis.read(data);
        fis.close();
        return data;
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        try {
            byte[] data = loadByte(name);
            //真正的加載步驟
            return defineClass(name, data, 0, data.length);
        } catch (Exception e) {
            e.printStackTrace();
            throw new ClassNotFoundException();
        }
    }
    public static void main(String[] args) throws Exception {
        MyClassLoader myClassLoader =
                new MyClassLoader("C:/myClassLoader");
        Class<?> clazz = myClassLoader.loadClass("com.bbk.code.User");
        Object o = clazz.newInstance();
        Method method = clazz.getDeclaredMethod("eat");
        method.invoke(o);
        System.out.println("目前MyClassLoader的類加載器:"+MyClassLoader.class.getClassLoader());
    }
}
           

自定義classLoade繼承ClassLoader初始話會先初始化父類,在這時候會給自定義classLoader指派parent為AppClassLoader。具體代碼展現在:

protected ClassLoader() {
        this(checkCreateClassLoader(), getSystemClassLoader());
    }
private ClassLoader(Void unused, ClassLoader parent) {
        this.parent = parent;
        ... ...
}
public static ClassLoader getSystemClassLoader() {
        initSystemClassLoader();
          ... ...
        return scl;
}
private static synchronized void initSystemClassLoader() {
    ... ...
    sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
    scl = l.getClassLoader(); //傳回this.loader,引導類在加載Launcher的時候會指派為AppClassLoader
    ... ...
}
           

執行main方法,需要把User的class從target中删除,并且加入到我們自定義的檔案夾中。執行輸出

我是User類eat方法,我的加載器是com.bbk.code.MyClassLoader@5a07e868
           

如何打破雙親委派機制

意思就是不在委托父加載,直接自己加載類。雙親委派的邏輯代碼實作在loadClass方法中。需要繼承ClassLoader重寫loadClass方法即可:

從JDK源碼級别剖析JVM類加載機制

image-20230224173458980

把紅框的代碼删除掉。

@Override
    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);
            if (c == null) {
                c = findClass(name);
            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
           

重新執行main方法,報錯:

java.io.FileNotFoundException: C:\myClassLoader\java\lang\Object.class (系統找不到指定的路徑。)
           

在加載User的時候,會現在父類Object,我們打破雙親委派之後,自定義的加載器不存在Object是以會報錯。

我們把Object.class放入到我們的本地檔案夾中。重新執行報錯:

//禁止加載java.lang 包
java.lang.SecurityException: Prohibited package name: java.lang
           

java.lang必須由我們引導類加載器加載,沙箱安全。我們隻能讓我們的引導類去加載java.lang。修改代碼

@Override
    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);
            if (c == null) {
                if(!name.startsWith("com.bbk")){
                    c = this.getParent().loadClass(name);
                }else{
                    c = findClass(name);
                }

            }
            if (resolve) {
                resolveClass(c);
            }
            return c;
        }
    }
           

隻有當name是由com.bbk開頭的由我們加載器加載。其他的都由父加載器加載。不會報錯。

繼續閱讀