首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >实习杂记(30):虚拟机类加载机制(3)

实习杂记(30):虚拟机类加载机制(3)

作者头像
wust小吴
发布2019-11-14 17:28:37
2990
发布2019-11-14 17:28:37
举报
文章被收录于专栏:风吹杨柳风吹杨柳

转载地址:http://blog.csdn.net/zhoudaxia/article/details/35824249

1 基本信息

  每个开发人员对Java.lang.ClassNotFoundExcetpion这个异常肯定都不陌生,这背后就涉及到了java技术体系中的类加载。Java的类加载机制是技术体系中比较核心的部分,虽然和大部分开发人员直接打交道不多,但是对其背后的机理有一定理解有助于排查程序中出现的类加载失败等技术问题,对理解java虚拟机的连接模型和java语言的动态性都有很大帮助。

2 Java虚拟机类加载器结构简述

2.1 JVM三种预定义类型类加载器

  我们首先看一下JVM预定义的三种类型类加载器,当一个 JVM启动的时候,Java缺省开始使用如下三种类型类装入器:

  启动(Bootstrap)类加载器:引导类装入器是用本地代码实现的类装入器,它负责将 <Java_Runtime_Home>/lib下面的核心类库或-Xbootclasspath选项指定的jar包加载到内存中。由于引导类加载器涉及到虚拟机本地实现细节,开发者无法直接获取到启动类加载器的引用,所以不允许直接通过引用进行操作。

  扩展(Extension)类加载器:扩展类加载器是由Sun的ExtClassLoader(sun.misc.Launcher$ExtClassLoader)实现的。它负责将< Java_Runtime_Home >/lib/ext或者由系统变量-Djava.ext.dir指定位置中的类库加载到内存中。开发者可以直接使用标准扩展类加载器。

  系统(System)类加载器:系统类加载器是由 Sun的 AppClassLoader(sun.misc.Launcher$AppClassLoader)实现的。它负责将系统类路径java -classpath或-Djava.class.path变量所指的目录下的类库加载到内存中。开发者可以直接使用系统类加载器。

  除了以上列举的三种类加载器,还有一种比较特殊的类型就是线程上下文类加载器,这个将在后面单独介绍。

2.2 类加载双亲委派机制介绍和分析

在这里,需要着重说明的是,JVM在加载类时默认采用的是双亲委派机制。通俗的讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。关于虚拟机默认的双亲委派机制,我们可以从系统类加载器和扩展类加载器为例作简单分析。

图一 标准扩展类加载器继承层次图

图二系统类加载器继承层次图

通过图一和图二我们可以看出,类加载器均是继承自java.lang.ClassLoader抽象类。我们下面我们就看简要介绍一下java.lang.ClassLoader中几个最重要的方法:

//加载指定名称(包括包名)的二进制类型,供用户调用的接口
  1. public Class<?> loadClass(String name) throws ClassNotFoundException{ … }
  2. //加载指定名称(包括包名)的二进制类型,同时指定是否解析(但是这里的resolve参数不一定真正能达到解析的效果),供继承用
  3. protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException{ … }
  4. //findClass方法一般被loadClass方法调用去加载指定名称类,供继承用
  5. protected Class<?> findClass(String name) throws ClassNotFoundException { … }
  6. //定义类型,一般在findClass方法中读取到对应字节码后调用,可以看出不可继承
  7. //(说明:JVM已经实现了对应的具体功能,解析对应的字节码,产生对应的内部数据结构放置到方法区,所以无需覆写,直接调用就可以了)
  8. protected final Class<?> defineClass(String name, byte[] b, int off, int len) throws ClassFormatError{ … }

通过进一步分析标准扩展类加载器(sun.misc.Launcher$ExtClassLoader)和系统类加载器(sun.misc.Launcher$AppClassLoader)的代码以及其公共父类(java.net.URLClassLoader和java.security.SecureClassLoader)的代码可以看出,都没有覆写java.lang.ClassLoader中默认的加载委派规则---loadClass(…)方法。既然这样,我们就可以通过分析java.lang.ClassLoader中的loadClass(String name)方法的代码就可以分析出虚拟机默认采用的双亲委派机制到底是什么模样:

[java] view plain copy

public Class<?> loadClass(String name) throws ClassNotFoundException {
  1. return loadClass(name, false);
  2. }
  3. protected synchronized Class<?> loadClass(String name, boolean resolve)
  4. throws ClassNotFoundException {
  5. // 首先判断该类型是否已经被加载
  6. Class c = findLoadedClass(name);
  7. if (c == null) {
  8. //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载
  9. try {
  10. if (parent != null) {
  11. //如果存在父类加载器,就委派给父类加载器加载
  12. c = parent.loadClass(name, false);
  13. } else {
  14. //如果不存在父类加载器,就检查是否是由启动类加载器加载的类,
  15. //通过调用本地方法native findBootstrapClass0(String name)
  16. c = findBootstrapClass0(name);
  17. }
  18. } catch (ClassNotFoundException e) {
  19. // 如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能
  20. c = findClass(name);
  21. }
  22. }
  23. if (resolve) {
  24. resolveClass(c);
  25. }
  26. return c;
  27. }

  通过上面的代码分析,我们可以对JVM采用的双亲委派类加载机制有了更感性的认识,下面我们就接着分析一下启动类加载器、标准扩展类加载器和系统类加载器三者之间的关系。可能大家已经从各种资料上面看到了如下类似的一幅图片:

图三 类加载器默认委派关系图

  上面图片给人的直观印象是系统类加载器的父类加载器是标准扩展类加载器,标准扩展类加载器的父类加载器是启动类加载器,下面我们就用代码具体测试一下:

[java] view plain copy

public class LoaderTest {
  1. public static void main(String[] args) {
  2. try {
  3. System.out.println(ClassLoader.getSystemClassLoader());
  4. System.out.println(ClassLoader.getSystemClassLoader().getParent());
  5. System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
  6. } catch (Exception e) {
  7. e.printStackTrace();
  8. }
  9. }
  10. }

  说明:通过java.lang.ClassLoader.getSystemClassLoader()可以直接获取到系统类加载器。   代码输出如下:

[plain] view plain copy

  1. sun.misc.Launcher$AppClassLoader@6d06d69c
  2. sun.misc.Launcher$ExtClassLoader@70dea4e
  3. null

  通过以上的代码输出,我们可以判定系统类加载器的父加载器是标准扩展类加载器,但是我们试图获取标准扩展类加载器的父类加载器时确得到了null,就是说标准扩展类加载器本身强制设定父类加载器为null。我们还是借助于代码分析一下。

  我们首先看一下java.lang.ClassLoader抽象类中默认实现的两个构造函数:

[java] view plain copy

  1. protected ClassLoader() {
  2. SecurityManager security = System.getSecurityManager();
  3. if (security != null) {
  4. security.checkCreateClassLoader();
  5. }
  6. //默认将父类加载器设置为系统类加载器,getSystemClassLoader()获取系统类加载器
  7. this.parent = getSystemClassLoader();
  8. initialized = true;
  9. }
  10. protected ClassLoader(ClassLoader parent) {
  11. SecurityManager security = System.getSecurityManager();
  12. if (security != null) {
  13. security.checkCreateClassLoader();
  14. }
  15. //强制设置父类加载器
  16. this.parent = parent;
  17. initialized = true;
  18. }

我们再看一下ClassLoader抽象类中parent成员的声明:

[java] view plain copy

  1. // The parent class loader for delegation
  2. private ClassLoader parent;

  声明为私有变量的同时并没有对外提供可供派生类访问的public或者protected设置器接口(对应的setter方法),结合前面的测试代码的输出,我们可以推断出:   1. 系统类加载器(AppClassLoader)调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为标准扩展类加载器(ExtClassLoader)。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)   2. 扩展类加载器(ExtClassLoader)调用ClassLoader(ClassLoader parent)构造函数将父类加载器设置为null。(因为如果不强制设置,默认会通过调用getSystemClassLoader()方法获取并设置成系统类加载器,这显然和测试输出结果不符。)   现在我们可能会有这样的疑问:扩展类加载器(ExtClassLoader)的父类加载器被强制设置为null了,那么扩展类加载器为什么还能将加载任务委派给启动类加载器呢?

图四 标准扩展类加载器和系统类加载器成员大纲视图

图五 扩展类加载器和系统类加载器公共父类成员大纲视图

  通过图四和图五可以看出,标准扩展类加载器和系统类加载器及其父类(java.net.URLClassLoader和java.security.SecureClassLoader)都没有覆写java.lang.ClassLoader中默认的加载委派规则---loadClass(…)方法。有关java.lang.ClassLoader中默认的加载委派规则前面已经分析过,如果父加载器为null,则会调用本地方法进行启动类加载尝试。所以,图三中,启动类加载器、标准扩展类加载器和系统类加载器之间的委派关系事实上是仍就成立的。(在后面的用户自定义类加载器部分,还会做更深入的分析)。

2.3 类加载双亲委派示例

  以上已经简要介绍了虚拟机默认使用的启动类加载器、标准扩展类加载器和系统类加载器,并以三者为例结合JDK代码对JVM默认使用的双亲委派类加载机制做了分析。下面我们就来看一个综合的例子。首先在IDE中建立一个简单的java应用工程,然后写一个简单的JavaBean如下:

[java] view plain copy

  1. package classloader.test.bean;
  2. public class TestBean {
  3. public TestBean() { }
  4. }

  在现有当前工程中另外建立一测试类(ClassLoaderTest.java)内容如下:

  测试一:

[java] view plain copy

  1. package classloader.test.bean;
  2. public class ClassLoaderTest {
  3. public static void main(String[] args) {
  4. try {
  5. //查看当前系统类路径中包含的路径条目
  6. System.out.println(System.getProperty("java.class.path"));
  7. //调用加载当前类的类加载器(这里即为系统类加载器)加载TestBean
  8. Class typeLoaded = Class.forName("classloader.test.bean.TestBean");
  9. //查看被加载的TestBean类型是被那个类加载器加载的
  10. System.out.println(typeLoaded.getClassLoader());
  11. } catch (Exception e) {
  12. e.printStackTrace();
  13. }
  14. }
  15. }

对应的输出如下:

[plain] view plain copy

  1. C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes
  2. sun.misc.Launcher$AppClassLoader@73d16e93

说明:当前类路径默认的含有的一个条目就是工程的输出目录。 测试二:

  将当前工程输出目录下的TestBean.class打包进test.jar剪贴到<Java_Runtime_Home>/lib/ext目录下(现在工程输出目录下和JRE扩展目录下都有待加载类型的class文件)。再运行测试一测试代码,结果如下:

[plain] view plain copy

  1. C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes
  2. sun.misc.Launcher$ExtClassLoader@15db9742

  对比测试一和测试二,我们明显可以验证前面说的双亲委派机制,系统类加载器在接到加载classloader.test.bean.TestBean类型的请求时,首先将请求委派给父类加载器(标准扩展类加载器),标准扩展类加载器抢先完成了加载请求。   测试三:   将test.jar拷贝一份到<Java_Runtime_Home>/lib下,运行测试代码,输出如下:

[plain] view plain copy

  1. C:\Users\JackZhou\Documents\NetBeansProjects\ClassLoaderTest\build\classes
  2. sun.misc.Launcher$ExtClassLoader@15db9742

  测试三和测试二输出结果一致。那就是说,放置到<Java_Runtime_Home>/lib目录下的TestBean对应的class字节码并没有被加载,这其实和前面讲的双亲委派机制并不矛盾。虚拟机出于安全等因素考虑,不会加载<Java_Runtime_Home>/lib存在的陌生类,开发者通过将要加载的非JDK自身的类放置到此目录下期待启动类加载器加载是不可能的。做个进一步验证,删除<Java_Runtime_Home>/lib/ext目录下和工程输出目录下的TestBean对应的class文件,然后再运行测试代码,则将会有ClassNotFoundException异常抛出。有关这个问题,大家可以在java.lang.ClassLoader中的loadClass(String name, boolean resolve)方法中设置相应断点运行测试三进行调试,会发现findBootstrapClass0()会抛出异常,然后在下面的findClass方法中被加载,当前运行的类加载器正是扩展类加载器(sun.misc.Launcher$ExtClassLoader),这一点可以通过JDT中变量视图查看验证。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2016-08-07 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 转载地址:http://blog.csdn.net/zhoudaxia/article/details/35824249
  • 1 基本信息
  • 2 Java虚拟机类加载器结构简述
    • 2.1 JVM三种预定义类型类加载器
      • 2.2 类加载双亲委派机制介绍和分析
        • 2.3 类加载双亲委派示例
        相关产品与服务
        腾讯云代码分析
        腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档