类加载机制是指虚拟机将描述类的数据从class文件加载到内存中,并进行数据验证、解析、初始化等过程,最终形成可以直接被虚拟机使用的java类型,java语言中类的加载、链接、初始化等过程并不是在编译时期完成,而是在运行时期进行的
好处在于为语言提供了动态扩展的特性,坏处在于增加了性能的开销;
一、类加载过程
类在jvm中的整个生命周期包括:
加载loading--->验证verification-->准备preparation-->解析resolution-->初始化initialization-->使用using-->卸载unloading
其中验证、准备、解析并称为链接阶段,并且加载、验证、准备、解析、初始化阶段的开始时机是确定的,而解析过程则不一定了,因为Java语言可能会存在运行时动态解析;
1 加载loading
加载本质上就是通过二进制字节流生成class对象的过程,进一步可以分为如下步骤:
- 通过类的全限定名来获取定义该类二进制字节流;
- 将这个字节流所表示的静态存储结构转换为方法区的动态运行时数据结构;
- 在内存中实例化一个java.lang.Class对象,作为方法区中该类的数据访问入口;
在加载过程中的获取二进制字节流阶段既可以使用JDK提供了类加载器进行加载,也可以使用我们自定义的类加载器加载;
2 验证verification
为了保证Class文件的字节流中包含的信息符合虚拟机的要求,并且不会危害虚拟机的安全,我们必须要对数据进行验证,否则随意的数据可能导致虚拟机崩溃,在java虚拟机规范中对数据的约束和规范规则较多,大致可以分为4种:
- 文件格式验证
- 元数据验证
- 字节码验证
- 符号引用验证
3 准备preparation
准备阶段是正式为类变量分配内存和设置类变量初始值的阶段,这里分配内存的仅仅是类变量(static)而不包括实例变量。实例变量内存分配和赋初值是在对对象实例化阶段进行的;
4 解析resolution
解析是虚拟机将常量池中的符号引用替换为直接引用的过程,解析主要对类,接口,字段,类方法,接口方法,方法类型等;
5 初始化initialization
初始化实际就是对变量赋值(不是赋初值)的过程,包含所有类变量的赋值以及静态代码语句块的执行代码,包括对父类的初始化;
二、类加载器
1 类与类加载器
通过类的全限定名来获取该类的二进制字节码流”,这一过程的实现就是通过类加载器完成的;
对于任意的一个类,都需要由该类本身和加载该类的类加载器来确定其在Java虚拟机中的唯一性,每一个类加载器都有一个独立的类名称空间如果要判断两个类是否一样,不能仅仅比较两个类是否通过同一个Class文件生成的,假如两个类通过同一个Class文件生成但是各自加载他们的类加载器不一样,那么这两个类也是不相等的,在使用equals方法和instanceof关键字等时都遵循这个原则;
2 几种类加载器
- BootStrap ClassLoader(启动类加载器):该类加载器负责加载<JAVA_HOME>/lib目录和由-Xbootclasspath参数指定的路径下的类库,且必须是虚拟机内定义好的名字的类库
- Extension ClassLoader(扩展类加载器):该类加载器负责加载<JAVA_HOME>/lib/ext目录下或者由java.ext.dirs变量指定的路径下的类库,该类加载器我们可以直接使用
- Application ClassLoader(应用类加载器):该类加载器负责加载用户路径(ClassPath)下的类库,也是默认的类加载器
此外,我们也可以继承ClassLoader类并重写findClass方法进行自定义类加载器;
以下图示类加载器的加载流程:
三、双亲委派模型(Parents Delegation Model)
1 双亲委派机制原理
双亲委派模型是如果一个类加载器收到类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成,每个类加载器都是如此,只有当父加载器在自己的搜索范围内找不到指定的类时,子加载器才会尝试自己去加载;
2 双亲委派机制处理流程
JDK中默认的双亲委派处理流程是怎么的呢?以下是java.lang.ClassLoader.loadClass()
方法的实现
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException{
synchronized (getClassLoadingLock(name)) {
Class<?> c = findLoadedClass(name);
//如果该类没有被加载则通过类加载器加载
//已经被加载就返回
if (c == null) {
long t0 = System.nanoTime();
try {
//判断父加载器是否为空 为空表示使用BootStrap ClassLoader加载
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
//父加载器无法加载该类 下面有当前类加载器自己加载
}
if (c == null) {
long t1 = System.nanoTime();
c = findClass(name);
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
转成流程图,即是:
从代码可以看出,总是先会尝试让父类加载器先加载,其次判断启动类加载器是否已经加载了,最后才尝试从当前类加载器加载,转换为更清晰的模型如下: