类加载器深入详解
类加载器
JAVA虚拟机自身提供的加载器
启动类加载器(Bootstrap ClassLoader)
主要负责加载 ${JAVA_HOME}/lib 目录下的类库
扩展类加载器(Extension ClassLoader)
主要负责加载 ${JAVA_HOME}/lib/ext 目录下的类库
应用程序类加载器(Application ClassLoader)
它主要负责加载用户类路径(classpath)上指定的类库
举个列:
package test.jvm;
/**
* @author long
* @Desc
* @date 2018/10/15 下午9:14
*/
public class ClassLoaderTest {
private String getClassPath() {
return this.getClass().getResource("/").getPath();
}
public static void main(String[] args) throws InterruptedException {
ClassLoaderTest classLoaderTest = new ClassLoaderTest();
System.out.println(classLoaderTest.getClassPath() );
}
}
输出:/Users/hejianglong/study/bingfabiancheng/out/production/bingfabiancheng/
意思AppClassLoader会去加载我创建在 study目录下的 bingfabiancheng这个项目的所有的自定义类。
用户自定义的加载器
什么时候需要自定义加载器呢?
比如当我们的class文件存在远程服务器上,并且将访问地址存入了数据库,当我们从数据库获取对应的class地址的时候,比如 http://www.ab.com/static/Test.class ,AppClassLoader也无法加载,这个时候就需要我们来实现自定义加载器来进行加载了,本文暂时不讲自定义加载器,改天我会单独写一篇文章来进行描述。
双亲委派模型
上面的内容我们了解到了有哪些类加载器,分别用于加载哪一块的内容,那么它是如何实现类加载的呢,就是通过双亲委派模型算法。
先来看一张图大致理解一下
主要思路
-
当类加载器收到类加载请求的时候,发现它还有父类加载器就委派给父类加载器,依次往上,这样的话最顶层肯定的是BootstrapClassLoader进行类加载
-
由于BootstrapClassLoader只加载${JAVA_HOME}/lib下面的类库,其它的它加载不了比如test.jvm.ClassLoaderTest 它就返回给ExtClassLoader给他说我加载不了,交给你了
-
由于ExtClassLoader只负责加载${JAVA_HOME}/lib/ext 目录,它也加载不了,就返回给Application ClassLoader
-
因为Application ClassLoader负责加载用户类路径(classpath)下的类库,他能加载成功
-
此处未涉及到自定义的类加载器,自定义加载器在AppClassLoader加载不了的时候使用
判断一个对象是否相等必须在同一个类加载器下才有意义,如果在不同的类加载器下他们肯定不相等。
通过上面几点就能保证一个类在类加载过程中只会被加载一次,以及它的唯一性
源码分析
loadClass(String name, boolean resolve)
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// 同步加载class
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
// 检查类是否已经被加载过了
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
// 启动类加载器进行加载
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
// 无法被启动类加载器加载的交给findClass
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;
}
}
看了这段代码结合上面的文字描述相信大家基本上都能明白双亲委派模型的大致实现思路
下面我从源码上来分析几个关键的方法
1. getClassLoadingLock(String name)
2. findClass(String name);
3. resolveClass(Class c);
getClassLoadingLock(String name)
将锁对象放入parallelLockMap中,放入成功则返回 lock = null,如果之前已经存则返回已经被加载的对象
那么此处put失败,返回之前的锁对象,这样就保证了如果是并行加载那么会对一个对象进行加锁同步
/**
* Returns the lock object for class loading operations.
* For backward compatibility, the default implementation of this method
* behaves as follows. If this ClassLoader object is registered as
* parallel capable, the method returns a dedicated object associated
* with the specified class name. Otherwise, the method returns this
* ClassLoader object.
* 主要意思,如果是单个类加载就返回锁对象, 如果是并行的加载就返回类加载的对象
*/
protected Object getClassLoadingLock(String className) {
Object lock = this;
if (parallelLockMap != null) {
// 获取一个锁对象
Object newLock = new Object();
// 将锁对象放入parallelLockMap中,放入成功则返回 lock = null,如果之前已经存则返回已经被加载的对象
// 那么此处put失败,返回之前的锁对象,这样就保证了如果是并行加载那么会对一个对象进行加锁同步
lock = parallelLockMap.putIfAbsent(className, newLock);
if (lock == null) {
lock = newLock;
}
}
return lock;
}
findClass(String name)
protected Class<?> findClass(String name) throws ClassNotFoundException {
throw new ClassNotFoundException(name);
}
这里它有非常多的classLoader实现,实际上加载用户类路径类库调用的是
URLClassLoader -> findClass
protected Class<?> findClass(final String name)
throws ClassNotFoundException
{
final Class<?> result;
try {
result = AccessController.doPrivileged(
new PrivilegedExceptionAction<Class<?>>() {
public Class<?> run() throws ClassNotFoundException {
String path = name.replace('.', '/').concat(".class");
Resource res = ucp.getResource(path, false);
if (res != null) {
try {
return defineClass(name, res);
} catch (IOException e) {
throw new ClassNotFoundException(name, e);
}
} else {
return null;
}
}
}, acc);
} catch (java.security.PrivilegedActionException pae) {
throw (ClassNotFoundException) pae.getException();
}
if (result == null) {
throw new ClassNotFoundException(name);
}
return result;
}
这里主要是defineClass(name, res)这行代码
它将根据类的全路径进行读取,将其加载到方法区,并生成一个以class对象入口,用来访问其类信息, 此处需要主要的是,这时类似不能被正常使用,因为还没有进行连接,初始化等过程可能还会存在错误。
resolveClass(Class c)
相信大家进行断点 ClassLoader的loadClass(String name, boolean resolve)的时候也注意到了
这里的 resolve默认为false的。
那么也就意味着不会执行以下代码
if (resolve) {
resolveClass(c);
}
那么它到底是用来做什么的呢,我们来看看他的源码及其描述
/**
* Links the specified class. This (misleadingly named) method may be
* used by a class loader to link a class. If the class <tt>c</tt> has
* already been linked, then this method simply returns. Otherwise, the
* class is linked as described in the "Execution" chapter of
* <cite>The Java™ Language Specification</cite>.
*/
protected final void resolveClass(Class<?> c) {
// 这是一个native方法
resolveClass0(c);
}
大致意思就是,这个方法目的是进行加载步骤的 “连接”(linking),如果已经连接过了那么他就直接返回,否则就会执行一遍连接过程。
如果不了解类加载,连接这些的可以看下这篇文章 JAVA虚拟类加载过程
我们知道linking(连接)包含 验证,准备,解析(可能会延迟解析) 然后还要进行初始化后才能正常使用
意思就是loadClass(name, false) 返回的类,只是已经被正确加载的类,还他不一定正确,因为还没有连接过。
为什么要这么做呢?个人猜测 这就相当于根据 resolve字段为true或者false选择延迟链接,好处是什么呢?
如果为false, 可以提升我们应用的启动速度算是其中一点把,因为少了链接这个步骤。但是应该会在初次使用类的时候速度回降低因为还需要进行连接
这里我们来看一下<<JAVA虚拟机规范8>>关于何时进行连接的描述
- 在类或接口被连接前,他必须被成功的加载过
- 在类或接口初始化前,他必须被成功的验证及准备过
- 若程序执行了某种可能需要直接或间接连接一个类或接口的动作,而连接类或接口的过程中又检测到了错误,则错误的抛出点应该是执行动作的那个点
由于笔者技术水平有限,如果有错误欢迎大家在评论中指正,谢谢。