一、类加载器概述
在开发中会遇到 java.lang.ClassNotFoundException 和 java.lang.NoClassDefError,想要更好解决这类问题,或者在一些特殊的应用场景,比如需要支持类的动态加载或需要对编译后的字节码文件进行加密解密操作,那么需要你自定义类加载器,因此了解类加载器及其加载机制成为了Java开发必备技能之一。
二、四种类加载器
1、引导类加载器(Bootstrap Classloader),又称为根类加载器
它负责加载 Java 的核心库(JAVA_HOME/jre/lib/rt.jar 等或 sun.boot.class.path 路径下的内容),是用原生代码(C/C++)来实现的,并不继承自 java.lang.ClassLoader,所以通过 Java 代码获取引导类加载器对象将会得到 null。(只有核心类库如 String 才使用 引导类加载器)
2、扩展类加载器(Extension Classloader)
它由 sun.misc.Launcher$ExtClassLoader 实现,是 java.lang.ClassLoader 的子类,负责加载 Java 的扩展库(JAVA_HOME/jre/ext/*.jar或java.ext.dirs路径下的内容)
3、应用程序类加载器(Application Classloader)
它由 sun.misc.Lanuncher$AppClassLoader 实现,是 java.lang.ClassLoader 的子类,负责加载 Java 应用程序类路径(classpath、java.class.path)下的内容。(通俗的讲:项目的路径bin文件夹下的字节码,以及如果你配置了环境变量classpath)
4、自定义类加载器
开发人员可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求,例如对字节码进行加密来避免class文件被反编译,或者加载特殊目录下的字节码数据。
小结:类加载器是用来完成类加载的。
三、经典委托模式
1、Java 中类加载器的双亲委托模式
类加载器负责加载所有的类,系统为所有被载入内存中的类生成一个java.lang.Class实例。一旦一个类被载入JVM中,同一个类就不会被再次载入了。
那么,怎么样算是“同一个类”呢?
在JVM中,一个类用其全限定类名和其类加载器作为其唯一标识。换句话说,同一个类如果用两个类加载器分别加载,JVM将视为“不同的类”,它们互不兼容。
那么,我们的类加载器在执行类加载任务的时候,如何确保一个类的全局唯一性呢?
Java虚拟机的设计者们通过一种称之为“双亲委派模型(Parent Delegation Model)”的委派机制来约定类加载器的加载机制。
按照双亲委派模型的规则,除了引导类加载器之外,程序中的每一个类加载器都应该拥有一个超类加载器,比如:ExtClassLoader的超类加载器是引导类加载器,而AppClassLoader的超类加载器是ExtClassLoader,而自定义类加载器的超类就是AppClassLoader。
那么当一个类加载器接收到一个类加载任务的时候,它并不会立即展开加载,先检测此类是否加载过,即在方法区寻找该类对应的Class对象是否存在,如果存在就是已经加载过了,直接返回该Class对象,否则会将加载任务委派给它的超类加载器去执行,每一层的类加载器都采用相同的方式,直至委派给最顶层的启动类加载器为止,如果超类加载器无法加载委派给它的类时,便会将类的加载任务退回给它的下一级类加载器去执行加载,如果所有的类加载器都加载失败,就会报java.lang.ClassNotFoundException或java.lang.NoClassDefFoundError。
在此大家需要注意,由于Java虚拟机规范并没有要求类加载器的加载机制一定要使用双亲委托模式,只是建议采用这种方式而已。比如在Tomcat中,类加载器所采用的加载机制就和传统的双亲委派模型有一定区别,当缺省的类加载器就接收到一个类的加载任务时,首先会由它自行加载,当它加载失败时,才会将类的加载任务委派给它的超类加载器去执行,这同时也是Servlet规范推荐的一种做法。
说明:
数组类型本身并不是由类加载器负责创建,而是由JVM在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建。因此,JVM会把数组元素类型的类加载器记录为数组类型的类加载器。
2、双亲委托模式目的是什么?
目的:为了安全,而且各司其职,保证核心类库的安全性
当我们自己声明一个 java.lang.String 类,类加载器会为我们加载自定义的String类还是系统的String类呢?
当应用程序类加载器接到加载某个类的任务时,例如:java.lang.String。
(1)会现在内存中,搜索这个类是否加载过了,如果是,就返回这个类的Class对象,不去加载。
(2)如果没有找到,即没有加载过。会把这个任务先提交给“父加载器”
当扩展类加载器接到加载某个类的任务时,例如:java.lang.String。
(1)会现在内存中,搜索这个类是否加载过了,如果是,就返回这个类的Class对象,不去加载。
(2)如果没有找到,即没有加载过。会把这个任务先提交给“父加载器”
当引导类加载器接到加载某个类的任务时,例如:java.lang.String。
(1)会现在内存中,搜索这个类是否加载过了,如果是,就返回这个类的Class对象,不去加载。
(2)如果没有找到,即没有加载过。会在它的负责的范围内尝试加载。
如果可以找到,那么就返回这个类的Class对象。就结束了。
如果没有找到,那么会把这个任务往回传,让“子加载器”扩展类加载器去加载。
“子加载器”扩展类加载器接到“父加载器”返回的任务后,去它负责的范围内加载。
如果可以找到,那么就返回这个类的Class对象。就结束了。
如果没有找到,那么会把这个任务往回传,让“子加载器”应用程序类加载器去加载。
“子加载器”应用程序类加载器接到“父加载器”返回的任务后,去它负责的范围内加载。
如果可以找到,那么就返回这个类的Class对象。就结束了。
如果没有找到,那么就报错ClassNotFoundException或java.lang.NoClassDefError
四、类加载器的作用
1、本质工作
类加载器的本质工作就是用于加载类
2、加载文件
类加载器还可以用来加载“类路径下”的资源文件。