在Java类生命周期中,只有加载步骤中的读取二进制流与初始化部分,能够被上层开发者,也就是大部分的Java程序员控制。而剩下的所有步骤,都是由JVM掌控,其中细节由JVM的开发人员处理,对上层开发者来说是个黑盒。
为什么要这么做呢?
这是一种面向对象中开闭原则和封装思想的设计。JVM 将类加载内部复杂的实现封装了起来,拒绝上层开发者修改,只提供了一个扩展接口,用于class文件二进制流的读取。而就是这么一点点的活动空间,也成了上层开发者施展拳脚的舞台,比如说基于该特性实现的 动态代理、热部署等等功能。
Java的类加载器
类加载器的分类属于JVM规范,是一种抽象的概念,各个不同的JVM实现的方式是不一定一样的,JVM规范中类加载器分为两大类
- 启动类加载器
- 非启动类加载器
以HotSpot虚拟机实现为例
在HotSpot中,Bootstrap ClassLoader是采用C/C++来实现的,是嵌套在了JVM内部,无法作为对象来被程序所引用,它主要用来加载Java的核心类库,比如说 <JAVA_HOME>/lib 路径下的jar包,或者是由启动参数来指定路径下的核心类库,此外,为了安全性Bootstrap ClassLoader只加载了包名在白名单中的文件,比如说由java,javax,sun开头的包,非Bootstrap ClassLoader又分为三类。分别为:
- Extension ClassLoader
- Application ClassLoader
- User ClassLoader
在 HotSpot中,非Bootstrap ClassLoader都采用java来实现,他们都继承自 java.lang.ClassLoader 这个类,他们可以作为对象被引用,那么接下来我们来介绍这三种类加载器的区别:
- Extension ClassLoader 是由Launcher 的内部类ExtClassLoader来实现的,主要来加载<JAVA_HOME>/lib/ext 目录下,或者是由系统变量指定的路径中的类库。Extension ClassLoader希望加载的是javaApi的扩展,是对java类库的一些补充能力。
- Application ClassLoader 是由Launcher 的内部类AppClassLoader来实现的,主要来加载环境变脸classpath或者是系统属性指定路径下的类库,Application ClassLoader 希望加载的是上层程序员编写的代码以及一些第三方的类库,我们平时编写的代码基本上都是由Application ClassLoader 来加载的。
问题:我们可不可以用Extension ClassLoader 来加载我们自己写的代码呢?
可以这么做,但是完全没有必要,这种操作不符合规范,工程项目是讲究分层和抽象的,就好像你可以把一个项目的代码都写在一个文件里,但是是不推荐这么做的。
- User ClassLoader 用户自己编写的类加载器,上面提及到的几种类加载器,都只能从本地文件中获取字节码来进行加载。而User ClassLoader可以让用户能够获取任何来源的字节码,并对他们进行加载,这就印证了在java类加载机制中允许用户从各个渠道获取class文件的二进制流来进行加载的结论。如果要编写一个User ClassLoader,其实非常简单,我们前面说到在HotSpot中,非Bootstrap ClassLoader都是采用java来实现的,他们都继承自java.lang.ClassLoader这个类,用户自定义的类加载器,其实只需要继承java.lang.ClassLoader ,然后单独实现获取二进制流的逻辑,而后续的步骤必须让java.lang.ClassLoader作为内置的逻辑来处理,用户无权进行重写和干涉。
问题:
- 不同的类加载器,除了读取二进制流的动作和范围不一样,后续的加载器逻辑是否也不一样?
- 遇到限定名一样的类,这么多类加载器会不会产生混乱?
JVM规范中,规定:每个类加载器都有属于自己的命名空间
即使你用不同的类加载器加载了同一个限定名的类,那么JVM也会认为这是两个不同的类,深入理解JVM这本书中,有这样的一个例子:
public class Main {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
ClassLoader myLoader = new 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[] b = new byte[is.available()];
is.read(b);
return defineClass(name, b, 0, b.length);
} catch (IOException e) {
e.printStackTrace();
throw new ClassNotFoundException(name);
}
}
};
Object obj = myLoader.loadClass("com.example.demo0413.test.A").newInstance();
System.out.println(obj.getClass());
System.out.println(obj instanceof A);
}
}
可以看到我们首先使用匿名类的方式实现了一个User ClassLoader ,我们之前所说的,在HotSpot中所有非Bootstrap ClassLoader 都是java.lang.ClassLoader的子类,那么我们这里就重写了它的loadClass 方法,loadClass方法的内容呢就是直接从文件中读取class文件,然后调用父类的defineClass来进行后续的加载操作,最后使用加载输出的 class 来进行对象的实例化,并且对产生的对象进行类型判断,我们可以看到对象的类限定名是一样的,但是instanceof 的结果却输出了false,看上去很奇怪,这是因为我们自定义的这个类分别被默认的Application ClassLoader 和我们自定义的User ClassLoader加载了,在最后的判断中,JVM认为这是两个完全不同的Class,这就印证了我们上面提到的每个类加载器都属于自己的命名空间这一点。