深入了解Java“双亲委派”模型

时间:2022-11-11 09:53:03

一、三种类加载器

 JVM并不是把所有的类一次性全部加载到JVM中的,也不是每次用到一个类的时候都去查找,对于JVM级别的类加载器在启动时就会把默认的 JAVA_HOME/lib里的class文件加载到JVM中,因为这些是系统常用的类,对于其他的第三方类,则采用用到时就去找,找到了就缓存起来的, 下次再用到这个类的时候就可以直接用缓存起来的类对象了。

    AppClassLoader的Parent是ExtClassLoader,而ExtClassLoader的Parent为Bootstrap ClassLoader。
之所以要定义这么多类加载器(当然还可以自己扩展)是因为java是动态加载类的,用到什么就加载什么以节省内存,
采用逐级加载的方式。
    (1)首先加载核心API,让系统最基本的运行起来。比如启动类加载器会加载jdk包里的rt.jar(里面有java.lang.*,
所以不需要我们在import了,当然还有其他很多包)
    (2)加载扩展类
    (3)加载用户自定义的类

1、启动类装载器

   启动(也称为原始)类加载器—bootstrap classloader,负责加载JAVA_HOME\lib目录中并且能被虚拟机识别的类库到JVM内存中。即——对于JVM级别的类加载器在启动时就会把默认的JAVA_HOME/lib里的class文件以及rt.jar中的java.lang包加载到JVM中,因为这些是系统常用的类。启动类装载器是用C++写的,它是Java虚拟机的一部分。

   这里给出一个sun.boot.class.path的概念,它是系统属性,它包含了核心类库的类路径,如果启动类加载器需要加载核心类库时,就可以根据该路径去查找类了。

System.out.println("boot:"+System.getProperty("sun.boot.class.path"));

在我的计算机上的结果为:
boot: C:\Program Files\Java\jre1.8.0_121\lib\resources.jar;

C:\ProgramFiles\Java\jre1.8.0_121\lib\rt.jar;

C:\ProgramFiles\Java\jre1.8.0_121\lib\jsse.jar;

C:\ProgramFiles\Java\jre1.8.0_121\lib\jce.jar;

C:\ProgramFiles\Java\jre1.8.0_121\lib\charsets.jar;

C:\ProgramFiles\Java\jre1.8.0_121\lib\jfr.jar        

2、扩展类装载器

扩展类加载器—extension classloader,它负责加载JRE的扩展目录(JAVA_HOME/jre/lib/ext)中JAR的类包。这为引入除Java核心类以外的新功能提供了一个标准机制。因为默认的扩展目录对所有从同一个JRE中启动的JVM都是通用的,所以放入这个目录的 JAR类包对所有的JVM和systemclassloader都是可见的。在这个实例上调用方法getParent()总是返回空值null,因为启动类装载器不是一个真正的ClassLoader实例。

3、系统类装载器

    系统(也称为应用)类加载器—system classloader,它负责在JVM被启动时,它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。总能通过静态方法ClassLoader.getSystemClassLoader()找到该类加载器。

补充:关于获取工程路径(运行时,类存放的路径)ClassLoader提供了两个方法用于从装载的类路径中取得资源:

        public URL  getResource (String name);

        public InputStream  getResourceAsStream (String name);

      这里name是资源的类路径,它是相对与“/”根路径下的位置。getResource得到的是一个URL对象来定位资源,而getResourceAsStream取得该资源输入流的引用保证程序可以从正确的位置抽取数据。但是真正使用的不是ClassLoader的这两个方法,而是Class的 getResource和getResourceAsStream方法,因为Class对象可以从你的类得到(如YourClass.class或 YourClass.getClass()),而ClassLoader则需要再调用一次YourClass.getClassLoader()方法,不过根据JDK文档的说法,Class对象的这两个方法其实是“委托”(delegate)给装载它的ClassLoader来做的,所以只需要使用 Class对象的这两个方法就可以了。

       下面是一些得到classpath和当前类的绝对路径的一些方法。你可能需要使用其中的一些方法来得到你需要的资源的绝对路径。

(1) this.getClass().getResource("")(在main方法中不能用this!)

得到的是当前类class文件的URI目录。不包括自己!,注该方法得到的是项目目录,也就是Bin的上一级!例如

如:file:/D:/workspace/jbpmtest3

(2) this.getClass().getResource("/").toURI().getPath()

得到的是当前的classpath的绝对URI路径 。

如:file:/D:/workspace/jbpmtest3/bin/

(3) this.getClass().getClassLoader().getResource("").toURI().getPath()

得到的也是当前ClassPath的绝对URI路径 。

如:file:/D:/workspace/jbpmtest3/bin/

(4) ClassLoader.getSystemResource("").toURI().getPath()

得到的也是当前ClassPath的绝对URI路径 。

如:file:/D:/workspace/jbpmtest3/bin/

(5) Thread.currentThread().getContextClassLoader().getResource("").toURI().getPath()

得到的也是当前ClassPath的绝对URI路径 。

如:file:/D:/workspace/jbpmtest3/bin/

(6) ServletActionContext.getServletContext().getRealPath(“read.txt”)

输出:

/D:/Software/apache-tomcat-9.0.0.M22/apache-tomcat-9.0.0.M22/wtpwebapps/Servlet/WEB-INF/classes/read.txt

    Web应用程序 中,得到Web应用程序的根目录的绝对路径。这样,我们只需要提供相对于Web应用程序根目录的路径,就可以构建出定位资源的绝对路径。

如:file:/D:/workspace/.metadata/.plugins/org.eclipse.wst.server.core/tmp0/wtpwebapps/WebProject

注意点:

(1) 尽量不要使用相对于System.getProperty("user.dir")当前用户目录的相对路径。这是一颗定时炸弹,随时可能要你的命。

(2) 尽量使用URI形式的绝对路径资源。它可以很容易的转变为URI,URL,File对象。

(3)尽量使用相对classpath的相对路径。不要使用绝对路径。使用上面ClassLoaderUtil类的public static URL getExtendResource(String relativePath)方法已经能够使用相对于classpath的相对路径定位所有位置的资源。

(4) 绝对不要使用硬编码的绝对路径。因为,我们完全可以使用ClassLoader类的getResource("")方法得到当前classpath的绝对路径。如果你一定要指定一个绝对路径,那么使用配置文件,也比硬编码要好得多!

补充: JAVA_HOME、CLASSPATH和PATH之间的区别

   (1)JAVA_HOME指向的是JDK的安装路径,如D:\JDK_1.4.2,在这路径下应该能够找到bin、lib等目录。

   (2)classpath环境变量Classpath设置的目的,在于告诉Java执行环境,在哪些目录下可以找到您所要执行的Java程序所需要的类或者包。JVM和其他JDK工具通过依次搜索平台库,扩展库,和类路径来查找类。

   (3)path环境变量指定了JDK命令搜索路径,设置path的作用是让操作系统可以找到JDK命令(如javac 、java)。path环境变量原来Windows里面就有,只需修改一下,使他指向JDK的bin目录,这样在控制台下面编译、执行程序时就不需要再键入一大串路径了。

二、类的加载过程

    在版本1.2中,装载本地可用的class文件的工作被分配到多个类装载器中。启动类装载器负责装载核心的Java API文件。因为核心Java API class文件是用于“启动”Java虚拟机的,所以启动类装载器的名字也因此而得。用户自定义类装载器负责其他class文件的装载,例如应用程序运行的class文件。在应用程序启动以前,它至少创建一个用户自定义类装载器,也可能会是多个,所有的这些类装载器被连接在一个“双亲——孩子”的关系链中,其顶端是启动类装载器,其末端是系统类装载器系统类装载器是右Java应用程序创建的,用于用户自定义类装载器的默认委派双亲。这个默认的委派双亲本质也是一个用户自定义的类装载器(实际上它是由Java虚拟机实现提供的),用于装载本地的class文件。

                         深入了解Java“双亲委派”模型

 每个ClassLoader加载Class的过程是:

   1.检测此Class是否载入过(即在cache中是否有此Class),如果有到8,如果没有到2

   2.如果parent classloader不存在(没有parent,那parent一定是bootstrap    classloader了),到4

   3.依托“双亲模式”请求parent classloader载入,如果成功到8,不成功到5

   4.请求jvm从bootstrap classloader中载入,如果成功到8

   5.寻找Class文件(从与此classloader相关的类路径中寻找)。如果找不到则到7.

   6.从文件中载入Class,到8.

   7.抛出ClassNotFoundException.

   8.返回Class.

   通俗地讲,就是某个特定的类加载器在接到加载类的请求时,首先将加载任务委托给父类加载器,依次递归(所有的类加载请求最终都会传送到顶层的“启动类加载器上”),如果父类加载器可以完成类加载任务,就成功返回;只有所有父类加载器无法完成此加载任务时,才自己去加载。(在没有明确地情况下,一般默认系统类装载器为初始的父类加载器)

源码如下:

protected synchronizedClass<?>  loadClass(String name,Boolean resolve) throws ClassNotFoundException{

     //首先,检查类是否已经加载过了

      Class c = findLoadedClass(name);

      If(c==null){

      try{

         if(parent!=null){

             c=parent.loadClass(name,false);

         }else{

             c=findBootstrapClassorNull(name);

           }

      }catch(ClassNotFoundException e){

      //如果父类加载器抛出ClassNotFoundException

     //说明福类加载器无法完成加载请求

   }

       If(c==null){

       //在父类加载器无法加载器的时候

      //再调用本身的findClass方法来进行类加载

              c= findClass(name);

       } 

 }

 If(resolve){

           resolveClass(name);

   }

    return c;

}

三、案例分析

下面给出一个例子,让我们来理解一下“双亲—孩子”模型的类加载顺序: 

例子1:定义一个自定义类加载器—FileSystemClassLoader

package com.classLoader;

import java.io.ByteArrayOutputStream;

import java.io.FileInputStream;

import java.io.InputStream;

/**

 * @authorzhegao

 * 自定义一个类装载器:通常需要对findClass进行重写

 */

public class FileSystemClassLoader extends ClassLoader{

    private String rootPath;

    public FileSystemClassLoader(String rootPath){

       this.rootPath=rootPath;

    }

    @Override

    protected Class<?> findClass(String name) throws ClassNotFoundException {

       //获取字节数组

       byte[] classData = getClassData(name);

       if(classData==null){

           throw new ClassNotFoundException();

       }else{

           return defineClass(name,classData,0,classData.length);

       }

    }

    /**

     * 读取文件的字节

     */

    private byte[] getClassData(String className){

       String classPath = classNameToPath(className);

       System.out.println(classPath);

       try {

           InputStream is =new FileInputStream(classPath);

           ByteArrayOutputStream bao =new ByteArrayOutputStream();

           byte[] buffer =new byte[5000];

           int point =0;

           while((point=is.read())!=-1){

              bao.write(point);

           }

           return bao.toByteArray();

       } catch (Exception e) {

           //e.printStackTrace();

       }

       return  null;

    }

    /**

     * 得到完整的类路径

     */

    private String classNameToPath(String className){

       //在这里需要做一个判断:一个是直接给类名;另一个是包下的类

       if(className.contains(".")){

           return rootPath+className.replace(".","\\")+".class";

       }else{

           return rootPath+className+".class";

       }  

    }  

}

 

package com.classLoader;

public class test {

    public static void main(String[] args) {

       String rootpath ="D:\\Software\\eclipse\\JavaJVM\\bin\\";

       FileSystemClassLoader fsc =new FileSystemClassLoader(rootpath);

       String className="com.examples.Sons";

       try

            Class<?> class1 =fsc.loadClass(className); //加载Sample 

            Object obj1 =class1.newInstance(); //创建对象 

            System.out.println(obj1.getClass().getName());//输出类名

            System.out.println(obj1.getClass().getClassLoader());//输出类加载器名称

        } catch (Exception e) { 

            e.printStackTrace(); 

        }   

    }

}

结果:  com.examples.Sons

            sun.misc.Launcher$AppClassLoader@4e25154f

分析:  类加载器的“双亲—孩子”模型得知,最底层到最高层依次是:我们的自定义类加载器—FileSystemClassLoader、系统类加载器、扩展类加载器和启动类加载器。在类第一次被加载时(一般运行时,类则被视为第一次被加载),自定义类加载器会委任给父类系统类加载器,然后像会“递归”依次委任给父类直到启动类加载器。然而启动类加载器只会根据sun.boot.class.path提供的路径加载相应的jar包,所以启动类加载器不能加载返回null;同样扩展类加载器会依据java.ext.dirs提供的路径加载相应的类,所以它也无法加载返回null;然后,到了系统类加载器,它会依据ClassPath去加载相应的类,显然能够找到,所以最终加载Sons类,其加载器就是“系统类加载器”。

 

例子2:假如还是上面的程序,我对com.classLoader.test做个修改

public class test {

    public static void main(String[] args) {

       String rootpath ="D:\\Software\\eclipse\\JavaJVM\\bin\\";

       FileSystemClassLoader fsc =new FileSystemClassLoader(rootpath);

       String className="com.examples.Son";//和上面比,修改了类名,由Sons变成了Son

       try

            Class<?> class1 =fsc.loadClass(className); //加载Sample 

            Object obj1 =class1.newInstance(); //创建对象 

            System.out.println(obj1.getClass().getName());

            System.out.println(obj1.getClass().getClassLoader());

        } catch (Exception e) { 

            e.printStackTrace(); 

        }   

    }

}

结果:C:\com\examples\Son.class

      java.lang.ClassNotFoundException

    at com.classLoader.FileSystemClassLoader.findClass(FileSystemClassLoader.java:22)

    at java.lang.ClassLoader.loadClass(UnknownSource)

    at java.lang.ClassLoader.loadClass(UnknownSource)

    at com.classLoader.test.main(test.java:11)

分析:和上面分析过程一样,根据“双亲—孩子”模式,然而启动类加载器只会根据sun.boot.class.path提供的路径加载相应的jar包,所以启动类加载器不能加载返回null;同样扩展类加载器会依据java.ext.dirs提供的路径加载相应的类,所以它也无法加载返回null;然后,到了系统类加载器,会发现该类路径ClassPath无法找到Son这个类,所以也是返回null。在所有父类均不能加载类的前提下,自定义类加载器将会试着自己加载该类。首先,在使用父类的load()方法中会调用findClass()方法即判断该类在路径上是否存在,而这里的findClass()方法是由FileSystemClassLoader重写的。所以在执行findClass()方法时会抛“ClassNotFoundException”异常。

所以:这就是为何只有在发生异常时才会调用FileSystemClassLoader的findClass()方法,是因为正常加载时,由父类“系统类加载器”加载类,只有父类加载不了时,自定义类加载器才会试着去加载类,才会触发了自定义类加载器的findClass()方法。