JAVA类加载机制

18 阅读4分钟

⼀、快速梳理JAVA类加载机制

三句话总结JDK8的类加载机制:

  1. 类缓存:每个类加载器对他加载过的类都有⼀个缓存。
  2. 双亲委派:向上委托查找,向下委托加载。
  3. 沙箱保护机制:不允许应⽤程序加载JDK内部的系统类。

1、JDK8的类加载体系

可以看到JDK8中的两个类加载体系:

图片.png 左侧是JDK中实现的类加载器,通过parent属性形成⽗⼦关系。应⽤中⾃定义的类加载器的parent都是AppClassLoader

右侧是JDK中的类加载器实现类。通过类继承的机制形成体系。未来我们就可以通过继承相关的类实现⾃定义类加载器。

JDK8的类加载体系:

public class LoaderDemo {
    public static String a ="aaa";
    public static void main(String[] args) throws ClassNotFoundException {
        // ⽗⼦关系 AppClassLoader <- ExtClassLoader <- BootStrap Classloader
        ClassLoader cl1 = LoaderDemo.class.getClassLoader();
        System.out.println("cl1 > " + cl1);
        System.out.println("parent of cl1 > " + cl1.getParent());
        // BootStrap Classloader由C++开发,是JVM虚拟机的⼀部分,本身不是JAVA类。
        System.out.println("grant parent of cl1 > " + cl1.getParent().getParent());
        // String,Int等基础类由BootStrap Classloader加载。
        ClassLoader cl2 = String.class.getClassLoader();
        System.out.println("cl2 > " + cl2);
        System.out.println(cl1.loadClass("java.util.List").getClass().getClassLoader());
        // java指令可以通过增加-verbose:class -verbose:gc 参数在启动时打印出类加载情况
        // 这些参数来⾃于 sun.misc.Launcher 源码
        // BootStrap Classloader,加载java基础类。
        System.out.println("BootStrap ClassLoader加载⽬录:" +
        System.getProperty("sun.boot.class.path"));
        // Extention Classloader 加载⼀些扩展类。 可通过-D java.ext.dirs另⾏指定⽬录
        System.out.println("Extention ClassLoader加载⽬录:" +
        System.getProperty("java.ext.dirs"));
        // AppClassLoader 加载CLASSPATH,应⽤下的Jar包。可通过-D java.class.path另⾏指定⽬录
        System.out.println("AppClassLoader加载⽬录:" +
        System.getProperty("java.class.path"));
    }
}

JDK8中的类加载器都继承于⼀个统⼀的抽象类ClassLoader,类加载的核⼼也在这个⽗类中。其中,加载类的核⼼⽅法如下:

//类加载器的核⼼⽅法
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{
    synchronized (getClassLoadingLock(name)) {
        // 每个类加载起对他加载过的类都有⼀个缓存,先去缓存中查看有没有加载过
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            //没有加载过,就⾛双亲委派,找⽗类加载器进⾏加载。
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
            }
            if (c == null) {
                long t1 = System.nanoTime();
                // ⽗类加载起没有加载过,就⾃⾏解析class⽂件加载。
                c = findClass(name);
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        //这⼀段就是加载过程中的链接Linking部分,分为验证、准备,解析三个部分。
        // 运⾏时加载类,默认是⽆法进⾏链接步骤的。
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

这个⽅法就是最为核⼼的双亲委派机制。并且这个⽅法是protected声明的,这意味着,这个⽅法是可以被⼦类覆盖的。所以,双亲委派机制也是可以被打破的。

当⼀个类加载器要加载⼀个类时,整体的过程就是通过双亲委派机制向上委托查找,如果没有查找到,就向下委托加载。整个过程整理如下图:

图片.png

2、沙箱保护机制

双亲委派机制有⼀个最⼤的作⽤就是要保护JDK内部的核⼼类不会被应⽤覆盖。⽽为了保护JDK内部的核⼼类,JAVA在双亲委派的基础上,还加了⼀层保险。就是ClassLoader中的下⾯这个⽅法。

private ProtectionDomain preDefineClass(String name,ProtectionDomain pd){
    if (!checkName(name))
        throw new NoClassDefFoundError("IllegalName: " + name);
    // 不允许加载核⼼类
    if ((name != null) && name.startsWith("java.")) {
        throw new SecurityException("Prohibited package name: " +name.substring(0, name.lastIndexOf('.')));
    }
    if (pd == null) {
        pd = defaultDomain;
    }
    if (name != null) checkCerts(name, pd.getCodeSource());
        return pd;
}

这个⽅法会⽤在JAVA在内部定义⼀个类之前。这种简单粗暴的处理⽅式,当然是有很多时代的因素。也因此在JDK中,你可以看到很多javax开头的包。这个奇怪的包名也是跟这个沙箱保护机制有关系的。

3、Linking链接过程

在ClassLoader的loadClass⽅法中,还有⼀个不起眼的步骤,resolveClass。这是⼀个native⽅法。⽽其实现的过程称为linking-链接。链接过程的实现功能如下图:

图片.png 其中关于半初始化状态就是JDK在处理⼀个类的static静态属性时,会先给这个属性分配⼀个默认值,作⽤是占住内存。然后等连接过程完成后,在后⾯的初始化阶段,再将静态属性从默认值修改为指定的初始值。

例如参照⼀下下⾯这个案例:

class Apple{
    static Apple apple = new Apple(10);
    static double price = 20.00;
    double totalpay;
    public Apple (double discount) {
        System.out.println("===="+price);
        totalpay = price - discount;
    }
}
public class PriceTest01 {
    public static void main(String[] args) {
        System.out.println(Apple.apple.totalpay);
    }
}

程序打印出的结果是-10 ,⽽不是10。 这感觉有点反直觉,为什么呢?就是因为这个半初始化状态。

其中Apple.apple访问了类的静态变量,会触发类的初始化,即加载-》链接-》初始化

当main⽅法执⾏构造函数时,price还没有初始化完成,处于链接阶段的准备阶段,其值为默认值0。这时构造函数的price就是0,所以最终打印出来的结果是-10 ⽽不是 10