类加载器

基础知识介绍

类加载器,就是把类加载到JVM到一个模块。一般来说JVM利用类加载器读取一个字节码文件将其转换为java.lang.Class对象。然后可以通过Class的newInstance方法生成一个类的实例。

首先声明一个类加载器

public class MyClassLoader extends ClassLoader {

    @Override
    public Class loadClass(String name) throws ClassNotFoundException {
        try {
            String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class";
            InputStream is = getClass().getResourceAsStream(fileName);
            if (is == null) {
                return super.loadClass(name);
            }
            byte[] bytes = new byte[is.available()];
            is.read(bytes);
            return defineClass(name, bytes, 0, bytes.length);
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

}

类的唯一性

用类加载器加载同一个类,判断一下类对象

    public static void main(String[] args) throws IllegalAccessException, InstantiationException,
        ClassNotFoundException {
        ClassLoader classLoader = new MyClassLoader();

        Class myLoaderClass = classLoader.loadClass(ClassLoaderTest.class.getName());
        System.out.println(myLoaderClass.getName());

        Object o = myLoaderClass.newInstance();
        System.out.println(o instanceof ClassLoaderTest);

        ClassLoaderTest a = (ClassLoaderTest)o;
    }
类加载器_第1张图片
image.png

可以看到的是 instance of 判断是false,因为已经不是同一个类了。后面的错误信息比较有趣

Exception in thread "main" java.lang.ClassCastException: com.funnycode.demo.springboot.env.classloader.ClassLoaderTest cannot be cast to com.funnycode.demo.springboot.env.classloader.ClassLoaderTest
    at com.funnycode.demo.springboot.env.classloader.ClassLoaderTest.main(ClassLoaderTest.java:19)

如果你不看上面代码,光看这个错误是比较奇怪的,相当于告诉你我不是我。下面说明下原因,强转的 ClassLoaderTest 是由系统的类去加载到JVM的,而 myLoaderClass 这个是通过自定义类加载加载的,显示和JVM加载的是不同的,两个类虽然来自于同一个class文件,但确是两个独立的类。

类名和类加载器共同确定了类的唯一性,它会影响包括Class对象的equals()、isAssignableFrom()、isInstance()方法,及我们常用的类判断 instanceof 关键字的使用。

类加载的时机

Java语言有一个特性,就是类的加载、连接和初始化是在程序运行期间完成的,并不是在编译期完成的。所以一个类的加载时期就需要被定义出来,但是虚拟机规范里并没有规定虚拟机在什么时间加载类,而是把这个交给虚拟机自己来决定,不过虚拟机规范严格地规定了几种情况必须立即对类进行初始化(这其中就包括类加载)

  • 遇到new、getstatic、putstatic或invokestatic这4条字节码时,如果没有初始化类,就立即初始化。对应的场景也就是使用new关键字实例化对象,读取或设置一个类的静态字段的时候,以及调用一个类的静态方法时。
  • 使用反射包java.lang.reflect对类进行反射调用时
  • 当类初始化,发现它的父类没有初始化,则需要先触发其父类的初始化
  • 当虚拟机启动时,main()方法所在的那个类
    从上面的描述可以看出来,类加载器是有一个缓存的,它能记录自己有没有加载过某一个类,对于同一个类,类加载器只会加载一次。
    下面有这么一段代码,如果用一个类加载器去加载B这个类的话,会去加载几个类?
    public static void main(String[] args) throws ClassNotFoundException {
        ClassLoader classLoader = new MyClassLoader();
        Class aClass = classLoader.loadClass(A.class.getName());

         A a = new A();
    }

加上VM参数-verbose:class
参考自https://docs.oracle.com/javase/8/docs/technotes/tools/windows/java.html

image.png

输出内容如下

[Loaded java.lang.Object from /Library/Java/JavaVirtualMachines/jdk1.8.0_151.jdk/Contents/Home/jre/lib/rt.jar]

···

[Loaded com.funnycode.demo.springboot.env.classloader.B from file:/Users/tc/Documents/workspace/gitee/springboot-env/java-base/target/classes/]
[Loaded com.funnycode.demo.springboot.env.classloader.A from file:/Users/tc/Documents/workspace/gitee/springboot-env/java-base/target/classes/]

...

[Loaded com.funnycode.demo.springboot.env.classloader.B from __JVM_DefineClass__]
[Loaded com.funnycode.demo.springboot.env.classloader.A from __JVM_DefineClass__]

从日志看到,Object是先加载的,这是因为main启动获取加载/jre/lib/rt.jar的类。我们用自定义类加载器和new一个对象都会出现加载类B然后会加载类A,结合上面的规定,就可以很好的理解了,加载类B的时候,发现父类没加载就会加载父类,最终类本身和所有父类的加载。这里因为main启动,所以java.lang.Object类先被加载了,如果当时没有加载rt.jar下的类,作为父类,那么Object类会在A加载后加载。

类的同源加载

对于一个类A中引入其它类B(可能是用来定义对象或声明方法的返回值),如果由于A触发B的加载流程,那么B将使用A的类加载器来加载。

下面是例子:
准备三个类 Factory、Trunk、Goods

public class Goods {
}

public class Factory {

    public Goods productGoods() {
        return new Goods();
    }

}

public class Trunk {

    private Goods goods;

    public void setGoods(Goods goods) {
        this.goods = goods;
    }

}

下面使用三行代码来定义一个我们程序中常用的流程,Factory提供一个工具方法productGoods生成Goods对象,Trunk有一个方法使用Goods对象作为入参,完成一个类似于Spring设值注入的流程。

        Factory factory = new Factory();
        Trunk trunk = new Trunk();
        trunk.setGoods(factory.productGoods());

这段代码编译和运行起来一定没有问题,这是基于一个前提,我们的运行环境中的Factory、Trunk都使用同一个类加载器加载到JVM中,Goods的加载按照加载时机来讲,应该是在factory.productGoods()new Goods()时被加载的。试想一下,如果Factory和Trunk是不同的类加载器加载的的,那么trunk.setGoods(factory.productGoods())这行代码还能正常运行吗?
我们再试下面的代码

        // 自定义类加载器1
        ClassLoader myClassLoaderOne = new MyClassLoader();
        // 用类加载器1加载Factory类
        Class factoryClass = myClassLoaderOne.loadClass(Factory.class.getName());
        Object f = factoryClass.newInstance();

        Method productGoods = factoryClass.getMethod("productGoods");
        // 反射调用productGoods
        Object goodsOne = productGoods.invoke(f);

        // 自定义类加载器2
        ClassLoader myClassLoaderTwo = new MyClassLoader();
        // 用类加载器2加载Trunk和Goods类
        Class trunkClass = myClassLoaderTwo.loadClass(Trunk.class.getName());
        Class goodsClass = myClassLoaderTwo.loadClass(Goods.class.getName());

        Object t = trunkClass.newInstance();

        Method setGoods = trunkClass.getMethod("setGoods", goodsClass);

        // goodsOne是类加载器1的,会报错
        setGoods.invoke(t, goodsOne);

        Object g = goodsClass.newInstance();
        // 反射调用,类加载器2加载
        setGoods.invoke(t, g);

会出现异常


类加载器_第2张图片
image.png

类加载的资料,未完待续
https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html?ca=drs-cn-0301

你可能感兴趣的:(类加载器)