1.类加载器概述
java类的加载由虚拟机来完成的,虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化最终形成可以被虚拟机直接说那个的java类型,这就是虚拟机的类加载机制。
2.类的生命周期
3.类加载器分类
Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:
1.引导类加载器(Bootstrap ClassLoader):它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader。负责加载 JAVA_HOME\lib 目录中的类
2.扩展类加载器(Extension ClassLoader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。负责加载 JAVA_HOME\lib\ext 目录中的
3.系统类加载器(System ClassLoader):它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。
4.java.lang.ClassLoader类:
上面除了引导类加载器,其他的加载器都继承ClassLoader类
作用:
1.基本职责负责就是根据一个指定的类的名称,找到或者生成其对应的字节代码,然后从这些字节代码中定义出一个 Java 类,即 java.lang.Class类的一个实例
2.ClassLoader还负责加载 Java 应用所需的资源,如图像文件和配置文件等
相关方法:
getParent() 返回该类加载器的父类加载器。
loadClass(String name) 加载名称为 name的类,返回的结果是 java.lang.Class类的实例。
findClass(String name) 查找名称为 name的类,返回的结果是 java.lang.Class类的实例。
findLoadedClass(String name) 查找名称为 name的已经被加载过的类,返回的结果是 java.lang.Class类的实例。
defineClass(String name, byte[] b, int off, int len) 把字节数组 b中的内容转换成 Java 类,返回的结果是 java.lang.Class类的实例。这个方法被声明为 final的。
resolveClass(Class<?> c) 链接指定的 Java 类。
注释:
每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 getClassLoader()方法就可以获取到此引用,也可以通过递归调用getParent()方法来输出全部的父类加载器
5.类加载器的代理模式(双亲委派模式):
工作原理:
如果一个类收到了加载类的请求,它并不会自己先去加载,而是把这个请求委托给父类加载器去执行,如果父类加载器还存在父类加载器,则进一步向上委托,依次类推。
请求最终到达顶层的启动类加载器,如果父类加载器可以完成类加载任务,就返回成功。
若父类加载器无法完成加载任务,子加载器才会尝试自己去加载。
优点:
保证类java核心库的类型安全。所有java应用都至少需要引用java.lang.Object类,也就是说在运行的时候,java.lang.Object这个类需要被加载到java虚拟机中。
若这个加载过程由自己的类加载器加载的话,就很可能存在多个版本的java.lang.Object类,而这些类之间是不兼容的。但是通过代理模式,对于java核心库的类加载工作由引导类加载器统一完成,保证了java应用所使用的都是同一个版本的java核心库的类,是相互兼容的。
不同的类加载器为相同名称的类创建了额外的名称空间。
相同名称的类可以并存在 Java 虚拟机中,只需要用不同的类加载器来加载它们即可。不同类加载器加载的类之间是不兼容的,这就相当于在 Java 虚拟机内部创建了一个个相互隔离的 Java 类空间
需要说明的是:
Java 虚拟机是如何判定两个 Java 类是相同的。
Java 虚拟机不仅要看类的全名是否相同,还要看加载此类的类加载器是否一样。只有两者都相同的情况,才认为两个类是相同的。
即便是同样的字节代码,被不同的类加载器加载之后所得到的类,也是不同的。
6.线程上下文类加载器:
线程上下文类加载器(Context ClassLoader)
类 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。
上述的类加载器不能解决java应用开发的全部问题,比如java提供了很多服务提供者接口(SPI),允许第三方提供接口实现,最常见的是JDBC,java.sql.Driver这些接口由java核心库提供
所以这些类由启动类加载器加载,但是这些接口的实现通过jar包引入到项目,由系统类加载器加载。引导类加载器无法找到SPI的实现类,也不能代理给系统类加载器。这也是代理模式无法解决的问题。
但是线程上下文类加载器正好解决了这个问题。若不做任何设置,java应用的线程的上下文类加载器默认就是系统上下文类加载器。在SPI接口的代码中使用上下文类加载器就可以成功的加载到SPI接口的实现类。
7.测试案例
本案例主要测试,使用不同的类加载器加载相同类的.class文件,并得到两个java.lang.Class类的实例,但是这两个实例是不相同的。
自定义类加载器:
public class MyClassLoader2 extends ClassLoader {
private String rootDir;
public MyClassLoader2(String path) {
this.rootDir = path;
}
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
byte[] classData = getClassData(name);
if (classData.length == 0) {
return super.findClass(name);
} else {
return defineClass(name, classData, 0, classData.length);
}
}
private byte[] getClassData(String className) {
ByteArrayOutputStream baos = new ByteArrayOutputStream();
String path = classNameToPath(className);
try {
InputStream is = new FileInputStream(path);
byte[] bytes = new byte[1024];
int len = 0;
while ((len = is.read(bytes)) != -1) {
baos.write(bytes, 0, len);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
baos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
return baos.toByteArray();
}
private String classNameToPath(String className) {
// File.separatorChar 与系统有关的默认名称分隔符
return rootDir + File.separatorChar + className.replace('.', File.separatorChar) + ".class";
}
}
加载流程:
一般来说,写自己的类加载器只需要重写父类的findClass(String name)方法即可,因为loadClass(String name)方法中封装了双亲委派模式的实现,该方法中①首先就会调用findLoadedClass(String name)方法,检查这个类是否被加载过,②若被加载过,直接返回该类的字节码对象,③若没有加载过,会先调用父类的loadClass()方法,尝试加载该类,④如果父类无法加载,就调用findClass(String name)查找该类。因此为了正常的类加载流程,最好重写findClass(String name)方法
测试代码及结果:
这里需要注意的是,如果在IDEA里面直接创建Student类的话,不会出现这种结果,因为系统类加载器会加载到Student而不是自己的类加载器去加载。所以这里我把Student类放在了非classpath目录下:
这里还需要注意一个问题就是,不能再IDEA创建好之后,在从其classpath下拿出.class文件直接放在这个目录下,因为.java文件在编译成.class之后类的描述信息已经存在.class文件中了,比如包名,如果直接copy到这个目录下,会出现类名称错误的异常。