Java中地所有类,必须被装载到JVM中才能运行,这个装载工作是由JVM中的类加载器完成的,类装载器所做的工作实质是把类文件从硬盘中取到内存中,JVM在加载类的时候,都是通过ClassLoader的loadClass()方法来加载class的,loadClass使用双亲委派模式,下面是过程流程图:
为了能更好的理解类的加载机制,我们来深入研究一下ClassLoader和他的loadClass()方法ClassLoader是一个负责加载classes的对象,ClassLoader类是一个抽象类,需要给出类的二进制名称,ClassLoader尝试定位或者产出一个class的数据,一个典型的策略是把二进制名字转换成文件名然后到文件系统中找到该文件。
该类主要由四个方法看,分别是defineClass、findClass、loadClass、resolveClass。
①protected final Class<?> defineClass(String name, byte[] b, int off, int len)
将byte字节流解析为JVM能够识别的Class对象,off和len参数表示实列Class信息在byte数组中的位置和长度,其中byte数组b是ClassLoader从外部获取的。这是受保护的方法,只有在自定义ClassLoader子类中可以使用。
②protected Class<?> findClass(String name) throws ClassNotFoundException
查找二进制名称为name的类,返回结果为java.lang.Class类的实例。这是一个受保护的方法,JVM鼓励我们重写此方法,需要自定义加载器遵循双亲委托机制,该方法会在检查完父类加载器之后被loadClass()方法调用。
③public Class<?>loadClass(String name)throws ClassNotFoundException
运行可以通过调用此方法加载一个名称为name的类,返回结果为java.lang.Class类的实列。如果找不到类,则返回ClassNotFoundException异常。该方法中的逻辑就是双亲委派模式的实现。
④protected final void resolveClass(Class<?> c)
链接指定的一个Java类。使用该方法可以使用类的Class对象创建完成的同时也被解析。前面我们说链接阶段主要是对字节码进行验证,为类变量分配内存并设置初始值同时将字节码文件中的符号引用转换为直接引用。
ClassLoader是调用其java.lang.ClassLoader#loadClass(String, boolean)方法来加载class的,loadClass核心代码如下:
String name要查找的类的二进制名,boolean serolve,一个标志,true表示将调用resolveClass解析该类,throws ClassNotFoundException 该方法会抛出找不到该类的异常,这是一个非运行时异常。
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// 先查一下该类是否已经记载过了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
//双亲委托机制,使用父加载器调用loadClass()方法
if (parent != null) {
c = parent.loadClass(name, false);
} else {
//如果parent为null,则用BootstartClassLoader进行加载
//如果parent属性为空,则可以理解为其父加载器为BootstartClassLoader
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// 如果没有找到类,则抛出ClassNotFoundException
// 从非空的父类装入器
}
if (c == null) {
//如果当前加载器的所有父加载器都加载失败了
//则叫由当前加载器重写findClass自定义加载逻辑
long t1 = System.nanoTime();
c = findClass(name);
// 这是定义类装入器;记录数据
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
//如果为true,则解析该类
if (resolve) {
resolveClass(c);
}
return c;
}
}
此parent为ClassLoader的一个成员属性,而非子父类继承关系。ClassLoader加载时,会优先尝试父加载器去加载(如果父加载器为null,则调用BootstrapClassLoader去加载),所有父加载器都尝试失败后才会交由当前 ClassLoader重写的findClass方法去加载。
首先我们会先看到synchronized (getClassLoadingLock(name)) 这行代码,这是一个同步代码块,来看一下这个方法的内部结构:
protected Object getClassLoadingLock(String className) {
Object lock = this;
//通过parallelLockMap判断当前的加载器是否具有并行能力
//parallelLockMap如果为null,没必要给他一个加锁对象直接返回this
if (parallelLockMap != null) {
Object newLock = new Object();
//根据传进来的className,检查该名字是否已经关联了一个value值,
// 如果已经关联过value值,那么直接把他关联的值返回,
lock = parallelLockMap.putIfAbsent(className, newLock);
if (lock == null) {
lock = newLock;
}
}
return lock;
}
以上是getClassLoadingLock(name)方法的实现细节,我们看到这里用到变量parallelLockMap ,根据这个变量的值进行不同的操作,如果这个变量是Null,那么直接返回this,如果这个属性不为Null,那么就新建一个对象,然后在调用一个putIfAbsent(className, newLock);方法来给刚刚创建好的对象赋值。
该类的作用:为类的加载操作返回一个锁对象。为了向后兼容,这个方法这样实现:如果当前的classloader对象注册了并行能力,方法返回一个与指定的名字className相关联的特定对象,否则,直接返回当前的ClassLoader对象。而变量parallelLockMap是在构造函数中根据一个属性ParallelLoaders的Registered状态的不同来给parallelLockMap 赋值。
类加载器的双亲委派机制
当一个类加载器收到一个类加载的请求,他首先会将该请求委派给父类加载器去加载,每一个层次的类加载器都是如此,因此所有的类加载器请求最终都应该被传入到顶层的启动类加载器(BootstrapClassLoader)中,只有当父类加载器反馈无法完成这个类的加载请求时(它的搜索范围内不存在这个类),父类加载器会委派子类加载器进行加载,若当前加载器发现没有子类,则开始进行类加载。整个过程就是从下到上询问,然后从上到下委派。