类的生命周期中的第一步,就是要被 JVM 加载进内存,类加载器就是来干这件事。
一、类加载器种类
系统提供了 3 种类加载器:
1.启动类加载器(Bootstrap ClassLoader) 由 C 和 C++ 编写,是在 JVM 启动后初始化的。可在这里查看到源码(OpenJDK):http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/933f6b06c1cb/src/share/native/java/lang/ClassLoader.c 负责将存放在 <JAVA_HOME>jrelib 目录中的,或者被 -Xbootclasspath 参数所指定的路径中的,并且能被虚拟机识别的(仅按照文件名识别,如 rt.jar,名字不符合的类库即使放在 lib 目录中也不会被加载)类库加载到虚拟机内存中。 2.扩展类加载器(Extension ClassLoader) 由 sun.misc.Launcher$ExtClassLoader 实现,负责加载 <JAVA_HOME>jrelibext 目录中的所有类库,以及系统变量 java.ext.dirs 指定路径中的所有类库,开发者可以直接使用扩展类加载器。 3.应用程序类加载器(Application ClassLoader) 由 sun.misc.Launcher$AppClassLoader 实现,可以通过 ClassLoader 类中的 getSystemClassLoader() 方法的获得,所以一般也称它为“系统类加载器”。 它负责加载用户类路径(classpath:CLASSPATH 环境变量指定的, 由 -classpath 或 -cp 选项定义的,或者是 jar 中的 Manifest 的 classpath 属性定义的)上所指定的类库,以及系统变量 java.class.path 指定路径中的所有类库。
开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
除了顶层的启动类加载器外,其余的类加载器都应当有自己的父类加载器(父子关系一般不会以继承的关系实现,而是以组合关系来复用父加载器的代码),结构如图:
getParent() 可获得父加载器
public class ClassLoaderTest { public static void main(String[] args) { ClassLoader classLoader = ClassLoaderTest.class.getClassLoader(); // 默认由 AppClassLoader 加载类 System.out.println(classLoader); // ExtClassLoader System.out.println(classLoader.getParent()); // Bootstrap ClassLoader,由 JVM 启动 System.out.println(classLoader.getParent().getParent()); } }
除系统提供的加载器外,还可以自己定义类加载器。(继承 java.lang.ClassLoader 类实现)
二、类加载器工作方式(加载机制)
2.1.委托机制(委派模型 或 父委派模型)
委派模型是描述类加载器之间的层次关系。
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此。
因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(找不到所需的类)时,子加载器才会尝试自己去加载。
在 java.lang.ClassLoader 中的 loadClass() 方法中实现该了过程。
public Class<?> loadClass(String name) throws ClassNotFoundException { return loadClass(name, false); } 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) { // 如果从非 null 的父类加载器中找不到该类,则抛出 ClassNotFoundException } if (c == null) { long t1 = System.nanoTime(); // 如果仍未找到,则调用 findClass 查找该类 c = findClass(name); // 这是定义的类加载器; 记录统计数据 sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0); sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1); sun.misc.PerfCounter.getFindClasses().increment(); } } if (resolve) { // 解析类,属于类加载的 link 阶段 resolveClass(c); } return c; } } /** * ClassLoader 的子类建议重写 findClass 方法,而不是 loadClass */ protected Class<?> findClass(String name) throws ClassNotFoundException { throw new ClassNotFoundException(name); }
自己写的 java.lang.String 类,是否可以替换 JDK 自带的类?
答案是不行的。但这非委托机制解决的,因为委托机制是可以被打破的,完全可以写一个 classLoader 来加载自己写的 java.lang.String 类。
2.2.可见性机制
子类加载器可以看到父类加载器加载的类,而反之则不行。当 Abc.class 已经被 Application 类加载器加载过了,然后想要使用 Extension 类加载器加载这个类,将会抛出 java.lang.ClassNotFoundException 异常。
2.3.单一性机制
根据委托机制,父加载器加载过的类不能被子加载器加载第二次。虽然重写 loadClass() 的类加载器可以做到不遵守委托机制和单一性机制,但这样做并不可取。
判断类是否“相等”
因此,比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个 Class 文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那么这两个类就必定不相等。
这里的“相等”,包括代表类的 Class 对象的 equals() 方法、isInstance() 方法的返回结果,也包括使用 instanceof 关键字做对象所属关系判定等情况。
关于破坏委派模型
java 引入了线程上下文类加载器(Thread Context ClassLoader),这个类加载器可以通过 Thread 类的 setContextClassLoader 进行设置,默认继承父线程类加载器,也可由父类加载器请求子类加载器完成类加载动作。
https://docs.oracle.com/javase/8/docs/api/java/lang/ClassLoader.html
https://docs.oracle.com/javase/tutorial/ext/basics/load.html
https://blog.csdn.net/lengxiao1993/article/details/86689331
https://github.com/doocs/jvm/blob/master/docs/10-class-loader.md