【JVM学习笔记】类加载器
概述
类加载器用来把类加载到Java虚拟机中。从JDK1.2版本开始,类的加载过程采用父委托机制,这种机制能更好地保证Java平台的安全。在此委托机制中,除了Java虚拟机自带的根类加载器以外,其余的类加载器都有且只有一个父加载器。当Java程序请求加载器loader1加载sample类时,loader1首先委托自己的父加载器去加载Sample类,若父加载器能加载,则由父加载器完成加载任务,否则才由加载器loader1本身加载sample类。
加载器的分类
Java虚拟机自带了以下几种加载器:
- 根(Bootstrap)类加载器:该加载器没有父加载器。它负责加载虚拟机的核心类库,如java.lang.*等。例如java.lang.Object就是由根类加载器加载的。根类加载器从系统属性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类的子类
除了以上虚拟机自带的加载器外,用户还可以定制自己的类加载器。Java提供了抽象类java.lang.ClassLoader,所有用户自定义的类加载器都应该继承ClassLoader类。
(扩展类加载器和系统类加载器都是由启动类加载器加载的)
类加载器的父委托机制
在父委托机制中,各个加载器按照父子关系形成了树形结构,除了根类加载器之外,其余的类加载器都有且只有一个父加载器(题外话,双亲委托机制是hotspot默认提供的一种机制,但并不是所有环境中都遵循这种机制,OSGI就打破了这种机制)
如果有一个类加载器能够成功加载某个类,那么这个类加载器被称为定义类加载器,所有能成功返回Class对象引用的类加载器(包括定义类加载器)都被称为初始类加载器
获得ClassLoader的途径
获得当前类的ClassLoader
clazz.getClassLoader();
获得当前线程上下文的ClassLoader
Thread.currentThread().getContextClassLoader()
获得系统的ClassLoader
ClassLoader.getSystemClassLoader()
获得调用者的ClassLoader(这种用的比较少)
DriverManager.getCallerClassLoader()
数组类的类对象不是由类加载器创建的,而是根据Java运行时的需要自动创建的。 Class.getClassLoader()返回的数组类的类加载器与其元素类型的类加载器相同; 如果元素类型是基本类型,则数组类没有类加载器。
public class ClassLoaderLearn { public static void main(String[] args) throws ClassNotFoundException { String[] strings = new String[2]; System.out.println(strings.getClass().getClassLoader()); ClassLoaderLearn[] array = new ClassLoaderLearn[5]; System.out.println(array.getClass().getClassLoader()); int[] nums = new int[5]; System.out.println(nums.getClass().getClassLoader()); } }
运行结果为
null sun.misc.Launcher$AppClassLoader@18b4aac2 null
入门示例
package com.learn.jvm.loader; import java.io.*; /** * @author wx * @Description * @date 2019/09/01 11:51 * 实际上我们自定义的这个ClassLoader只重写了findclass方法 */ public class ClassLoaderLearn extends ClassLoader { private String classLoaderName; // classloader的名称 private String path; // 加载路径 private String fileName; // 文件名 public void setPath(String path) { this.path = path; } public void setFileName(String fileName) { this.fileName = fileName; } public ClassLoaderLearn(String classLoaderName) { // 这句话可以省略,因为默认就会去调用父类的无参构造方法 // 在这里写出来就是为了强调它的作用,super()这个方法会去创建一个新的ClassLoader,其父加载器为SystemClassLoader super(); this.classLoaderName = classLoaderName; } public ClassLoaderLearn(ClassLoader parent, String classLoaderName) { // super(parent)会创建一个新的类加载器,这个新的类加载器的父加载器为parent super(parent); this.classLoaderName = classLoaderName; } @Override public String toString() { return "ClassLoaderLearn{" + "classLoaderName='" + classLoaderName + '\'' + '}'; } // 对父类中findClass方法doc文档的翻译: // 查找具有指定二进制名称的类。 此方法应由遵循委托模型的类加载器实现覆盖以加载类,并且在检查所请求类的父类加载器之后将由loadClass方法调用。 默认实现抛出ClassNotFoundException @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); } // className 是要加载的类的binaryname private byte[] loadClassData(String className) { InputStream is = null; byte[] data = null; ByteArrayOutputStream baos = null; try { is = new FileInputStream(new File(path+fileName)); baos = new ByteArrayOutputStream(); int ch; while (-1 != (ch = is.read())) { baos.write(ch); } data = baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally { try { is.close(); baos.close(); } catch (IOException ex) { ex.printStackTrace(); } } return data; } public static void main(String[] args) throws Exception { ClassLoaderLearn loader = new ClassLoaderLearn("MyClassLoader"); // loader.setPath("D:\\workspace-learn\\common-learn\\learn-jvm\\target\\classes"); loader.setPath("D:/"); loader.setFileName("Test.class"); // loadClass是ClassLoader提供的方法 // 深入源代码的doc文档,可以看到,loadClass方法按照如下顺序寻找类 // 1.调用findLoadedClass(String)方法检查这个类是否已经被加载过了 // 2.调用父加载器的loadClass方法,如果父加载器为null,就会使用根类加载器 // 3.调用findClass(String)方法来寻找这个类 Class<?> clazz = loader.loadClass("com.learn.jvm.loader.Test"); System.out.println("class: " + clazz); Object obj = clazz.newInstance(); System.out.println("obj:" + obj); System.out.println(obj.getClass().getClassLoader()); } }
上述代码中的Test类,是项目路径中的一个类。如果不删除生成的class文件则Test将会由根类加载器加载,加载的系统路径下的Test.class
如果删除项目路径下的Test.class文件,将其放到D盘,运行上诉程序,则Test类将由我们自定义的类加载器加载,如下面输出结果:
findClass invoked: com.learn.jvm.loader.Test class loader name: MyClassLoader class: class com.learn.jvm.loader.Test obj:com.learn.jvm.loader.Test@14ae5a5 ClassLoaderLearn{classLoaderName='MyClassLoader'}
命名空间
- 每个类加载器都有自己的命名空间,命名空间由该加载器及所有父加载器所加载的类组成
- 在同一个命名空间中,不会出现类的完整名字(包括类的包名)相同的两个类
- 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类
- (因此,子加载器所加载的类能够访问父加载器所加载的类,但父加载器加载的类无法访问到子加载器所加载的类)
(感觉OSGI就是这样的)
以下代码,在项目路径下有Test.class和项目路径没有Test.class这两种情况下,输出结果是不同的
public class ClassLoaderLearn extends ClassLoader { private String classLoaderName; // classloader的名称 private String path; // 加载路径 private String fileName; // 文件名 public void setPath(String path) { this.path = path; } public void setFileName(String fileName) { this.fileName = fileName; } public ClassLoaderLearn(String classLoaderName) { // 这句话可以省略,因为默认就会去调用父类的无参构造方法 // 在这里写出来就是为了强调它的作用,super()这个方法会去创建一个新的ClassLoader,其父加载器为SystemClassLoader super(); this.classLoaderName = classLoaderName; } public ClassLoaderLearn(ClassLoader parent, String classLoaderName) { // super(parent)会创建一个新的类加载器,这个新的类加载器的父加载器为parent super(parent); this.classLoaderName = classLoaderName; } @Override public String toString() { return "ClassLoaderLearn{" + "classLoaderName='" + classLoaderName + '\'' + '}'; } // 对父类中findClass方法doc文档的翻译: // 查找具有指定二进制名称的类。 此方法应由遵循委托模型的类加载器实现覆盖以加载类,并且在检查所请求类的父类加载器之后将由loadClass方法调用。 默认实现抛出ClassNotFoundException @Override protected Class<?> findClass(String className) throws ClassNotFoundException { byte[] data = this.loadClassData(className); return this.defineClass(className, data, 0, data.length); } // className 是要加载的类的binaryname private byte[] loadClassData(String className) { InputStream is = null; byte[] data = null; ByteArrayOutputStream baos = null; try { is = new FileInputStream(new File(path+fileName)); baos = new ByteArrayOutputStream(); int ch; while (-1 != (ch = is.read())) { baos.write(ch); } data = baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally { try { is.close(); baos.close(); } catch (IOException ex) { ex.printStackTrace(); } } return data; } public static void main(String[] args) throws Exception { ClassLoaderLearn loader1 = new ClassLoaderLearn("loader1"); loader1.setPath("D:/"); loader1.setFileName("Test.class"); Class<?> clazz1 = loader1.loadClass("com.learn.jvm.loader.Test"); System.out.println("class hashcode: " + clazz1.hashCode()); Object obj1 = clazz1.newInstance(); System.out.println("obj:" + obj1); System.out.println("classloader: " + obj1.getClass().getClassLoader()); System.out.println("=========================="); ClassLoaderLearn loader2 = new ClassLoaderLearn("loader2"); loader2.setPath("D:/"); loader2.setFileName("Test.class"); Class<?> clazz2 = loader2.loadClass("com.learn.jvm.loader.Test"); System.out.println("class hashcode: " + clazz2.hashCode()); Object obj2 = clazz2.newInstance(); System.out.println("obj:" + obj2); System.out.println("classloader: " + obj2.getClass().getClassLoader()); } }
输出结果(项目路径下有Test.class)
class hashcode: 356573597 obj:com.learn.jvm.loader.Test@677327b6 classloader: sun.misc.Launcher$AppClassLoader@18b4aac2 ========================== class hashcode: 356573597 obj:com.learn.jvm.loader.Test@14ae5a5 classloader: sun.misc.Launcher$AppClassLoader@18b4aac2
输出结果(项目路径下没有Test.class)
class hashcode: 21685669 obj:com.learn.jvm.loader.Test@7f31245a classloader: ClassLoaderLearn{classLoaderName='loader1'} ========================== class hashcode: 1173230247 obj:com.learn.jvm.loader.Test@330bedb4 classloader: ClassLoaderLearn{classLoaderName='loader2'}
这是为什么呢,为什么Test类被加载了两次。这就涉及到类加载器的命名空间问题,由于loader1和loader2在双亲委托机制中没有委托关系,所以是属于两个不同的命名空间
类的卸载
- 当一个类被加载、连接和初始化后,它的生命周期就开始了。当这个类的Class对象不再被引用,即不可抵达时,Class对象就会结束生命周期,这个类在方法区的数据也会被卸载,从而结束这个类的生命周期
- 一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期
由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。前面已经介绍过,Java虚拟机自带的类加载器包括根类加载器、扩展类加载器和系统类加载器。Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的
由用户自定义的类加载器所加载的类是可以被卸载的
在类加载器的内部实现中,用一个Java集合来存放所加载类的引用。另一方面,一个Class对象总是会引用它的类加载器,调用Class对象的getClassLoader()方法,就能获得它的类加载器。由此可见,类的Class对象与类加载器之间为双向关联关系。
public class ClassLoaderLearn extends ClassLoader { private String classLoaderName; // classloader的名称 private String path; // 加载路径 private String fileName; // 文件名 public void setPath(String path) { this.path = path; } public void setFileName(String fileName) { this.fileName = fileName; } public ClassLoaderLearn(String classLoaderName) { // 这句话可以省略,因为默认就会去调用父类的无参构造方法 // 在这里写出来就是为了强调它的作用,super()这个方法会去创建一个新的ClassLoader,其父加载器为SystemClassLoader super(); this.classLoaderName = classLoaderName; } public ClassLoaderLearn(ClassLoader parent, String classLoaderName) { // super(parent)会创建一个新的类加载器,这个新的类加载器的父加载器为parent super(parent); this.classLoaderName = classLoaderName; } @Override public String toString() { return "ClassLoaderLearn{" + "classLoaderName='" + classLoaderName + '\'' + '}'; } // 对父类中findClass方法doc文档的翻译: // 查找具有指定二进制名称的类。 此方法应由遵循委托模型的类加载器实现覆盖以加载类,并且在检查所请求类的父类加载器之后将由loadClass方法调用。 默认实现抛出ClassNotFoundException @Override protected Class<?> findClass(String className) throws ClassNotFoundException { byte[] data = this.loadClassData(className); return this.defineClass(className, data, 0, data.length); } // className 是要加载的类的binaryname private byte[] loadClassData(String className) { InputStream is = null; byte[] data = null; ByteArrayOutputStream baos = null; try { is = new FileInputStream(new File(path+fileName)); baos = new ByteArrayOutputStream(); int ch; while (-1 != (ch = is.read())) { baos.write(ch); } data = baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally { try { is.close(); baos.close(); } catch (IOException ex) { ex.printStackTrace(); } } return data; } public static void main(String[] args) throws Exception { ClassLoaderLearn loader1 = new ClassLoaderLearn("loader1"); loader1.setPath("D:/"); loader1.setFileName("Test.class"); Class<?> clazz1 = loader1.loadClass("com.learn.jvm.loader.Test"); System.out.println("class hashcode: " + clazz1.hashCode()); Object obj1 = clazz1.newInstance(); System.out.println("obj:" + obj1); System.out.println("classloader: " + obj1.getClass().getClassLoader()); System.out.println("=========================="); loader1 = null; obj1 = null; clazz1 = null; System.gc(); Thread.sleep(200000); loader1 = new ClassLoaderLearn("loader1"); loader1.setPath("D:/"); loader1.setFileName("Test.class"); clazz1 = loader1.loadClass("com.learn.jvm.loader.Test"); System.out.println("class hashcode: " + clazz1.hashCode()); obj1 = clazz1.newInstance(); System.out.println("obj:" + obj1); System.out.println("classloader: " + obj1.getClass().getClassLoader()); } }
输出结果(注意,项目路径下已经删掉了Test.class)
class hashcode: 21685669 obj:com.learn.jvm.loader.Test@7f31245a classloader: ClassLoaderLearn{classLoaderName='loader1'} ========================== [Unloading class com.learn.jvm.loader.Test 0x00000007c0061828] class hashcode: 1173230247 obj:com.learn.jvm.loader.Test@330bedb4 classloader: ClassLoaderLearn{classLoaderName='loader1'}
使用jvisualvm看到的结果如下
如果A类的构造函数里创建了B类对象,那么B类和A类在加载的时候,是被同一个加载器加载
定义一个MyCat类
public class MyCat { public MyCat() { System.out.println("MyCat is loaded by " + this.getClass().getClassLoader()); } }
定义一个MySample类
public class MySample { public MySample() { System.out.println("MySample is loaded by: " + this.getClass().getClassLoader()); new MyCat(); } }
然后是我们的类加载器和入口函数
public class ClassLoaderLearn extends ClassLoader { private String classLoaderName; // classloader的名称 private String path; // 加载路径 private String fileName; // 文件名 public void setPath(String path) { this.path = path; } public void setFileName(String fileName) { this.fileName = fileName; } public ClassLoaderLearn(String classLoaderName) { // 这句话可以省略,因为默认就会去调用父类的无参构造方法 // 在这里写出来就是为了强调它的作用,super()这个方法会去创建一个新的ClassLoader,其父加载器为SystemClassLoader super(); this.classLoaderName = classLoaderName; } public ClassLoaderLearn(ClassLoader parent, String classLoaderName) { // super(parent)会创建一个新的类加载器,这个新的类加载器的父加载器为parent super(parent); this.classLoaderName = classLoaderName; } @Override public String toString() { return "ClassLoaderLearn{" + "classLoaderName='" + classLoaderName + '\'' + '}'; } // 对父类中findClass方法doc文档的翻译: // 查找具有指定二进制名称的类。 此方法应由遵循委托模型的类加载器实现覆盖以加载类,并且在检查所请求类的父类加载器之后将由loadClass方法调用。 默认实现抛出ClassNotFoundException @Override protected Class<?> findClass(String className) throws ClassNotFoundException { byte[] data = this.loadClassData(className); return this.defineClass(className, data, 0, data.length); } // className 是要加载的类的binaryname private byte[] loadClassData(String className) { InputStream is = null; byte[] data = null; ByteArrayOutputStream baos = null; try { is = new FileInputStream(new File(path+fileName)); baos = new ByteArrayOutputStream(); int ch; while (-1 != (ch = is.read())) { baos.write(ch); } data = baos.toByteArray(); } catch (Exception e) { e.printStackTrace(); } finally { try { is.close(); baos.close(); } catch (IOException ex) { ex.printStackTrace(); } } return data; } public static void main(String[] args) throws Exception { ClassLoaderLearn loader1 = new ClassLoaderLearn("loader1"); loader1.setPath("D:/"); loader1.setFileName("MySample.class"); Class<?> clazz1 = loader1.loadClass("com.learn.jvm.loader.MySample"); System.out.println("class hashcode: " + clazz1.hashCode());
loader1.setFileName("MyCat.class"); Object obj1 = clazz1.newInstance(); } }
运行结果(未删除项目路径下MyCat和MySample的情况)
class hashcode: 356573597 MySample is loaded by: sun.misc.Launcher$AppClassLoader@18b4aac2 MyCat is loaded by sun.misc.Launcher$AppClassLoader@18b4aac2
运行结果(已删除项目路径下MyCat和MySample的情况,将MyCat和MySample放在了D盘 )
class hashcode: 21685669 MySample is loaded by: ClassLoaderLearn{classLoaderName='loader1'} MyCat is loaded by ClassLoaderLearn{classLoaderName='loader1'}
如果在项目路径下只有MySample.class,没有MyCat.class
而在D盘两者都有,运行以上程序,输出结果为
class hashcode: 356573597 MySample is loaded by: sun.misc.Launcher$AppClassLoader@18b4aac2 Exception in thread "main" java.lang.NoClassDefFoundError: com/learn/jvm/loader/MyCat at com.learn.jvm.loader.MySample.<init>(MySample.java:11) 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.learn.jvm.loader.ClassLoaderLearn.main(ClassLoaderLearn.java:95) Caused by: java.lang.ClassNotFoundException: com.learn.jvm.loader.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:335) at java.lang.ClassLoader.loadClass(ClassLoader.java:357) ... 7 more
这是因为我们定义的类加载器,没有指定其父加载器,默认父加载器为系统类加载器。系统类加载器能够在classpath下面找到MySample类,但是接着却找不到MyCat类了
又一个例子,保持ClassLoaderLearn 类不变,新增一个Person类如下
public class Person { private Person myPerson; public void setMyPerson(Object obj) { this.myPerson = (Person)obj; } }
修改main函数如下
public static void main(String[] args) throws Exception { ClassLoaderLearn loader1 = new ClassLoaderLearn("loader1"); ClassLoaderLearn loader2 = new ClassLoaderLearn("loader2"); loader1.setPath("D:/"); loader1.setFileName("Person.class"); loader2.setPath("D:/"); loader2.setFileName("Person.class"); Class<?> clazz1 = loader1.loadClass("com.learn.jvm.loader.Person"); Class<?> clazz2 = loader2.loadClass("com.learn.jvm.loader.Person"); Object obj1 = clazz1.newInstance(); Object obj2 = clazz2.newInstance(); Method method = clazz1.getMethod("setMyPerson", Object.class); method.invoke(obj1,obj2); }
输出结果(项目路径未删除Person.class)
(无输出,程序正常结束)
输出结果(项目路径已删除Person.class并放到了D盘)
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.learn.jvm.loader.ClassLoaderLearn.main(ClassLoaderLearn.java:100)
Caused by: java.lang.ClassCastException: com.learn.jvm.loader.Person cannot be cast to com.learn.jvm.loader.Person
at com.learn.jvm.loader.Person.setMyPerson(Person.java:12)
... 5 more
这是因为这两个类是由两个不同命名空间里的loader加载的,不是同一个类对象