⼀、快速梳理JAVA类加载机制
三句话总结JDK8的类加载机制:
- 类缓存:每个类加载器对他加载过的类都有⼀个缓存。
- 双亲委派:向上委托查找,向下委托加载。
- 沙箱保护机制:不允许应⽤程序加载JDK内部的系统类。
1、JDK8的类加载体系
可以看到JDK8中的两个类加载体系:
左侧是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声明的,这意味着,这个⽅法是可以被⼦类覆盖的。所以,双亲委派机制也是可以被打破的。
当⼀个类加载器要加载⼀个类时,整体的过程就是通过双亲委派机制向上委托查找,如果没有查找到,就向下委托加载。整个过程整理如下图:
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-链接。链接过程的实现功能如下图:
其中关于半初始化状态就是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