从源码级剖析Java类加载原理

相信大多数熟悉Java的研发工程师,都知道Java类加载原理:Java中的类是由类加载器采用双亲委派机制进行加载。其中,Java核心库中实现了三种类型的类加载器,它们分别是:引导类加载器BootstrapLoader、扩展类加载器ExtClassloader、应用程序类加载器AppClassloader,它们之间存在父子关系,不过这种关系不是所谓的子类继承父类的关系,而是基于委托之间的父子关系。另外,用户可以通过继承java.lang.ClassLoader实现自定义类加载器。
下面,我们来看看Java核心类库的三种类加载器,示例代码如下:

/**
 * Java核心类库的三种类加载器
 *
 * @author 编程老司机
 * @date 2023-06-15
 */
public class JDKClassLoaderMechanismDemo {

    public static void main(String[] args) {

        System.out.println("******* Java核心类库的三种类加载器示例  *******");
        System.out.println();
        ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
        ClassLoader extClassloader = appClassLoader.getParent();
        ClassLoader bootstrapLoader = extClassloader.getParent();
        // 由于引导类加载器是在JVM启动时通过C++代码创建的,
        // 因此在运行Java代码时可能无法访问它,所以返回null
        System.out.println("BootstrapLoader引导类加载器:" + bootstrapLoader);
        System.out.println("ExtClassloader扩展类加载器" + 
					extClassloader.getClass().getName());
        System.out.println("AppClassLoader应用程序类加载器:" + 
					appClassLoader.getClass().getName());

        System.out.println();
        System.out.println("三种类加载器加载哪些文件:");
        System.out.println("BootstrapLoader加载以下文件:");
        String filePaths = System.getProperty("sun.boot.class.path");
        System.out.println(getPaths(filePaths));

        System.out.println();
        System.out.println("\nExtClassloader加载以下文件:");
        filePaths = System.getProperty("java.ext.dirs");
        System.out.println(getPaths(filePaths));

        System.out.println();
        System.out.println("AppClassLoader加载以下文件:");
        filePaths = System.getProperty("java.class.path");
        System.out.println(getPaths(filePaths));

        System.out.println();
        System.out.println("三种类加载器之间的关系:");
        ClassLoader classLoader = JDKClassLoaderMechanismDemo.class.
						getClassLoader();
        ClassLoader parentClassLoader = classLoader.getParent();
        ClassLoader parentParentClassLoader = parentClassLoader.getParent();
        System.out.println("加载当前类的类加载器:" + 		
		 classLoader.getClass().getName());
        System.out.println(classLoader.getClass().getName() + 
			"类加载器的父类加载器:" 
		+ parentClassLoader.getClass().getName());
        System.out.println(parentClassLoader.getClass().getName() +
	"类加载器的父类加载器:" + (Objects.isNull(parentParentClassLoader) ? 
          "null" : parentParentClassLoader.getClass().getName()));

    }

    public static String getPaths(String filePaths){
        StringJoiner stringJoiner = new StringJoiner("\n");
        for (String path: filePaths.split(";")){
            stringJoiner.add(path);
        }
        return stringJoiner.toString();
    }
}







上述代码运行结果:

******* Java核心类库的三种类加载器示例  *******
    
    BootstrapLoader引导类加载器:null
    ExtClassloader扩展类加载器sun.misc.Launcher$ExtClassLoader
    AppClassLoader应用程序类加载器:sun.misc.Launcher$AppClassLoader

    三种类加载器加载哪些文件:
    BootstrapLoader加载以下文件:
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\resources.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\rt.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\sunrsasign.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\jsse.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\jce.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\charsets.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\jfr.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\classes
    
    ExtClassloader加载以下文件:
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext
    C:\Windows\Sun\Java\lib\ext
    
    AppClassLoader加载以下文件:
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\charsets.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\deploy.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\access-bridge-64.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\cldrdata.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\dnsns.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\jaccess.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\jfxrt.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\localedata.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\nashorn.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\sunec.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\sunjce_provider.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\sunmscapi.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\sunpkcs11.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\ext\zipfs.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\javaws.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\jce.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\jfr.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\jfxswt.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\jsse.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\management-agent.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\plugin.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\resources.jar
    D:\Program Files\Java\jdk1.8.0_152\jre\lib\rt.jar
    D:\workspaces\utils\target\classes
    D:\Program Files\apache-maven-3.8.1\myRepo\junit\junit\4.12\junit-4.12.jar
    D:\Program Files\JetBrains\IntelliJ IDEA 2020.2\lib\idea_rt.jar
  
    三种类加载器之间的关系:
    加载当前类的类加载器:sun.misc.Launcher$AppClassLoader
    sun.misc.Launcher$AppClassLoader类加载器的父类加载器:sun.misc.Launcher$ExtClassLoader
    sun.misc.Launcher$ExtClassLoader类加载器的父类加载器:null

通过运行结果我们可以看出三种不同的类加载器加载的类文件是特定的目录和文件,也看出了三种类加载器的关系。
接下来,在剖析Java类加载原理之前,我们先看一段代码:

类加载的示例代码

/**
 * 剖析类加载原理Demo
 *
 * @author 编程老司机
 * @date 2023-06-13
 */
public class JDKMechanismDemo {

    static {
        System.out.println("加载main方法所在主类JDKMechanismDemo的静态代码块");
    }

    public JDKMechanismDemo() {
        System.out.println("实例化main方法主类JDKMechanismDemo");
    }

    public static void main(String[] args) {
        ClassA a = new ClassA();  // new 关键字告诉JVM生成一个ClassA的实例
        ClassB b = null; // 只声明,未实例,不会被加载
    }
}

class ClassA {

    static {
        System.out.println("加载ClassA");
    }

    public ClassA() {
        System.out.println("实例化ClassA");
    }
}

class ClassB {

    static {
        System.out.println("加载类ClassB");
    }

    public ClassB() {
        System.out.println("实例化ClassB");
    }

}

上述代码运行结果:

加载main方法所在主类JDKMechanismDemo的静态代码块
加载ClassA
实例化ClassA

从运行结果我们看到了,虽然ClassB,我们在代码中声明了,但是它并没有像ClassA输出结果,这说明:JVM在启动程序,执行main方法时不会一次性加载所有类,只加载使用到的类,也就是说,虽然我们的应用程序jar包或者war包中包含很多类,但是并不会被全部加载,而是只加载运行应用程序所需要的类。

类加载器的初始化

阅读过《深入剖析创建Java虚拟机的实现方法》文章的读者应该知道在JVM初始化的时候,通过调用ClassLoader::initialize(THREAD)方法将BootstrapLoader引导类加载器进行初始化的。

下面,我们通过源码了解应用程序类加载器的初始化和扩展类加载器的初始化。

结合《JVM源码分析:剖析JavaMain方法中的LoadMainClass的实现》中的内容,我们知道在调用LoadMainClass方法的时候,JVM通过sun.launcher.LauncherHelper类的checkAndLoadMain方法,通过sloader.loadClass(var3)获取到main方法主类,这个sloader对象,通过LauncherHelper源码我们可知,是由下面代码初始化的:

private static final ClassLoader scloader = ClassLoader.getSystemClassLoader();

我们进入ClassLoader.getSystemClassLoader()方法中:

@CallerSensitive
    public static ClassLoader getSystemClassLoader() {
	// 初始化系统类加载器,实际上是应用程序类加载器,对scl变量赋值
        initSystemClassLoader();
        if (scl == null) {
            return null;
        }
        SecurityManager sm = System.getSecurityManager();
        if (sm != null) {
            checkClassLoaderPermission(scl, Reflection.getCallerClass());
        }
	// 返回应用程序类加载器
        return scl;
    }

继续进入initSystemClassLoader()方法:

private static synchronized void initSystemClassLoader() {
        if (!sclSet) {
            if (scl != null)
                throw new IllegalStateException("recursive invocation");
            sun.misc.Launcher l = sun.misc.Launcher.getLauncher();
            if (l != null) {
                Throwable oops = null;
		// scl 赋值,原来是通过sun.misc.Launcher对象获取的
                scl = l.getClassLoader();
		...省略一些不需要关注的代码
            }
            sclSet = true;
        }
    }

我们进入sun.misc.Launcher类:

public class Launcher {
    private static URLStreamHandlerFactory factory = new Launcher.Factory();
    private static Launcher launcher = new Launcher();
    // BootstrapLoader引导类加载器加载的类目录系统属性
private static String bootClassPath = System.getProperty("sun.boot.class.path");
    private ClassLoader loader;
    private static URLStreamHandler fileHandler; 
    // 采用单例设计模式,保证一个JVM虚拟机内只有一个sun.misc.Launcher实例
    public static Launcher getLauncher() {
        return launcher;
    }

    public Launcher() {
        Launcher.ExtClassLoader var1;
        try {
            // 初始化扩展类加载器
            var1 = Launcher.ExtClassLoader.getExtClassLoader();
        } catch (IOException var10) {
            throw new InternalError("Could not create extension class loader",
		 var10);
        }

        try {
	    // 初始化应用程序类加载器,它的父加载器是扩展类加载器
            this.loader = Launcher.AppClassLoader.getAppClassLoader(var1);
        } catch (IOException var9) {
            throw new InternalError("Could not create application class loader", 
		var9);
        }

        Thread.currentThread().setContextClassLoader(this.loader);
        String var2 = System.getProperty("java.security.manager");
        if (var2 != null) {
            SecurityManager var3 = null;
            if (!"".equals(var2) && !"default".equals(var2)) {
                try {
                    var3 = (SecurityManager)this.loader.loadClass(var2)
				.newInstance();
                } catch (IllegalAccessException var5) {
                } catch (InstantiationException var6) {
                } catch (ClassNotFoundException var7) {
                } catch (ClassCastException var8) {
                }
            } else {
                var3 = new SecurityManager();
            }

            if (var3 == null) {
                throw new InternalError("Could not create SecurityManager: " 
					+ var2);
            }

            System.setSecurityManager(var3);
        }

    }

    public ClassLoader getClassLoader() {
        return this.loader;
    }
    ...省略一些不需要关注的代码
}

在sun.misc.Launcher类中,我们也看到为扩展类加载器和应用程序类加载的类定义:

1、扩展类加载器定义

// 继承URLClassLoader,URLClassLoader继承ClassLoader抽象类
static class ExtClassLoader extends URLClassLoader {
        ...
        public ExtClassLoader(File[] var1) throws IOException {
            super(getExtURLs(var1), (ClassLoader)null, Launcher.factory);
	SharedSecrets.getJavaNetAccess().getURLClassPath(this).
		initLookupCache(this);
        }

        private static File[] getExtDirs() {
	    // 扩展类加载器的加载类目录系统属性
            String var0 = System.getProperty("java.ext.dirs");
            File[] var1;
            if (var0 != null) {
                StringTokenizer var2 = new StringTokenizer(var0, 
				File.pathSeparator);
                int var3 = var2.countTokens();
                var1 = new File[var3];

                for(int var4 = 0; var4 < var3; ++var4) {
                    var1[var4] = new File(var2.nextToken());
                }
            } else {
                var1 = new File[0];
            }

            return var1;
        }
	...省略一些不需要关注的代码
}

2、应用程序类加载器定义

// 继承URLClassLoader,URLClassLoader继承ClassLoader抽象类
static class AppClassLoader extends URLClassLoader {
        final URLClassPath ucp = 
SharedSecrets.getJavaNetAccess().getURLClassPath(this);

        public static ClassLoader getAppClassLoader(
		final ClassLoader var0) 
		throws IOException {
	    // 应用程序类加载器加载类目录系统属性
            final String var1 = System.getProperty("java.class.path");
            final File[] var2 = var1 == null ? new File[0] : 
		Launcher.getClassPath(var1);
            return (ClassLoader)AccessController.doPrivileged(new 
		PrivilegedAction<Launcher.AppClassLoader>() {
                public Launcher.AppClassLoader run() {
                    URL[] var1x = var1 == null ? new URL[0] : 
			Launcher.pathToURLs(var2);
                    return new Launcher.AppClassLoader(var1x, var0);
                }
            });
        }

        AppClassLoader(URL[] var1, ClassLoader var2) {
            super(var1, var2, Launcher.factory);
            this.ucp.initLookupCache(this);
        }
	...省略一些不需要关注的代码
    }

总结:引导类加载器是在JVM初始化的时候调用ClassLoader::initialize(THREAD)方法进行初始化的。扩展类加载器和应用程序类加载器都是由sun.misc.Launcher实例进行初始化的,sun.misc.Launcher是采用单例来保证一个JVM虚拟机内只有一个sun.misc.Launcher实例。

那么类是如何加载的呢?

在《JVM源码分析:深入剖析JavaMain方法中的LoadMainClass的实现》章节提到了,调用Java核心rt.jar包中Java启动辅助类sun.launcher.LauncherHelper的checkAndLoadMain方法,可以获取AppClassLoader应用程序类加载器,然后由AppClassLoader加载main主类,那我们就从这里入手分析类的加载机制,进入checkAndLoadMain方法:

public enum LauncherHelper {
    ...
    private static final ClassLoader scloader = ClassLoader.getSystemClassLoader();
    ...
    /* var0参数是指输出什么级别的日志:
     *       var0 = true代表使用标准错误输出流System.err
     *       var0 = false代表使用标准输出流System.out
     * var1参数是指采用哪种模式,有两种模式:
     *       var1 = 1时,参数var2的值代表的是类名;
     *       var1 = 2时,参数var2的值代表的是jar包文件名
     */
    public static Class<?> checkAndLoadMain(boolean var0, int var1, String var2) {
        // 初始化LauncherHelper的输出流
        initOutput(var0);
        String var3 = null;
        switch(var1) {
            case 1:
		// 类名就是var2
                var3 = var2;
                break;
            case 2:
                // 从jar包中获取主类类名
                var3 = getMainClassFromJar(var2);
                break;
            default:
                throw new InternalError("" + var1 + ": Unknown launch mode");
        }

        var3 = var3.replace('/', '.');
        Class var4 = null;

        try {
           // 此处scloader是应用程序类加载器
            var4 = scloader.loadClass(var3);
        } catch (ClassNotFoundException | NoClassDefFoundError var8) {
            if (System.getProperty("os.name", "").contains("OS X") && Normalizer.isNormalized(var3, Normalizer.Form.NFD)) {
                try {
                    var4 = scloader.loadClass(Normalizer.normalize(var3, Normalizer.Form.NFC));
                } catch (ClassNotFoundException | NoClassDefFoundError var7) {
                    abort(var8, "java.launcher.cls.error1", var3);
                }
            } else {
                abort(var8, "java.launcher.cls.error1", var3);
            }
        }

        appClass = var4;
        // 下面是JavaFX应用程序获取主类的流程
        if (!var4.equals(sun.launcher.LauncherHelper.FXHelper.class) && !sun.launcher.LauncherHelper.FXHelper.doesExtendFXApplication(var4)) {
           //校验JavaFX中的主类
            validateMainClass(var4);
            return var4;
        } else {
            sun.launcher.LauncherHelper.FXHelper.setFXLaunchParameters(var2, var1);
            return sun.launcher.LauncherHelper.FXHelper.class;
        }
    }
    ...
}

接下来,我们进入var4 = scloader.loadClass(var3);这行代码中调用的loadClass方法:

public abstract class ClassLoader {
    ... 
    // ClassLoader 类加载器构造函数传参是父加载器
    protected ClassLoader(ClassLoader parent) {
        this(checkCreateClassLoader(), parent);
    }
    ...

    public Class<?> loadClass(String name) throws ClassNotFoundException {
        return loadClass(name, false);
    }

    protected Class<?> loadClass(String name, boolean resolve)
            throws ClassNotFoundException {
        synchronized (getClassLoadingLock(name)) {
            // 首先检查指定类是否已经加载,该方法会调用native方法findLoadedClass0,它由JVM实现
            Class<?> c = findLoadedClass(name);
            if (c == null) {
                long t0 = System.nanoTime();
                try {
                    if (parent != null) {  // 没加载,存在父加载器,就从父加载器中加载指定类
			// 从这里我们就可以看出,类的加载是采用双亲委派机制
                        c = parent.loadClass(name, false);
                    } else {
                        // 引导类加载器是不存在父加载器,以下方法会调用native方法findBootstrapClass,
                        // 通过JVM初始化的引导类加载器(也叫JVM内置类加载器)加载指定类
                        c = findBootstrapClassOrNull(name);
                    }
                } catch (ClassNotFoundException e) {
                    // 有可能父加载器或者引导类加载器中都没有指定类,会加载不到
                }

                if (c == null) {
                    long t1 = System.nanoTime();
                    // 经过上面加载还未加载到类,就调用findClass方法,findClass方法在
                    // 抽象类ClassLoader中并未实现,是ClassLoader的子类实现的
                    c = findClass(name);

                    // ClassLoader记录统计数据
                    sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                    sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                    sun.misc.PerfCounter.getFindClasses().increment();
                }
            }
            if (resolve) {
		// 这行代码的意思是:类加载完成的同时也被解析。该方法会调用native方法
		// resolveClass0,它由JVM实现
		// 有读者可能就会问了,解析是什么意思?这里就涉及到了类的生命周期了,
		// 我给大家普及一下:类的生命周期有:1、加载;2、验证;3、准备;
 		// 4、解析(或者叫链接);5、初始化;6、使用;7、卸载。所以这里的
		// 解析是指的第4步
                resolveClass(c);
            }
            return c;
        }
    }

    protected Class<?> findClass(String name) throws ClassNotFoundException {
        throw new ClassNotFoundException(name);
    }
    ...
}

通过上面代码的分析,我们知道怎么实现双亲委派机制加载类的,下面用一张图来概括:

另外,我们还知道了,类加载的底层也都是由JVM实现的,对于Java应用程序研发工程师,了解到这个程度,在工作中也够用了。

今天,想必读者们也知道了Java类加载原理。如果读者想深入到JVM源码底层了解类加载是如何实现的,后续如果人数多的话,我会单独写一篇文章进行深入分析。

posted @ 2023-06-15 23:59  编程老司机A  阅读(67)  评论(0编辑  收藏  举报