JVM技术专题针对于加载器与双亲委派机制分析和研究指南 「 入门篇」
Posted 浩宇の天尚
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JVM技术专题针对于加载器与双亲委派机制分析和研究指南 「 入门篇」相关的知识,希望对你有一定的参考价值。
任何足够先进的科技,都与魔法无异
加载器与双亲委派机制
- 类加载器是怎么被创建出来的?
- 什么是双亲委派机制?为什么要有这种机制?
- Class实例和类加载器究竟是在Java Heap中,还是在方法区中?
类加载器: 可以实现通过一个类的全限定名称来获取描述此类的二进制字节流。实现这个动作的代码模块成为”类加载器“。
通过自定义类加载器可以实现各种有趣而强大的功能更:OSGi,热部署,代码加密等。
类加载器的加载流程
启动类加载器
1.系统启动的时候,首先会通过由C++实现的启动类加载器,加载<JAVA_HOME>/lib目录下面的jar包,或者被-Xbootclasspath参数指定的路径并且被虚拟机识别的文件名的jar包。把相关Class加载到方法区中。
2.这一步会加载关键的一个类:sun.misc.Launcher。这个类包含了两个静态内部类:
- ExtClassLoader:扩展类加载器内部类,下面会讲;
- AppClassLoader:应用程序类加载器内部类,下面会讲;
在加载到Launcher类完成后,会对该类进行初始化,初始化的过程中,会创建 ExtClassLoader 和 AppClassLoader,源码如下:
public Launcher()
ExtClassLoader extClassLoader;
try
extClassLoader = ExtClassLoader.getExtClassLoader();
catch (IOException iOException)
throw new InternalError("Could not create extension class loader", iOException);
try
this.loader = AppClassLoader.getAppClassLoader(extClassLoader);
catch (IOException iOException)
throw new InternalError("Could not create application class loader", iOException);
Thread.currentThread().setContextClassLoader(this.loader);
...
由于启动类加载器是由C++实现的,所以在Java代码里面是访问不到启动类加载器的,如果尝试通过String.class.getClassLoader()获取启动类的引用,会返回null;
问题:
- 启动类加载器,扩展类加载器和应用类加载器都是由谁加载的?
- 启动类加载器是JVM的内部实现,在JVM申请好内存之后,由JVM创建这个启动类加载器
- 扩展类加载器和应用程序类加载器是由启动类加载器加载进来的;
说说以下代码输出什么:
public static void main(String[] args)
System.out.println("加载当前类的加载器:" + TestClassLoader.class.getClassLoader());
System.out.println("加载应用程序类加载器的加载器"
+ TestClassLoader.class.getClassLoader().getClass().getClassLoader());
System.out.println("String类的启动类加载器" + String.class.getClassLoader());
类加载器的双亲委派机制
双亲委派机制原理
双亲委派模型在JDK1.2之后被引入,并广泛使用,这不是一个强制性的约束模型,而是Java设计者推荐给开发者的一种类加载器实现方式。我们也可以覆盖对应的方式,实现自己的加载模型。
类加载器的双亲委派机制如下:
- 一个类加载器收到了类加载请求,不会自己立刻尝试加载类,而是把请求委托给父加载器去完成,每一层都是如此,所有的家在请求最终都传递到最顶层的类加载器进行处理;
- 如果父加载器不存在了,那么尝试判断有没有被启动类加载器加载;
- 如果的确没有被加载,则再自己尝试加载。
为什么要有这么复杂的双亲委派机制?
如果没有这种机制,我们就可以篡改启动类加载器中需要的类了,如,修自己编写一个java.lang.Object用自己的类加载器进行加载,系统中就会存在多个Object类,这样Java类型体系最基本的行为也就无法保证了。
双亲委派机制处理流程
JDK中默认的双亲委派处理流程是怎么的呢?接下来我们看看代码,以下是java.lang.ClassLoader.loadClass()方法的实现:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
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.
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;
转成流程图,即是:
如山图所以,总是先会尝试让父类加载器先加载,其次判断启动类加载器是否已经加载了,最后才尝试从当前类加载器加载。转换为更清晰的模型如下:
双亲委派模型具有以下特点:
- 可见性原则:
- 应用类加载器可以读取到扩展类加载器和启动类加载器加载进来的Class的;
- 扩展类加载器可以读取到由启动类加载器加载进来的Class的;
- 唯一性:
- 类是唯一的,没有重复的类;
类加载器和Class实例的题外话
启动类加载器,扩展类加载器,应用程序类加载器,他们分别管理者各自方法区里的一个区块。
方法区里面主要存储的是类的运行时数据结构,这个类的在方法区中的各种数据结构信息通过类的Class实例进行访问。
如下图:
- 类加载器:负责管理各个存储区的类信息,如加载和卸载类信息;
- Class实例:负责对接外部需求,如果外部有人想查看里面的类信息,则需要通过Class实例来获取;
另外,方法区里面,启动类加载器类信息对扩展两类加载器类信息可见,而前面两者的类信息又对应用程序类加载器类信息可见。
其他非双亲委派模型的案例
JDK 1.0遗留问题
JDK1.0已经存在了ClassLoader类,但是当时还没有双亲委派机制,用户为了自定义类加载器,需要重新loadClass()方法,而我们知道,在JDK1.2以后,loadClass里面就是双亲委派机制的实现代码,此时,要实现自定义类加载器,需要重新findClass()类即可。如果重新了loadClass()方法,也就意味着不再遵循双亲委派模型了。
线程上下文类加载器
Tomcat中的类加载器
我们知道Tomcat目录结构中有以下目录:
-
/common/: 该目录下的类库可被Tomcat和所有的WebApp共同使用;
-
/server/: 该目录下的类库可被Tomcat使用,但对所有的WebApp不可见;
-
/shared/: 该目录下的类库可被所有的WebApp共同使用,但对Tomcat自己不可见;
-
另外Web应用程序还有自身的类库,放在/WebApp/WEB-INF目录中:这里面的类库仅仅可以被此Web应用程序使用,对Tomcat和其他Web应用程序都不可见。
-
为了实现以上各个目录的类库可见性效果,Tomat提供了如下的自定义类加载器:
现在如下场景:
我们发现Tomcat下面有若干个webapp,每个webapp都用到了spring,于是我们把spring的jar包放到了shared目录中。
于是问题出现了:由于spring的jar包是由Shared类加载器加载的,假设我们要使用SpringContext的getBean方法,获取webapp中的Bean,如果是按照双亲委派模型,就会有问题了,因为webapp中的Java类是对SharedClassLoader不可见的:
Spring中的线程上下文类加载器
为了解决这个问题,Spring使用了线程上下文类加载器,即从ThreadLocal中获取到当前线程的上下文类加载器,来加载所有的类库和类。
Spring中的bean类加载器
ApplicationContext中有一个beanClassLoader字段,这个是bean的类加载器,在prepareBeanFactory()方法中做了初始化:
beanFactory.setBeanClassLoader(getClassLoader());
getClassLoader方法如下:
@Override
@Nullable
public ClassLoader getClassLoader()
return (this.classLoader != null ? this.classLoader :
ClassUtils.getDefaultClassLoader());
ClassUtils.getDefaultClassLoader()方法:
@Nullable
public static ClassLoader getDefaultClassLoader()
ClassLoader cl = null;
try
cl = Thread.currentThread().getContextClassLoader();
catch (Throwable ex)
// Cannot access thread context ClassLoader - falling back...
if (cl == null)
// No thread context class loader -> use class loader of this class.
cl = ClassUtils.class.getClassLoader();
if (cl == null)
// getClassLoader() returning null indicates the bootstrap ClassLoader
try
cl = ClassLoader.getSystemClassLoader();
catch (Throwable ex)
// Cannot access system ClassLoader - oh well, maybe the caller can live with null...
return cl;
可以发现,这里最终取了当前线程上下文中的ClassLoader。
加载Bean
来看看Spring加载Class的代码。这里我们直接找到实例化Singletons的方法跟进去找需要关注的代码:
我们发现在加载Bean Class的时候调用了这个方法:
AbstractBeanFactory:
ClassLoader beanClassLoader = getBeanClassLoader();
也就是用到了ApplicationContext中的beanClassLoader,线程上下文类加载器来加载Bean Class实例。
总结
Spring作为一个第三方类库,可能被任何的ClassLoader加载,所以最灵活的方式是直接使用上下文类加载器。
以上是关于JVM技术专题针对于加载器与双亲委派机制分析和研究指南 「 入门篇」的主要内容,如果未能解决你的问题,请参考以下文章