前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >第18次文章:JVM中的类加载机制

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

作者头像
鹏-程-万-里
发布2019-09-28 18:07:43
4970
发布2019-09-28 18:07:43
举报

这周介绍一下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类的方式实现自己的类加载器,以满足一些特殊的需求。

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

代码语言:javascript
复制
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

代码语言:javascript
复制
package java.lang;
public class String {  public String toString() {    return "aaa";  }}

然后我们使用调用此类:

代码语言:javascript
复制
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(...)转抛异常,终止加载过程。

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

代码语言:javascript
复制
/** * 自定义文件系统类加载器 * */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();        }      }    }  }  }

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

代码语言:javascript
复制
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中了,并不需要使用自定义文件系统类加载器。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-05-12,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Java小白成长之路 微信公众号,前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 这周介绍一下JVM中的类加载机制,主要是类加载器的层次结构,代理模式以及自定义类加载器。
  • 一、类加载器的层次结构(树状结构)
    • 1、引导类加载器(bootstrap class loader)
      • 2、扩展程序类(extensions class loader)
        • 3、应用程序类加载器(application class loader)
          • 4、自定义类加载器
            • 双亲委托机制:
        • 二、类加载器的代理模式:
        • 三、自定义类加载器
          • 自定义类加载器的流程:
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档