类的加载
一个类从被加载到虚拟机内存到卸载出内存,它整个的生命周期为:
其中连接
又分为 验证、准备和解析
其中的加载过程就需要使用到类加载器
加载阶段需要完成三件事情:
- 通过一个类的全限定名来获取定义此类的
二进制字节流
- 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
- 在内存中生成一个代表这个类的 java.lang.Class 对象,作为方法区这个类的各种数据的访问入口
ClassLoader就来做这三件事
类加载器
类加载器分成两类:启动类加载器
和 用户自定义的类加载器
启动类加载器有三个: 启动类加载器(Bootstrap Class Loader)
、扩展类加载器(Extension Class Loader)
和 应用程序类加载器(Application Class Loader)
- 启动类加载器主要加载 <JAVA_HOME>\lib 下的Java类
- 扩展类加载器主要负责加载 <JAVA_HOME>\lib\ext 目录中的类
- 应用程序类加载器负责加载用户类路径(
ClassPath
)上的所有类库。
双亲委派模型:
上面所说的几种类加载器是有层次关系的:
这种层次关系也被称为 双亲委派模型
工作机制:
双亲委派模型的工作过程是 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
这句话在ClassLoader 源码中也有体现:
在Classloader.loadClass(String name, boolean resolve)
方法的注释上有这么一句话:
The default implementation of this method searches for classes in the following order:
*
* <ol>
*
* <li><p> Invoke {@link #findLoadedClass(String)} to check if the class has already been loaded. </p></li>
*
* <li><p> Invoke the {@link #loadClass(String) loadClass} method on the parent class loader. If the parent is {@code null} the class loader built into the virtual machine is used, instead. </p></li>
*
* <li><p> Invoke the {@link #findClass(String)} method to find the class. </p></li>
*
* </ol>
- 首先检查该类是否已经被加载
- 然后交给父加载器,如果父加载器为null,那么就使用虚拟机创建的类加载器
- 最后自己加载
注意: 有的问题说 父加载器找不到是返回null还是什么,我们观察源码可以发现:
catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
可以发现,如果父加载器找不到的话是直接抛出异常的
类加载器使用的策略
类加载器的一个重要用途是在JVM中为相同名称的Java类创建隔离空间。在JVM中,判断两个类是否相同,不仅是根据该类的全限定类名 ,还需要根据两个类的定义类加载器。只有两者完全一样,才认为两个类是相同的。因此,即便是同样的Java字节代码,被两个不同的类加载器定义之后,所得到的Java类也是不同的。如果试图在两个类的对象之间进行赋值操作,会抛出 java.lang.ClassCastException。
代码分析:
运行如下代码:
其中Person类可以自己定义
public class first {
public static void main(String[] args) throws Exception{
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[] bytes = new byte[is.available()];
is.read(bytes);
return defineClass(name, bytes, 0, bytes.length);
} catch (Exception e) {
e.printStackTrace();
}
return super.loadClass(name);
}
};
Object o = myLoader.loadClass("com.example.demo4.Person").getConstructor().newInstance();
System.out.println(o.getClass());
System.out.println(o instanceof com.example.demo4.Person);
}
}
运行结果如下;
第二行在和 com.example.demo4.Person
进行所属类型检查时返回了false。这是因为前者是使用我们自定义的类加载器加载的,而后者是虚拟机的应用程序类加载器加载的,虽然 全限定类名都相同,都来自同一个Class文件,但是在虚拟机中仍然是两个独立的类,所以会判断false
自定义类加载器
我们上面自定义了自己的类加载器,但是只重写了loadClass()
这一个方法,如果需要自定义,需要着重了解这些方法:
- defineClass():这个方法用来完成从Java字节代码的字节数组到java.lang.Class的转换。这个方法是不能被覆写的,一般是用原生代码来实现的。
- findLoadedClass():这个方法用来根据名称查找已经加载过的Java类。一个类加载器不会重复加载同一名称的类。
- findClass():这个方法用来根据名称查找并加载Java类。
- loadClass():这个方法用来根据名称加载Java类。
- resolveClass():这个方法用来链接一个Java类。