Loading

【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加载的,不是同一个类对象

posted @ 2019-08-31 11:58  注销111  阅读(242)  评论(0编辑  收藏  举报