您的位置:首页 > 编程语言 > Java开发

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

2018-02-07 18:13 555 查看
一、三种类加载器

 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
4000
/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文件。
                         




 每个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()方法。
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  双亲委派机制