类加载器ClassLoader
前言
在我们日常开发中,应用遇到过 java.lang.ClassNotFoundException 这个异常,追溯的话就需要谈一谈类加载器。
类加载的过程
类加载包括3个阶段:加载、链接、初始化,其中链接又包括验证、准备和解析。
1、加载:查找定义类的二进制字节流,将其加载到虚拟中的方法区(类中静态存储结构转成方法区运行时数据结构),并生成一个Class对象供访问。这是 ClassLoader做的事,也叫类加载器。
2、验证:确保Class文件中的字节流包含的信息符合虚拟机规范,不会危害其安全。
3、准备:为类变量分配内存并设置类变量的初始值(初始值不是=号后面的值,而是如int是0,boolean是false),类变量的内存在方法区中配置。
4、解析:虚拟机将常量池内的符号引用替换成直接引用。
符号引用:虚拟机规范中的任何形式的字面量,在Class文件中的如CONSTANT_Class_info等
直接引用:对内存中的内容的一个指向,可以是指针、相对偏移量或间接定位到目标的句柄。
5、初始化:执行类构造器 <init>方法,完成类变量的赋值,和类静态代码块的执行。
补充下,类的生命周期:除了类加载的3个阶段,还有 使用和卸载两个阶段。
类加载器分类
上面提到的类加载的过程中,“加载”这个阶段是有类加载器完成的,这个类加载器是的设计是一项创新,允许用户自定义来决定类从哪里加载。
类加载器一共包括了:启动类加载器、扩展类加载器、应用类加载器。同时可以实现自定义类加载器,如下结构:
1、启动类加载器:
启动类加载器主要负责加载JDK内部类,通常是rt.jar包和$JAVA_HOME/jre/lib目录下的jar(如java.*开头),包括其它2个类加载器。
除了特定的目录,也可以指定环境变量-Xbootclasspath来使用启动类加载器加载。
启动程序类加载器是Java虚拟机的一部分,用本机代码编写(比如,HotSpot使用C++),不同的平台的实现可能有所不同。
2、扩展类加载器:
扩展类加载器是sun.misc.Launcher$ExtClassLoader实现的,继承于URLClassLoader,URLClassLoader又间接继承于CLassLoader。
它主要负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类)。
3、应用类加载器:
应用类加载器是sun.misc.Launcher$AppClassLoader实现的,同样继承于URLClassLoader。该加载器是ClassLoader类中getSystemClassLoader()方法返回的加载器。
它主要负责加载用户类路径(ClassPath)上所指定的类库或-Djava.class.path指定的类。
有个点需要注意:启动类不能被java应用直接引用,它是本地语言实现的,没有对象之类的获取,如果通过ExtClassLoader的parent获取的为null。
示例:
@Test
public void testClassLoad() {
// 系统类加载器(应用程序类加载器):sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(ClassLoadTest.class.getClassLoader());
// 扩展类加载器:sun.misc.Launcher$ExtClassLoader@50040f0c
System.out.println(Logger.class.getClassLoader());
// 启动类加载器:null(显示null)
System.out.println(ArrayList.class.getClassLoader());
// sun.misc.Launcher$AppClassLoader@18b4aac2
System.out.println(ClassLoader.getSystemClassLoader());
}
类加载机制
上面图中展示的类加载器的关系图,叫做类的“双亲委派”模型。
除了最顶层的启动类加载器,都有自己的父类加载器。这里面所谓的父类并不是父子,而是以一种组合的方式实现的。可以看到ClassLoader抽象类有个ClassLoader parent的成员变量。
所谓双亲委派模型意思是:
1、当应用程序类加载器(AppClassLoader)加载类时,会交由ExtClassLoader进行加载;
2、扩展类加载器ExtClassLoader不会自己加载,会交由BootstrapClassLoader进行加载;
3、如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
4、如果ExtClassLoader加载失败,会使用AppClassLoader进行加载,如果没找到对应的class,则会抛出ClassNotFoundException。
如果有自定义加载器是一样的,会先委派其父类加载器,一层层直到启动类加载器,都找不到,再有自己进行加载。
实现双亲委派的代码:
// java.lang.ClassLoader
// resolve参数是否进行类解析,默认不解析
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 首先,检测该类是不是已经当前类加载器加载到虚拟机中,它是一个native方法。
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
// 如果没有被加载,且存在父类加载器,则使用父类加载器进行加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 如果没有parent,则到顶层启动类加载器了,进行查询
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// findClass是个关键方法
// 如果父类加载器和启动类加载器都加载不到,则通过对应类加载器从ClassPath中找到对应的.class
long t1 = System.nanoTime();
c = findClass(name);
}
}
// 如果需要解析,再解析
if (resolve) {
resolveClass(c);
}
// 返回查找到的Class对象
return c;
}
}
注意:
1、loadClass方法最后使用的是findClass方法,通过ClassPath查找,AppClassLoader和ExtClassLoader都是继承了URLClassLoader的findClass方法。由此也可以知道,在自定义类加载器时,建议通过实现该方法完成类的查找加载,以便loadClass破坏了双亲委派的模型。
2、不同的ClassLoader加载的相同的字节码类,并不是相等的,Class对象不等。因为不同的类加载器会指定不同的名称空间。
双亲委派的优势:
1、防止系统中出现多份相同的字节码,例如Object类,不管哪个类加载器加载,都是同一个。
2、保证java程序安全稳定运行。
自定义类加载器
自定义类加载器,意思是我们程序自定义类加载器来加载Class类,使用的场景如本地磁盘的Class类或者网络中的文件。
实现自定义的类加载器需要以下几步:
1、继承java.lang.ClassLoader类;
2、重写其findClass方法;
3、在方法中,找到Class文件再调用defineClass方法将其加载到虚拟机中,获得Class对象;
实现自定义类加载器,不建议重写其loadClass方法,以免破坏掉双亲委派模型。
演示步骤:
1、首先再本地磁盘中创建一个测试类:Test.java
// pageage + 类名,组成加载的名称
package top.xudj;
public class Test {
public void say(){
System.out.println("Say Hello");
}
}
2、定义CustomClassLoader类,继承ClassLoader
public class CustomClassLoader extends ClassLoader {
private String path;
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
String fileName = getFileName(name);
File file = new File(path, fileName);
FileInputStream inputStream = null;
ByteArrayOutputStream bos = null;
try {
inputStream = new FileInputStream(file);
bos = new ByteArrayOutputStream();
int len;
while ((len = inputStream.read()) != -1) {
bos.write(len);
}
byte[] data = bos.toByteArray();
// 调用defineClass 生成Class对象
return defineClass(name, data, 0, data.length);
} catch (Exception e) {
e.printStackTrace();
} finally {
try {
if (inputStream != null) {
inputStream.close();
}
if (bos != null) {
bos.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return super.findClass(name);
}
/**
* 获取要加载的Class文件
*
* @param name:类全限定名
* @return
*/
private String getFileName(String name) {
int index = name.lastIndexOf('.');
if (index == -1) {
return name + ".class";
} else {
return name.substring(index + 1) + ".class";
}
}
}
3、使用自定义类加载器加载类
public static void main(String[] args) throws ClassNotFoundException {
// 自定义类加载器
CustomClassLoader customClassLoader = new CustomClassLoader("/Users/xudj");
// 查找要加载的Class文件;调用loadClass方法,双亲委派。
Class<?> clazz = customClassLoader.loadClass("top.xudj.Test");
// 创建对象
if (clazz != null) {
try {
Object instance = clazz.newInstance();
Method method = clazz.getDeclaredMethod("say", null);
// 调用方法,输出:Hello
method.invoke(instance);
} catch (InstantiationException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
} catch (NoSuchMethodException e) {
e.printStackTrace();
} catch (InvocationTargetException e) {
e.printStackTrace();
}
}
}
输出:
Hello
补充:
1、在自定义加载器加载Calss类时,使用loadClass方法进行加载,保证其双亲委派模型。
2、使用 Class.forName(Clazz.class.getName()); 进行加载类时,默认是使用当前调用类的加载器,不进行类初始化;可以指定是否初始化并指定类加载器进行查找。
总结
类加载器在类的生命周期中,负责将Class文件加载到内存中,并获得Class对象。加载遵循双亲委派模型,保证Java稳定运行。
当然也有些场景可能会破坏类加载器的双亲委派模型,例如线程上下文类加载的配置和使用,有可能会是父类加载器请求之类加载器进行加载,如JNDI场景。
不同的类加载器都有自己的加载类的范围,程序如果想加载磁盘或者网络中的类,如果使用启动或扩展类或应用程序类加载器加载会报java.lang.ClassNotFoundException,这个加载在某种意义上来说就是查找的意思。