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

  • 2019 年 10 月 8 日
  • 筆記

这周介绍一下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中了,并不需要使用自定义文件系统类加载器。