类加载器深入剖析

JVM 的类加载器

Java 的类加载,就是把字节码格式“.class”文件加载到 JVM 的方法区,并在 JVM 的堆区建立一个java.lang.Class对象的实例,用来封装 Java 类相关的数据和方法。那 Class 对象又是什么呢?你可以把它理解成业务类的模板,JVM 根据这个模板来创建具体业务类对象实例。
JVM 并不是在启动时就把所有的“.class”文件都加载一遍,而是程序在运行过程中用到了这个类才去加载。
JVM 类加载是由类加载器来完成的,JDK 提供一个抽象类 ClassLoader,这个抽象类中定义了三个关键方法,理解清楚它们的作用和关系非常重要。
public abstract class ClassLoader { // 每个类加载器都有个父加载器 private final ClassLoader parent; public Class<?> loadClass(String name) { // 查找一下这个类是不是已经加载过了 Class<?> c = findLoadedClass(name); // 如果没有加载过 if( c == null ){ // 先委托给父加载器去加载,注意这是个递归调用 if (parent != null) { c = parent.loadClass(name); }else { // 如果父加载器为空,查找 Bootstrap 加载器是不是加载过了 c = findBootstrapClassOrNull(name); } } // 如果父加载器没加载成功,调用自己的 findClass 去加载 if (c == null) { c = findClass(name); } return c; } protected Class<?> findClass(String name){ //1. 根据传入的类名 name,到在特定目录下去寻找类文件,把.class 文件读入内存 ... //2. 调用 defineClass 将字节数组转成 Class 对象 return defineClass(buf, off, len); } // 将字节码数组解析成一个 Class 对象,用 native 方法实现 protected final Class<?> defineClass(byte[] b, int off, int len){ ... } }
从上面的代码我们可以得到几个关键信息:
  • JVM 的类加载器是分层次的,它们有父子关系,每个类加载器都持有一个 parent 字段,指向父加载器。
  • defineClass 是个工具方法,它的职责是调用 native 方法把 Java 类的字节码解析成一个 Class 对象,所谓的 native 方法就是由 C 语言实现的方法,Java 通过 JNI 机制调用。
  • findClass 方法的主要职责就是找到“.class”文件,可能来自文件系统或者网络,找到后把“.class”文件读到内存得到字节码数组,然后调用 defineClass 方法得到 Class 对象。
  • loadClass 是个 public 方法,说明它才是对外提供服务的接口,具体实现也比较清晰:
    • 首先检查这个类是不是已经被加载过了,如果加载过了直接返回,否则交给父加载器去加载。
      请你注意,这是一个递归调用,也就是说子加载器持有父加载器的引用,当一个类加载器需要加载一个 Java 类时,会先委托父加载器去加载,然后父加载器在自己的加载路径中搜索 Java 类,当父加载器在自己的加载范围内找不到时,才会交还给子加载器加载,这就是双亲委托机制。
       
JDK 中有哪些默认的类加载器?它们的本质区别是什么?为什么需要双亲委托机制?JDK 中有 3 个类加载器,另外你也可以自定义类加载器,它们的关系如下图所示。
  • BootstrapClassLoader 是启动类加载器,由 C 语言实现,用来加载 JVM 启动时所需要的核心类,比如rt.jarresources.jar等。
  • ExtClassLoader 是扩展类加载器,用来加载\jre\lib\ext目录下 JAR 包。
  • AppClassLoader 是系统类加载器,用来加载 classpath 下的类,应用程序默认用它来加载类。
  • 自定义类加载器,用来加载自定义路径下的类。
这些类加载器的工作原理是一样的,区别是它们的加载路径不同,也就是说 findClass 这个方法查找的路径不同。
双亲委托机制是为了保证一个 Java 类在 JVM 中是唯一的,假如你不小心写了一个与 JRE 核心类同名的类,比如 Object 类,双亲委托机制能保证加载的是 JRE 里的那个 Object 类,而不是你写的 Object 类。
 
这是因为 AppClassLoader 在加载你的 Object 类时,会委托给 ExtClassLoader 去加载,而 ExtClassLoader 又会委托给 BootstrapClassLoader,BootstrapClassLoader 发现自己已经加载过了 Object 类,会直接返回,不会去加载你写的 Object 类。
这里请你注意,类加载器的父子关系不是通过继承来实现的,比如 AppClassLoader 并不是 ExtClassLoader 的子类,而是说 AppClassLoader 的 parent 成员变量指向 ExtClassLoader 对象

类加载器的分类

有两种类型的类加载器
  1. java虚拟机自带的加载器
      • 根加载器(BootStrap)
        • 根加载器没有父加载器.它负责加载虚拟机的核心类库,如Java.lang.*等。根加载器从系统属性sun.boot.class.path所指定的目录中加载类库
          根加载器的实现依赖于底层操作系统,属于虚拟机的实现一部分,并没有继承java.lang.ClassLoader
      • 扩展加载器(Extension)
        • 它的父加载器为根类加载器.它从java.ext.dirs系统属性所指定的目录中加载类库,或者从JDK的安装目录的jre\lib\ext子目录下加载类库.
          如果把用户创建的jar文件文件放在这个目录下,也会自动由扩展类加载器加载,扩展类加载器是纯java类,java.lang.ClassLoader的子类
      • 系统(应用)类加载器(System)
        • 父加载器为扩展类加载器,它是从环境变量classpath或者系统属性java.class.path所指定的目录中加载类,它是用户自定义的类加载器的默认父加载器
          是纯java类,**是java.lang.ClassLoader的子类
  1. 用户自定义的类加载器
      • 必须继承java.lang.ClassLoader
      • 用户可以自定义定制类的加载方式
案例
public class MyTest13 { public static void main(String[] args) { ClassLoader classLoader = ClassLoader.getSystemClassLoader(); System.out.println(classLoader); while (null != classLoader) { classLoader = classLoader.getParent(); System.out.println(classLoader); } } } //sun.misc.Launcher$AppClassLoader@18b4aac2 系统类加载器 //sun.misc.Launcher$ExtClassLoader@4554617c 扩展类加载器 //null 根类加载器 ,打印返回null 源码注释 /** * Returns the parent class loader for delegation. Some implementations may * use <tt>null</tt> to represent the bootstrap class loader. This method*/

类加载器加载类的时机

JVM规范允许类加载器在预料某个类将要被使用时就预先加载它,
如果在预先加载的过程中遇到了.class文件缺失或存在错误,类加载器必须在程序首次主动使用该类时才报告错误(LinkageError错误)
直接删除.class文件不行,会报ClassNotFindException
如果这个类一直没有被程序主动使用,那么类加载器就不会报告错误
public class ClassA { public static void main(String[] args) { System.out.println(MyChild1.str); } } class MyParent1 { public static String str = "hello world"; static { System.out.println("MyParent1 static block"); } } class MyChild1 extends MyParent1 { public static String str2 = "welcome"; static { System.out.println("MyChild1 static block"); } }
添加虚拟机参数追踪类加载过程
MyChild1没有主动使用,但是类加载器任然加载了它,因为MyChild1.str存在引用关系,
编译器先去解析Mychild1,将符号引用解析为直接引用,会加载子类,
这也是阿里java开发规范中类变量必须用所在类名去引用的原因,减少了jvm不必要的开销

类加载器的双亲委派模型

类加载器用来把类加载到java虚拟机中,从JDK1.2版本开始,类的加载过程采用双亲委托机制,
这种机制能更好地保证java平台的安全.在此委托机制中,除了Java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器.
当Java程序请求加载器loader1加载Samle类时,loader1首先委托自己的父加载器去加载Sample类,如果父加载器能加载,则由父加载器完成加载任务,否则才由加载器loader1本身加载
总结,自下向上委托请求,自上向下尝试加载
实际上是包含关系,而不是继承关系
案例1
public class Test7 { public static void main(String[] args) throws ClassNotFoundException { Class<?> clazz1 = Class.forName("java.lang.String"); System.out.println(clazz1.getClassLoader()); Class<?> clazz2 = Class.forName("com.google.it.C"); System.out.println(clazz2.getClassLoader()); } } class C {} // null 根加载器加载类返回 // sun.misc.Launcher$AppClassLoader@18b4aac2 系统类加载器 $符表示内部类

类加载器的双亲委托模型的优点

  • 可以确保java核心库的类型安全问题:所有的java应用都至少会引用java.lang.Object类,也就是在运行期,java.lang.Object这个类会被加载到java虚拟机中;如果这个加载过程是由java应用自己的类加载器所完成的,那么很可能会在JVM中存在多个版本(多个命名空间)的java.lang.Object类,而且这些类之间还是不兼容的,相互不可见的.借助于双亲委托机制,java核心类库中的类的加载工作都是由启动类加载器来统一完成,从而确保了java应用所使用的都是同一个版本的java核心类库,他们之间是互相兼容的
  • 可以确保java核心类库所提供的类不会被自定义的类所替代
  • 不同的类加载器可以为相同名称(binary name)的类创建额外的命名空间,相同名称的类可以并存在java虚拟机中,只需要用不同的类加载器来加载他们即可.不同类加载器所加载的类之间是不兼容的,这就相当于在java虚拟机内部创建了一个又一个相互隔离的Java类空间,这类技术在很多框架中都得到了实际应用.

类加载器的获取方式

  • 获得当前类的ClassLoader : clazz.getClassLoader();
  • 获取当前线程上下文的ClassLoader:Thread.currentThread.getContexClassLoader()
  • 获取系统的ClassLoader: ClassLoader.getSystemClassLoader()
  • 获得调用者的ClassLoader: DriverManager.getCallerClassLoader()
案例2
public class MyTest14 { public static void main(String[] args) throws IOException { ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); String resourceName = "com/google/it/MyTest13.class"; Enumeration<URL> urls = classLoader.getResources(resourceName); while (urls.hasMoreElements( )) { URL url = urls.nextElement(); System.out.println(url); } System.out.println("========"); Class<?> clazz = String.class; // JDK自带的类由根类加载器加载 System.out.println(clazz.getClassLoader()); clazz = MyTest14.class; // 自定义类由系统加载器加载 System.out.println(clazz.getClassLoader()); } } //output: //file:/D:/study/%e7%9f%a5%e8%af%86%e4%bd%93%e7%b3%bb/java%e5%9f%ba%e7%a1%80/jvm/classloader/target/classes/com/google/it/MyTest13.class ======== null sun.misc.Launcher$AppClassLoader@18b4aac2

不同类型元素的类加载器

  1. 基本类型无类加载器
  1. JDK自带的类(如String) 根类加载器
  1. 自定义类包括引入的外部jar包 系统类加载器
  1. 数组类型
      • 数组的Class对象不是由类加载器创建的,而是java运行期根据需要创建的
      • Class.getClassLoader()返回的数组类的类加载器与其元素类型的类加载器相同;
        • 如果元素类型是原始类型,则数组类没有类加载器。
public class MyTest15 { public static void main(String[] args) { String[] strings = new String[2]; // null hotspot虚拟机中是根类加载器的返回值 System.out.println(strings.getClass().getClassLoader()); System.out.println("=============="); MyTest15[] myTest15s = new MyTest15[2]; // 系统类加载器 System.out.println(myTest15s.getClass().getClassLoader()); System.out.println("=============="); int[] ints = new int[2]; // null 没有类加载器 System.out.println(ints.getClass().getClassLoader()); } }

类加载器的命名空间

  • 每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成
  • 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类
  • 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类
变式3 删除claspath下的ClassA.class将loader1作为loader2的父加载器
public static void main(String[] args) throws Exception { MyTest16 loader1 = new MyTest16("loader1"); loader1.setPath("C:\\Users\\wangj\\Desktop\\"); Class<?> clazz = loader1.loadClass("com.google.it.ClassA"); System.out.println("class:" + clazz.hashCode()); Object object = clazz.newInstance(); System.out.println(object); //将loader1作为loader2的父加载器 MyTest16 loader2 = new MyTest16(loader1,"loader2"); loader2.setPath("C:\\Users\\wangj\\Desktop\\"); Class<?> clazz2 = loader2.loadClass("com.google.it.ClassA"); System.out.println("class2:" + clazz2.hashCode()); Object object2 = clazz2.newInstance(); System.out.println(object2); } //输出结果 // findClass invoked: com.google.it.ClassA // class loader name: loader1 // class:356573597 // com.google.it.ClassA@677327b6 // class2:356573597 // com.google.it.ClassA@14ae5a5 // loadClass会执行findLoadedClass,会检查该类是否已经加载过,已经加载直接返回加载的class对象 loadClass源码doc文档 * <li><p> Invoke {@link #findLoadedClass(String)} to check if the class * has already been loaded. </p></li>
从这个变式中,loader1和loader2是同一个类的两个不同实例,但是loader1可以作为loader2的父类加载器,所以所谓的parentClassLoader并不是java中的继承关系,而是包含关系,通过构造方法传入父加载的一个引用
变式4 rebuild项目,恢复classpath下的ClassA.class
public static void main(String[] args) throws Exception { MyTest16 loader1 = new MyTest16("loader1"); loader1.setPath("C:\\Users\\wangj\\Desktop\\"); Class<?> clazz = loader1.loadClass("com.google.it.ClassA"); System.out.println("class:" + clazz.hashCode()); Object object = clazz.newInstance(); System.out.println(object); System.out.println("=========="); //将loader1作为loader2的父加载器 MyTest16 loader2 = new MyTest16(loader1,"loader2"); loader2.setPath("C:\\Users\\wangj\\Desktop\\"); Class<?> clazz2 = loader2.loadClass("com.google.it.ClassA"); System.out.println("class2:" + clazz2.hashCode()); Object object2 = clazz2.newInstance(); System.out.println(object2); System.out.println("=========="); MyTest16 loader3 = new MyTest16("loader3"); loader3.setPath("C:\\Users\\wangj\\Desktop\\"); Class<?> clazz3 = loader3.loadClass("com.google.it.ClassA"); System.out.println("class:" + clazz3.hashCode()); Object object3 = clazz.newInstance(); System.out.println(object3); } //输出结果 class:1163157884 com.google.it.ClassA@74a14482 ========== class2:1163157884 com.google.it.ClassA@1540e19d ========== class:1163157884 com.google.it.ClassA@677327b6 //将ClassA.clas删除,再次运行 //输出结果 findClass invoked: com.google.it.ClassA class loader name: loader1 class:356573597 com.google.it.ClassA@677327b6 ========== class2:356573597 com.google.it.ClassA@14ae5a5 ========== findClass invoked: com.google.it.ClassA class loader name: loader3 class:325040804 com.google.it.ClassA@45ee12a7
变式5 将loader2作为loader3的父加载器
findClass invoked: com.google.it.ClassA class loader name: loader1 class:356573597 com.google.it.ClassA@677327b6 ========== class2:356573597 com.google.it.ClassA@14ae5a5 ========== class:356573597 com.google.it.ClassA@7f31245a //只由loader1加载
变式6 复杂类加载情况的运行分析
public class MyCat { public MyCat() { System.out.println("Mycat is loader by: " + this.getClass().getClassLoader()); } } public class MySample { public MySample(){ System.out.println("MySample is loaderd by: "+this.getClass().getClassLoader()); new MyCat(); } } public class MyTest17 { public static void main(String[] args) throws Exception { MyTest16 loader1 = new MyTest16("loader1"); Class<?> clazz = loader1.loadClass("com.google.it.MySample"); System.out.println("class: " + clazz.hashCode()); //如果注释掉这行,不会实例化MySample方法,也不会实例化MyCat Object object = clazz.newInstance(); } } //输出结果 class: 1956725890 MySample is loaderd by: sun.misc.Launcher$AppClassLoader@18b4aac2 Mycat is loader by: sun.misc.Launcher$AppClassLoader@18b4aac2
变式7
MySampleMyCat的字节码移动到桌面
public class MyTest17_1 { public static void main(String[] args) throws Exception { MyTest16 loader1 = new MyTest16("mytest16"); loader1.setPath("C:\\Users\\wangj\\Desktop\\"); Class<?> clazz = loader1.loadClass("com.google.it.MySample"); System.out.println("class: " + clazz.hashCode()); Object object = clazz.newInstance(); } } //输出 findClass invoked: com.google.it.MySample class loader name: mytest16 class: 1735600054 MySample is loaderd by: MyTest16{classloaderName='mytest16'} findClass invoked: com.google.it.MyCat class loader name: mytest16 Mycat is loader by: MyTest16{classloaderName='mytest16'} //Mycat关联在MySample内,在加载MySample之后会加载MyCat(预加载) //这个预加载是由加载外部类的类加载器进行的,并且任然遵循委托机制
rebuild项目,删除classpath下的myCat 再次运行
class: 1956725890 MySample is loaderd by: sun.misc.Launcher$AppClassLoader@18b4aac2 Exception in thread "main" java.lang.NoClassDefFoundError: com/google/it/MyCat at com.google.it.MySample.<init>(MySample.java:12) at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at java.lang.Class.newInstance(Class.java:442) at com.google.it.MyTest17_1.main(MyTest17_1.java:16) Caused by: java.lang.ClassNotFoundException: com.google.it.MyCat at java.net.URLClassLoader.findClass(URLClassLoader.java:381) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ... 7 more //加载Sample类,通过委托机制最终由系统类加载器加载,系统类加载器在加载MySample类时, //会预加载其内部关联的MyCat,将加载的请求自下向上委托,再自上向下尝试加载,最终返回给系统类加载器,因为classpath下的MyCat已经被删除,无法加载而报错
rebuild项目,classpath下删除MySample类,再次运行
findClass invoked: com.google.it.MySample class loader name: mytest16 class: 1735600054 MySample is loaderd by: MyTest16{classloaderName='mytest16'} Mycat is loader by: sun.misc.Launcher$AppClassLoader@18b4aac2 //Sample类最终由自定义加载器加载,Mycat由自定义类加载器开始向上委托,最终由系统类加载器加载
修改MyCat类 在其中显示地引用MySample.class
public class MyCat { public MyCat() { System.out.println("Mycat is loader by: " + this.getClass().getClassLoader()); System.out.println(MySample.class); } } //再次运行,输出 findClass invoked: com.google.it.MySample class loader name: mytest16 class: 1735600054 MySample is loaderd by: MyTest16{classloaderName='mytest16'} Mycat is loader by: sun.misc.Launcher$AppClassLoader@18b4aac2 Exception in thread "main" java.lang.NoClassDefFoundError: com/google/it/MySample at com.google.it.MyCat.<init>(MyCat.java:12) at com.google.it.MySample.<init>(MySample.java:12) at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at java.lang.Class.newInstance(Class.java:442) at com.google.it.MyTest17_1.main(MyTest17_1.java:16) Caused by: java.lang.ClassNotFoundException: com.google.it.MySample at java.net.URLClassLoader.findClass(URLClassLoader.java:381) at java.lang.ClassLoader.loadClass(ClassLoader.java:424) at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ... 8 more //解析: //MySample类由自定义类加载器加载,Mycat类由系统类加载器,前者类加载器命名空间包含后者,子加载器加载的类可以访问父类加载器加载的类,父加载器加载的类访问不了子载器加载的类
还原MyCat类,修改MySample类,,显示地引用MyCat
public class MySample { public MySample() { System.out.println("MySample is loaderd by: " + this.getClass().getClassLoader()); new MyCat(); System.out.println(MyCat.class); } } findClass invoked: com.google.it.MySample class loader name: mytest16 class: 1735600054 MySample is loaderd by: MyTest16{classloaderName='mytest16'} Mycat is loader by: sun.misc.Launcher$AppClassLoader@18b4aac2
变式8 类加载器的加载目录
public class MyTest18 { public static void main(String[] args) { //启动类加载器(根类加载器) System.out.println(System.getProperty("sun.boot.class.path")); //拓展类加载器 System.out.println(System.getProperty("java.ext.dirs")); //classpath System.out.println(System.getProperty("java.class.path")); } } D:\Java\jdk1.8\jre\lib\resources.jar;D:\Java\jdk1.8\jre\lib\rt.jar;D:\Java\jdk1.8\jre\lib\sunrsasign.jar;D:\Java\jdk1.8\jre\lib\jsse.jar;D:\Java\jdk1.8\jre\lib\jce.jar;D:\Java\jdk1.8\jre\lib\charsets.jar;D:\Java\jdk1.8\jre\lib\jfr.jar;D:\Java\jdk1.8\jre\classes ----------------------- D:\Java\jdk1.8\jre\lib\ext;C:\WINDOWS\Sun\Java\lib\ext ----------------------- D:\Java\jdk1.8\jre\lib\charsets.jar;D:\Java\jdk1.8\jre\lib\deploy.jar;D:\Java\jdk1.8\jre\lib\ext\access-bridge-64.jar;D:\Java\jdk1.8\jre\lib\ext\cldrdata.jar;D:\Java\jdk1.8\jre\lib\ext\dnsns.jar;D:\Java\jdk1.8\jre\lib\ext\jaccess.jar;D:\Java\jdk1.8\jre\lib\ext\jfxrt.jar;D:\Java\jdk1.8\jre\lib\ext\localedata.jar;D:\Java\jdk1.8\jre\lib\ext\nashorn.jar;D:\Java\jdk1.8\jre\lib\ext\sunec.jar;D:\Java\jdk1.8\jre\lib\ext\sunjce_provider.jar;D:\Java\jdk1.8\jre\lib\ext\sunmscapi.jar;D:\Java\jdk1.8\jre\lib\ext\sunpkcs11.jar;D:\Java\jdk1.8\jre\lib\ext\zipfs.jar;D:\Java\jdk1.8\jre\lib\javaws.jar;D:\Java\jdk1.8\jre\lib\jce.jar;D:\Java\jdk1.8\jre\lib\jfr.jar;D:\Java\jdk1.8\jre\lib\jfxswt.jar;D:\Java\jdk1.8\jre\lib\jsse.jar;D:\Java\jdk1.8\jre\lib\management-agent.jar;D:\Java\jdk1.8\jre\lib\plugin.jar;D:\Java\jdk1.8\jre\lib\resources.jar;D:\Java\jdk1.8\jre\lib\rt.jar;D:\study\知识体系\java基础\jvm\classload\out;C:\Program Files\DevelopIDE\IntelliJ IDEA 2018.3.4\lib\idea_rt.jar
更改定义类的加载路径
public class MyTest18_1 { public static void main(String[] args) throws ClassNotFoundException { MyTest16 loader1 = new MyTest16("loader1"); loader1.setPath("\u202AC:\\Users\\wangj\\Desktop\\"); Class<?> clazz = loader1.loadClass("com.google.it.ClassA"); System.out.println("class: "+clazz.hashCode()); System.out.println("class loader: "+clazz.getClassLoader()); } //class: 1956725890 //class loader: sun.misc.Launcher$AppClassLoader@18b4aac2
在jre下新建classes目录
再次执行程序,返回class loader结果为null
public class MyTest19 { public static void main(String[] args) { AESKeyGenerator aesKeyGenerator = new AESKeyGenerator(); System.out.println(aesKeyGenerator.getClass().getClassLoader()); System.out.println(MyTest19.class.getClassLoader()); } } //sun.misc.Launcher$ExtClassLoader@7ea987ac //sun.misc.Launcher$AppClassLoader@18b4aac2
更改扩展类加载器默认加载路径为classes目录
扩展类加载器注意,要用扩展类加载器加载类,类必须要打成jar包
第一行更改扩展类加载器默认加载目录为classes目录,扩展类加载器会从classes目录下的test jar包中加载MyTest1
第二行更改默认加载路径为根目录,找不到test jar 会由系统类加载器加载
启动类加载器注意
  • 启动类加载器是内嵌于vm中的c++代码,启动时加载,会加载java.lang.ClassLoader以及其他的java平台类,当jvm启动时,一块特殊的机器码会运行,它会加载扩展类加载器与系统类加载器,这块特殊的机器码叫做启动类加载器
  • 启动类加载器并不是java类,而其他的加载器则都是Java类,启动类加载器是特定于平台的机器指令,它负责整个加载过程
  • 所有类加载器(除了启动类加载器)都被实现为java类,不过,总归要有一个组件来加载第一个java类加载器,从而让整个加载过程能够顺利进行下去,加载第一个纯java类加载器就是启动加载器的职责
  • 启动类加载器还会负责提供JRE正常运行所需要的基本组件,包括java.utiljava.lang包中的类
更换默认的系统类加载器
获得系统类加载器的官方文档
在自定义类加载器MyTest16中添加一个单个参数为ClassLoader类型的构造方法
在命令行中替换java.system.class.loader的默认属性
在控制台中运行(idea运行不行)

不同类加载器的命名空间关系

  • 同一个命名空间内的类是相互可见的.子加载器的命名空间包含所有父加载器的命名空间.
    • 因此由子加载器加载的类能看见父加载器加载的类,例如系统类加载器加载的类能看见根类加载器加载的类,由父加载器加载的类不能看见子加载器加载的类
  • 如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见
  • 在运行期间,一个java类是由该类的完全限定名(binary name,二进制名)和用于加载该类的定义类加载器(defining loader)所共同决定的,如果同样名字(即相同的完全限定名)的类是由两个不同的类加载器所加载,那么这些类就是不同的,即便.class文件的字节码完全一样,并且从相同的位置加载亦如此
类加载器命名空间与反射实例
public class MyPerson { private MyPerson myPerson; public void setMyPerson(Object o) { this.myPerson = (MyPerson) o; } }
删除classpath下的myPerson,将其复制到桌面
public class MyTest20 { public static void main(String[] args) throws Exception { MyTest16 loader1 = new MyTest16("loader1"); MyTest16 loader2 = new MyTest16("loader2"); loader1.setPath("C:\\Users\\wangj\\Desktop\\"); loader2.setPath("C:\\Users\\wangj\\Desktop\\"); Class<?> clazz1 = loader1.loadClass("com.google.it.MyPerson"); Class<?> clazz2 = loader2.loadClass("com.google.it.MyPerson"); System.out.println(clazz1 == clazz2); Object o1 = clazz1.newInstance(); Object o2 = clazz2.newInstance(); Method method = clazz1.getMethod("setMyPerson",Object.class); method.invoke(o1,o2); } } //输出结果: findClass invoked: com.google.it.MyPerson class loader name: loader1 findClass invoked: com.google.it.MyPerson class loader name: loader2 false Exception in thread "main" java.lang.reflect.InvocationTargetException at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.lang.reflect.Method.invoke(Method.java:498) at com.google.it.MyTest20.main(MyTest20.java:22) Caused by: java.lang.ClassCastException: com.google.it.MyPerson cannot be cast to com.google.it.MyPerson at com.google.it.MyPerson.setMyPerson(MyPerson.java:13) ... 5 more //结果分析: 由类加载器的委托模型可知,最终由自定义类加载器加载了MyPerson.class,loader1和loader2分属2个不同的命名空间,所以彼此返回的class对象不同,无法进行类型转换

Launcher类源码分析(使用openJDK)

//<p> This method is first invoked early in the runtime's startup* sequence, at which point it creates the system class loader and sets it* as the context class loader of the invoking <tt>Thread</tt>. 在运行时启动程序开始创建系统类加载器对象,并将其设为调用线程的初始上下文加载器 @CallerSensitive public static ClassLoader getSystemClassLoader() { initSystemClassLoader(); if (scl == null) { return null; } SecurityManager sm = System.getSecurityManager(); if (sm != null) { checkClassLoaderPermission(scl, Reflection.getCallerClass()); } return scl; } private static synchronized void initSystemClassLoader() { if (!sclSet) { if (scl != null) throw new IllegalStateException("recursive invocation"); sun.misc.Launcher l = sun.misc.Launcher.getLauncher();

自定义类加载器深入解析

系统类加载器默认从classpath下加载类,自定义类加载器将加载类的请求委托其父类加载器(默认为系统类加载器),
类加载又将请求向上传递,然后从根类加载器自上而下尝试加载,最终系统类加载器从classpath下加载ClassA
package com.google.it; import java.io.*; /** * @author 王杰 * @version 2.0.1 * @description 自定义类加载器 * @date 2019/5/31 */ public class MyTest16 extends ClassLoader { private String classloaderName; private String path; private String fileExtension = ".class"; public MyTest16(String classloaderName) { super(); // 默认将系统类加载器当作该类的父类加载器 this.classloaderName = classloaderName; } public MyTest16(ClassLoader parent, String classloaderName) { // 显示指定该类加载器的父加载器 super(parent); this.classloaderName = classloaderName; } public void setPath(String path) { this.path = path; } @Override public String toString() { return "MyTest16{" + "classloaderName='" + classloaderName + '\'' + '}'; } @Override protected Class<?> findClass(String className) throws ClassNotFoundException { System.out.println("findClass invoked: " + className); System.out.println("class loader name: " + this.classloaderName); byte[] data = this.loadClassData(className); return this.defineClass(className, data, 0, data.length); } private byte[] loadClassData(String name) { InputStream is = null; byte[] data = null; ByteArrayOutputStream baos = null; name = name.replace(".", "/"); try { is = new FileInputStream(new File(this.path + name + this.fileExtension)); baos = new ByteArrayOutputStream(); int ch = 0; while (-1 != (ch = is.read())) { baos.write(ch); } data = baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally { try { is.close(); baos.close(); } catch (IOException e) { e.printStackTrace(); } } return data; } public static void main(String[] args) throws Exception { MyTest16 loader1 = new MyTest16("loader1"); test(loader1); //output: com.google.it.ClassA@4554617c并没有执行自定义类加载器的输出语句,即并非由loader1去加载的ClassA } public static void test(ClassLoader classLoader) throws Exception { //loadClass方法会默认调用findClass()方法 Class<?> clazz = classLoader.loadClass("com.google.it.ClassA"); Object object = clazz.newInstance(); System.out.println(object); } }
变式1 将ClassA.class复制到windows路径C:\Users\wangj\Desktop\com\google\it\ClassA,并保留classpath下(即out文件夹下的ClaassA.class)
public static void main(String[] args) throws Exception { MyTest16 loader1 = new MyTest16("loader1"); // loader1.setPath("D:\\study\\知识体系\\java基础\\jvm\\classload\\out\\com\\google\\it"); loader1.setPath("C:\\Users\\wangj\\Desktop\\"); Class<?> clazz = loader1.loadClass("com.google.it.ClassA"); System.out.println("class:" + clazz.hashCode()); Object object = clazz.newInstance(); System.out.println(object); //output: class:1163157884 //com.google.it.ClassA@74a14482 } //仍是由系统类加载器加载 //将classpath下的ClassA.clas删除,系统来加载器无法从classpath下加载A,会让其子加载器加载,由自定义类加载器加载 //findClass invoked: com.google.it.ClassA //class loader name: loader1 //class:356573597 //com.google.it.ClassA@677327b6
变式2 rebuild 恢复classpath下的ClassA.class 创建自定义类加载器loader2
public static void main(String[] args) throws Exception { MyTest16 loader1 = new MyTest16("loader1"); loader1.setPath("C:\\Users\\wangj\\Desktop\\"); Class<?> clazz = loader1.loadClass("com.google.it.ClassA"); System.out.println("class:" + clazz.hashCode()); Object object = clazz.newInstance(); System.out.println(object); MyTest16 loader2 = new MyTest16("loader2"); loader2.setPath("C:\\Users\\wangj\\Desktop\\"); Class<?> clazz2 = loader2.loadClass("com.google.it.ClassA"); System.out.println("class2:" + clazz2.hashCode()); Object object2 = clazz2.newInstance(); System.out.println(object2); } //output: class:1163157884 //com.google.it.ClassA@74a14482 //class2:1163157884 //com.google.it.ClassA@1540e19d //结论 系统类加载器只加载了一次ClassA //将out目录下ClassA.class删除 ,再次运行输出以下结果 findClass invoked: com.google.it.ClassA class loader name: loader1 class:356573597 com.google.it.ClassA@677327b6 findClass invoked: com.google.it.ClassA class loader name: loader2 class2:1836019240 com.google.it.ClassA@135fbaa4 //两次类加载器都加载了ClaasA,但是返回的Class对象不一样 //原因两个类加载器命名空间不一样

自定义类加载器的使用场景

  1. 隔离加载类。 在某些框架内进行中间件与应用的模块隔离,把类加载到不同的环境。 比如, 阿里内某容器框架通过自定义类加载器确保应用中依赖的jar包不会影响到中间件运行时使用的jar包。
  1. 修改类加载方式: 类的加载模型并非强制, 除Bootstrap外, 其他的加载并非一定要引入, 或者根据实际情况在某个时间点进行按需进行动态加载。
  1. 扩展加载源: 比如从数据库、 网络, 甚至是电视机机顶盒进行加载.
  1. 防止源码泄漏: Java代码容易被编译和篡改, 可以进行编译加密。 那么类加载器也需要自定义, 还原加密的字节码。

线程上下文类加载器

当前类加载器(Current Classloader)

  • 每个类都会使用自己的类加载器(即加载自身的类加载器)来去加载其他类(指的是所依赖的类)
  • 如果ClassX引用了ClassY,那么ClassX的类加载器就会去加载ClassY(前提是ClassY尚未被加载)

线程上下文类加载器(Context ClassLoader)

  • 线程上下文类加载器是从JDK1.2开始引入的,类Tread中的getContextLoader()setContextClassLoader(ClassLoader cl)分别用来获取和设置上下文类加载器
  • 如果没有通过setContextClassLoader(ClassLoader cl)进行设置的话,线程将继承上下文类加载器.
  • java应用运行时的初始线程的上下文类加载器是系统类加载器,在线程中运行的代码可以通过该类加载器类与资源
public class MyTest25 implements Runnable { private Thread thread; public MyTest25() { this.thread = new Thread(this); thread.start(); } @Override public void run() { ClassLoader classLoader = this.thread.getContextClassLoader(); this.thread.setContextClassLoader(classLoader); System.out.println("Class: "+classLoader.getClass()); System.out.println("Parent: "+classLoader.getParent().getClass()); } public static void main(String[] args) { new MyTest25(); } } //输出 Class: class sun.misc.Launcher$AppClassLoader Parent: class sun.misc.Launcher$ExtClassLoader

线程上下文加载器的重要性

SPI(Service Provider Interface)关系中存在的问题

JDBC驱动加载
Connection类在java系统路径是由根类加载器加载,右边是由厂商提供的驱动jar包在classpath下无法由根类加载器加载
ClassLoader可以使用当前线程Thread.currentThread().getContextLoader()classloader加载的类,
这就改变了父ClassLoader不能使用子ClassLoader或者其他没有直接父子关系的ClassLoader加载的类的情况,即改变了双亲委托模型
public class MyTest26 { public static void main(String[] args) { ServiceLoader<Driver> loader= ServiceLoader.load(Driver.class); Iterator<Driver> iterator = loader.iterator(); while (iterator.hasNext()){ Driver driver = iterator.next(); System.out.println("driver: "+ driver.getClass()+", loader: "+driver.getClass().getClassLoader()); } System.out.println("当前线程上下文类加载器: "+Thread.currentThread().getContextClassLoader()); System.out.println("ServiceLoader的类加载器: "+ServiceLoader.class.getClassLoader()); } } //输出 driver: class com.mysql.jdbc.Driver, loader: sun.misc.Launcher$AppClassLoader@18b4aac2 driver: class com.mysql.fabric.jdbc.FabricMySQLDriver, loader: sun.misc.Launcher$AppClassLoader@18b4aac2 当前线程上下文类加载器: sun.misc.Launcher$AppClassLoader@18b4aac2 ServiceLoader的类加载器: null
线程上下文类加载器就是当前线程的Current Classloader.
在双亲委托模型下,类加载是由下至上的,即下层的类加载器会委托上层进行类加载,但是对于SPI来说,有些接口是java核心类库提供的,而java核心库是由启动类加载器来加载的,
而这些接口的实现却来自于不同的jar包(厂商提供),java的启动类加载器是不会加载其他来源的jar包.
因此传统的双亲委派模型无法满足SPI的要求,而通过给当前线程设置上下文类加载器,就可以由设置的上下文类加载器来实现接口实现类的加载

打破传统类加载器模式

 
其他打破传统类加载器模型的案例:tomcat的类加载器, spring 类加载器

线程上下文类加载器一般使用模式

获取=>使用=>还原
ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); try{ Thread.currentThread().setContextClassLoader(targetc1); myMethod(); }finally { Thread.currentThread().setContextClassLoader(classLoader); } //myMethod里面调用了Thread.currentThread().getContextClassLoader(),获取当前线程的上下文类加载器做某些事情 //如果一个类由类加载器A加载,那么这个类的依赖类也是相同的类加载器加载的(如果依赖类之前没有被加载过的话) //ContextClassLoader的作用就是为了破坏java的类加载委托机制 //当高层提供了统一的接口让底层去实现,同时又要在高层加载(或实例化)低层的类时,就必须要通过线程上下文类加载器来帮助高层的ClassLoader找到并加载该类

类加载器案例

tomcat的类加载器

在Tomcat目录结构中,有四组目录(/common/,/server//shared/Webapp/WEB-INF/*)可以存放java类库
  1. 放置在/common/目录:类库可被tomcat和所有Web应用程序共同使用.
  1. 放置在/server目录中,类库可被Tomcat使用,对所有的Web应用程序都不可见
  1. 放置在/shared目录中:类库可被所有的web应用程序共同使用,但是对Tomcat自己不可见
  1. 放置在Webapp/WEB-INF/*目录:类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见
为了实现这套目录结构,并对目录里面的类库进行加载和隔离,Tomcat自定义了多个类加载器,这些类加载器按照经典的双亲委派模型来实现.
CommonCiassLoader 、 CatalinaClassLoader、 SharedClassLoader 和WebappClassLoader 则是 Tomcat 自己定义的类加载器, 它们分别加载 /common/*/server//shared/ /WebApp/WEB-fNF /*中 Java 类库的逻辑。
 
如果有10个Web应用程序都是用Spring来进行组织和管理的话, 可以把Spring放到Shared目录下让这些程序共享。
Spring要对用户程序的类进行管理, 自然要 能访问到用户程序的类, 而用户的程序显然是放在 /WebApp/WEB-INF目录中的。 那么被CommonClassLoader或SharedClassLoader加载的Spring如何访问并不在其加载范围内的用户程序呢?