专栏首页Java小白成长之路第18次文章:JVM中的类加载机制

第18次文章:JVM中的类加载机制

这周介绍一下JVM中的类加载机制,主要是类加载器的层次结构,代理模式以及自定义类加载器。

一、类加载器的层次结构(树状结构)

1、引导类加载器(bootstrap class loader)

-主要用来加载java的核心库,是用原生代码C语言来实现的,并不继承自java.lang.ClassLoader。

-加载扩展类和应用程序类加载器。并不指定他们的父类加载器。

2、扩展程序类(extensions class loader)

-用来加载java的扩展库。java虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载java类。

-由sun.misc.Launcher$ExtClassLoader实现

3、应用程序类加载器(application class loader)

-它是根据java应用的类路径(classpath,java.class.path),一般来说,java应用的类都是由它来完成加载的。

4、自定义类加载器

-开发人员可以通过继承java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。

下面我们先简单的测试一下几个类加载器的层次结构:

public class Demo02 {
  public static void main(String[] args) {    //应用程序类加载器jdk.internal.loader.ClassLoaders$AppClassLoader@2a33fae0    System.out.println(ClassLoader.getSystemClassLoader());    //扩展类加载器jdk.internal.loader.ClassLoaders$PlatformClassLoader@707f7052    System.out.println(ClassLoader.getSystemClassLoader().getParent());    //引导类加载器,使用原生代码实现(C),所以此处无法获取    System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());        System.out.println(System.getProperty("java.class.path"));
  }
}

查看一下结果:

tips:

(1)首先我们获取当前线程的类加载器,可以得到一个应用程序类加载器(AppClassLoader),然后获取其父类加载器,得到一个扩展类加载器(PlatformClassLoader),最后再获取扩展类加载器的父类,输出为null,是因为扩展类加载器的父类为引导类加载器(Bootstrap class loader),同时,引导类加载器是使用原生代码实现的,并不是继承自java.lang.ClassLoader,所以在获取其名称时,无法在java环境中显示。由此可以得到,在类加载器中,其层次结构为:自定义类加载器——>应用程序类加载器——>扩展类加载器——>引导类加载器。

(2)我们获取当前类加载器的加载目录“java.class.path”,可以看出,其加载目录在当前工程文件下的bin目录内。

二、类加载器的代理模式:

类加载器的代理模式是指:在加载指定的类的时候,当前类加载器并不直接加载这个类,而是交给其他类进行加载。下面我们介绍一种代理模式——双亲委托机制。

双亲委托机制:

-就是某个特定的类加载器在介绍加载类的请求时,首先将加载任务委托给父类加载器,依次追溯,直到最原始的父类,如果父类加载器可以完成类加载任务,就成功返回;只有父类加载器无法完成此加载任务时,才自己去加载。

-双亲委托机制是为了保证java核心库的类型安全。这种机制就保证了用户无法使用自己定义的java.lang.Object类的情况。

-类加载器除了用于加载类,也是安全的最基本的屏障。

我们举一个简单的实例,进行分析双亲委托机制的安全性:

首先,我们自己建立一个经常使用,但是却从来没有建过的类java.lang.String

package java.lang;
public class String {  public String toString() {    return "aaa";  }}

然后我们使用调用此类:

package com.peng.test;
public class Demo02 {
  public static void main(String[] args) {    String a = "peng";    System.out.println(a.getClass().getClassLoader());    System.out.println(a.toString());      }
}

输出结果:

tips:

(1)我们先关注一下结果,在自定义的String类中,我们是返回一个字符串“aaa”,而最后打印在控制台上的内容是我们重新定义的一个变量“peng”。所以类加载器在加载String类的时候,直接加载了java核心包(rt.jar)中的java.lang.String类,而不是我们自定义的java.lang.String类。

(2)导致这种结果的原因就是类加载机制中的双亲委派机制。当我们的系统类加载器获取到String类的时候,首先会交给其父类扩展类加载器,然后又交给扩展类加载器的父类——引导类加载器。引导类加载器为最高层父类,所以,当一个类被加载的时候,首先是将其一层一层向上传递,最后交给引导类加载器,从引导类加载器开始进行加载。当引导类加载器获取到java.lang.String类的时候,直接可以从java核心库当中加载此类,所以不需要将此类向下传递加载。

(3)这种机制就确保了我们无法使用自定义的java核心库中的类,保护了java核心库的安全性。

(4)代理模式有很多种,双亲委托机制是代理模式的一种,也并不是所有的类加载器都采用双亲委托机制。比如说:Tomcat服务器类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。

三、自定义类加载器

自定义类加载器的流程:

-继承:java.lang.ClassLoader

-首先检查请求的类型是否已经被这个类加载器加载到命名空间中了,如果已经加载,直接返回;

-委派类加载请求给父类加载器,如果父类加载器能够完成,则返回父类加载器加载的Class实例;

-调用本类加载器的findClass(...)方法,试图获取对应的字节码,如果获取的到,则调用defineClass(...)导入类型到方法区;如果获取不到对应的字节码或者其他原因失败,返回异常给loadClass(...),loadClass(...)转抛异常,终止加载过程。

下面,我们根据上述流程,写一个具体的自定义文件系统类加载器:

/** * 自定义文件系统类加载器 * */public class FileSystemClassLoader extends ClassLoader {
  private String rootDir;//根目录  public FileSystemClassLoader(String rootDir) {    this.rootDir = rootDir;  }    @Override  protected Class<?> findClass(String name) throws ClassNotFoundException {    Class<?> c = findLoadedClass(name);//在已加载的类中查找name        //应该要先查询有没有加载过这个类,如果已经加载,则直接返回加载号的类。如果没有,则加载新的类    if(c!=null) {      return c;    }else {//双亲委派机制,委派给父类进行加载      ClassLoader parent = this.getParent();//获取该类的父类加载器      try {        c = parent.loadClass(name);//委派给父类加载      } catch (Exception e) {        // TODO: handle exception      }            if(c != null) {        return c;      }else {//如果父类加载器中也没有将该类进行加载,则通过IO流,将该类别信息输入,然后自定义此类        byte[] classData = getClassData(name);//获取此类的信息        if(classData == null) {          throw new ClassNotFoundException();        }else {          c = defineClass(name, classData, 0, classData.length);//定义此类        }                return c;      }    }  }    /**   * 以字节数组的方式获取类信息   * @param className   * @return   */  private byte[] getClassData(String className) {    String classPath = this.rootDir + "/" + className.replace(".", "/") + ".class";        InputStream is = null;    ByteArrayOutputStream baos = new ByteArrayOutputStream();
    try {//读写待加载类的文件      is = new FileInputStream(classPath);      byte[] buffer = new byte[1024];      int temp = 0;      while(-1 != (temp=is.read(buffer))) {        baos.write(buffer, 0, temp);      }      return baos.toByteArray();
    } catch (Exception e) {      // TODO Auto-generated catch block      e.printStackTrace();      return null;    } finally {//关闭IO流      if(is != null) {        try {          is.close();        } catch (IOException e) {          // TODO Auto-generated catch block          e.printStackTrace();        }      }      if(baos != null) {        try {          baos.close();        } catch (IOException e) {          // TODO Auto-generated catch block          e.printStackTrace();        }      }    }  }  }

然后我们简单的测试一下这个自定义文件系统类加载器:

package com.peng.test;
/** * 测试自定义的类加载器 FileSystemClassLoader */public class Demo03 {  public static void main(String[] args) throws ClassNotFoundException {    FileSystemClassLoader loader = new FileSystemClassLoader("G:/java学习/test");    FileSystemClassLoader loader2 = new FileSystemClassLoader("G:/java学习/test");        Class<?> c = loader.loadClass("com.peng.test.User");    Class<?> c2 = loader.loadClass("com.peng.test.User");    Class<?> c3 = loader2.loadClass("com.peng.test.User");        Class<?> c4 = loader2.loadClass("java.lang.String");    Class<?> c5 = loader2.loadClass("com.peng.test.Demo01");        System.out.println(c);    System.out.println(c.hashCode());    System.out.println(c2.hashCode());    System.out.println(c3.hashCode());//同一个类,被两个类加载器加载的同一个类,JVM认不认为是相同的类        System.out.println(c4.hashCode());        System.out.println(c3.getClassLoader());//使用我们自定义的类加载器    System.out.println(c4.getClassLoader());//引导类加载器    System.out.println(c5.getClassLoader());//系统默认的类加载器AppClassLoader  }}

输出结果如下:

tips:

(1)首先我们观察对象c和c2,两者是使用同一个文件系统类加载器,加载同一个类得到对象,所以两个对象的hashcode相同,属于同一个对象。

(2)但是我们观察对象c3和c,c3使用了另一个类加载器loader2进行加载,加载的类和c加载的类是相同的,但是最后两者的hashcode不同,代表了两个不同的对象,这个结果证明被两个类加载器加载的同一个类,JVM认为是不同的类。

(3)我们再创建两个对象c4和c5,分别加载核心类“java.lang.String”和当前工程文件中的类“com.peng.test.Demo01”,分别获得c3、c4、c5的类加载器,并输出到控制台上。可以发现c3使用的是我们自定义的文件系统类加载器,c4依旧使用的是引导类加载器,c5使用的是应用程序类加载器。因为c5中加载的Demo01对象属于此工程文件中的一个文件,所以我们的主程序Demo03在加载的时候,就已经使用应用程序类加载器将其加载在JVM中了,并不需要使用自定义文件系统类加载器。

本文分享自微信公众号 - Java小白成长之路(Java_xiaobai),作者:鹏程万里

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-05-12

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 第19次文章:类加载器的加密解密+内部类

    在上一期的文章中,我们介绍了自定义类加载器做法的整个流程,还没有理解同学可以点击回看哈!《第18次文章:JVM中的类加载机制》。在日常生活中,我们有时候需要将一...

    鹏-程-万-里
  • 第17次文章:初探JVM

    JVM把class文件加载到内存,并对数据进行校验、解析和初始化,最终形成JVM可以直接使用的java类型的过程。

    鹏-程-万-里
  • 刷题第3篇:重复字符串的删除

    给你一个字符串 s,「k 倍重复项删除操作」将会从 s 中选择 k 个相邻且相等的字母,并删除它们,使被删去的字符串的左侧和右侧连在一起。你需要对 s 重复进行...

    鹏-程-万-里
  • 类加载器以及双亲委派模型

    首先我们来描述一个小说场景,通过这个场景在去理解我们相关的类加载器的执行以及双亲委派模型。

    胖虎
  • 深入理解Java类加载器(ClassLoader)

    本篇博文主要是探讨类加载器,同时在本篇中列举的源码都基于Java8版本,不同的版本可能有些许差异。主要内容如下

    用户1257215
  • 深入理解Java类加载器机制

    Java里面的类加载机制,可以说是Java虚拟机核心组件之一,掌握和理解JVM虚拟机的架构,将有助于我们站在底层原理的角度上来理解Java语言,这也是为什么我们...

    我是攻城师
  • JVM系列十三(类加载器).

    比较两个类是否“相等”,只有在这两个类是由同一个类加载器加载的前提之下才有意义,否则,即使这两个类是来源于同一个 Class 文件,只要加载它们的类加载器不同,...

    JMCui
  • 类加载器与双亲委派模型1 类加载器 2 双亲委派模型

    类加载器(ClassLoader)是Java语言的一项创新,也是Java流行的一个重要原因。 在类加载的第一阶段“加载”过程中,需要通过一个类的全限定名来获取...

    JavaEdge
  • 类加载器并行加载类 实战及代码解析

    类加载器是如何加载一个类的? 类加载器如何实现并行加载类? 带着这2个问题,我们看下面的内容。

    用户5325874
  • 深入理解JVM虚拟机---类的双亲委托模型

    类的双亲委托模型&相关动作&命名空间&上下文类加载器

    俺也想起舞

扫码关注云+社区

领取腾讯云代金券