前言
- 前面说过一个类只会被加载一次。
- 但是,这是在同一个命名空间下的类加载器的情况下,一个类只会被加载一次。
命名空间
- 每个类加载器都有自己的命名空间,命名空间由加载该类的加载器及所有父加载器所加载的类组成。
- 在同一个命名空间中,不会出现类的完整名字(包括类的包名) 相同的两个类。
- 在不同的命名空间中,有可能会出现类的完整名字(包括类的包名)相同的两个类。
样例
样例接着上一篇博客
- 这时,自定义类加载器的第二个构造方法就有用了,
public UserClassLoader(ClassLoader parent, String classLoaderName) { super(parent);//显式定义该类加载器的父加载器 this.classLoaderName = classLoaderName; } - 还是在默认路径下删除要加载的.class文件,然后用自定义类加载去加载指定路径是上的类。
public static void main(String[] args) throws Exception { UserClassLoader userClassLoader = new UserClassLoader("userClassLoader"); userClassLoader.setPath("C:/Users/25852/Desktop/"); Class<?> clazz = userClassLoader.loadClass("com.jvmstudy.classloading.ClassLoaderTest");//不能是当前类 Object object = clazz.newInstance(); System.out.println("class HashCode: "+clazz.hashCode()); System.out.println(object.getClass().getClassLoader()); System.out.println("-----------------------"); //以userClassLoader为父加载器 UserClassLoader userClassLoader1 = new UserClassLoader(userClassLoader,"userClassLoader1"); userClassLoader1.setPath("C:/Users/25852/Desktop/"); Class<?> clazz1 = userClassLoader1.loadClass("com.jvmstudy.classloading.ClassLoaderTest");//不能是当前类 Object object1 = clazz1.newInstance(); System.out.println("class HashCode: "+clazz1.hashCode()); System.out.println(object1.getClass().getClassLoader()); System.out.println("-----------------------"); UserClassLoader userClassLoader2 = new UserClassLoader("userClassLoader2"); userClassLoader2.setPath("C:/Users/25852/Desktop/"); Class<?> clazz2 = userClassLoader2.loadClass("com.jvmstudy.classloading.ClassLoaderTest");//不能是当前类 Object object2 = clazz2.newInstance(); System.out.println("class HashCode: "+clazz2.hashCode()); System.out.println(object2.getClass().getClassLoader()); } - 结果
- 说明
- 首先自定义类加载的父加载器一定有AppClassLoader,虽然构造方法可以指定父加载器。就像自定义类加载器的父父加载器就会是AppClassLoader。
- 因为默认路径的类是由AppClassLoader加载,因为已经删掉了对应的class文件,显然它是无法加载的, 根据双亲委托机制,委托一遍下来,都加载不了,就由自定义类加载器自己加载了。
- 上面的代码,userClassLoader1的父类加载是userClassLoader,根据双亲委托机制,类就由userClassLoader加载,看看命名空间的定义,前面两个类加载器的命名空间一样,都是以userClassLoader为准,所以相同类就只加载一次。
- 再看userClassLoader2,它的直接父类加载器就是AppClassLoader,根据双亲委托机制,其父类加载器加载不了,就由userClassLoader2自己加载,此时命名空间以userClassLoader2为准。所以它加载类和前面两个不一样,也就是说相同类被重新加载了一次。
类的卸载
定义
- 当某个类被加载、连接和初始化后,它的生命周期就开始了。
- 当代表某个类的Class对象不再被引用,即不可触及(没有引用指向)时,Class对象就会结束生命周期,某个类在方法区内的数据也会被卸载,从而结束MySample类的生命周期。
- 一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。
- 由Java虚拟机自带的类加载器所加载的类,在虚拟机的生命周期中,始终不会被卸载。前面已经介绍过,Java虚拟机自带的类加载器包括根类加载器、扩展类加载器和系统类加载器。Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。
- 由用户自定义的类加载器所加载的类是可以被卸载的。
- 运行程序时,某个类由loader加载。在类加载器的内部实现中,用一个Java集合来存放所加载类的引用。 另一方面,一个Class对象总是会引用它的类加载器, 调用Class对象的getClassLoader()方法,就能获得它的类加载器。由此可见,代表某个类的Class实例与loader之间为双向关联关系。
- 一个类的实例总是引用代表这个类的Class对象。 在 Object类中定义了getClass()方法,这个方法返回代表对象所属类的Class对象的引用。此外,所有的Java类都有一个静态属性class,它引用代表这个类的Class对象。
用代码演示类的卸载
- 要使用JVM参数
-XX:+TraceClassUnloading来打印类的卸载消息 - 还是在默认路径下删除要加载的.class文件的情况下,代码如下
public static void main(String[] args) throws Exception { UserClassLoader userClassLoader = new UserClassLoader("userClassLoader"); userClassLoader.setPath("C:/Users/25852/Desktop/"); Class<?> clazz = userClassLoader.loadClass("com.jvmstudy.classloading.ClassLoaderTest");//不能是当前类 Object object = clazz.newInstance(); System.out.println("class HashCode: "+clazz.hashCode()); System.out.println(object.getClass().getClassLoader()); System.out.println("-----------------------"); //让类和自定义类加载器不再互相引用 userClassLoader = null; clazz = null; object = null; System.gc();//实际场景不这么用 userClassLoader = new UserClassLoader("userClassLoader"); userClassLoader.setPath("C:/Users/25852/Desktop/"); clazz = userClassLoader.loadClass("com.jvmstudy.classloading.ClassLoaderTest");//不能是当前类 object = clazz.newInstance(); System.out.println("class HashCode: "+clazz.hashCode()); System.out.println(object.getClass().getClassLoader()); } - 结果
关于命名空间的重要说明
- 子加载器所加载的类能够访问父加载器所加载的类。
- 父加载器所加载的类无法访问到子加载器所加载的类。
- 在运行期,一个Java类是由该类的完全限定名(binary name,二进制名)和用于加载该类的定义类加载器(defining loader)所共同决定的。如果同样名字((即相同的完全限定名)厂的类是由两个不同的加载器所加载,那么这些类就是不同的,即便.class文件的字节码完全一样,并且从相同的位置加载亦如此。
不同类加载器的命名空间关系
- 同一个命名空间内的类是相互可见的。只是可见,但不一定能互相访问,能不能访问由修饰符决定,比如private public 等。
- 子加载器的命名空间包含所有父加载器的命名空间。因此由子加载器加载的类能看见父加载器加载的类。例如系统类加载器加载的类能看见根类加载器加载的类。
- 由父加载器加载的类不能看见子加载器加载的类。
- 如果两个加载器之间没有直接或间接的父子关系,那么它们各自加载的类相互不可见。
类加载器的双亲委托模型的好处:
- 可以确保Java核心库的类型安全:所有的Java应用都至少会引用java.lang.Object类,也就是说在运行期,java,lang.Object这个类会被加载到Java虚拟机中,如果这个加载过程是由Java应用自己的类加载器所完成的,那么很可能就会在JVM中存在多个版本的Java.lang.0bject类,而且这些类之间还是不兼容的,相互不可见的(正是命名空间在发挥着作用)。
- 借助于双亲委托机制,Java核心类库中的类的加载工作都是由启动类加载器来统一完成,从而确保了Java应用所使用的都是同一个版本的Java核心类库,他们之间是相互兼容的。
- 避免类的重复加载:同一个类可能被多个不同的类加载器加载,每个加载器都会创建该类的独立实例,导致类的多重定义和不一致性。
- 可以确保Java核心类库所提供的类不会被自定义的类所替代。
- 不同的类加载器可以为相同名称(binary name)的类创建额外的命名空间。相同名称的类可以并存在Java虚拟机中,只需要用不同的类加载器来加载他们即可。不同类加载器所加载的类之间是不兼容的,这就相当于在Java虚拟机内部创建了一个又一个相互隔离的Java类空间,这类技术在很多框架中都得到了实际应用。