基础知识介绍
类加载器,就是把类加载到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;
}
可以看到的是 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
输出内容如下
[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);
会出现异常
类加载的资料,未完待续
https://www.ibm.com/developerworks/cn/java/j-lo-classloader/index.html?ca=drs-cn-0301