Java类加载机制-类加载器(ClassLoader)与双亲委派模型

自建博客地址:https://www.bytelife.net,欢迎访问! 本文为博客自动同步文章,为了更好的阅读体验,建议您移步至我的博客👇

本文作者: Jeffrey
本文链接: https://www.bytelife.net/articles/44598.html
版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!


Java虚拟机类加载过程中的“加载”阶段第一步就是“通过一个类的全限定名来获取描述此类的二级制字节流”,这个动作由Java虚拟机外部实现,以便让应用程序自己决定如何去获取所需要的类,实现这个动作的模块叫做“类加载器”。

类与类加载器

类加载器虽然只用于实现类的加载动作,但它在Java程序中的作用远不限于此。

对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类命名空间。

换言之,比较两个类“相等”,只有在两个类是由同一个类加载器加载的前提下才有意义,否则,即使这两个类来源于同一个Class文件,被同一个虚拟机加载,只要加载它们的类加载器不同,那这两个类就必定不相等。

Java中的类加载器

Java虚拟机只有两种不同的类加载器:

  • 启动类加载器(Bootstrap ClassLoader):使用C++语言(HotSpot)实现,是虚拟机的一部分,该类加载器实例无法被用户获取;
  • 所有其它的类加载器:均由Java语言实现,独立于虚拟机外部,并且全部继承自抽象类java.lang.ClassLoader;

从Java程序员的角度,类加载器还可以继续细化,绝大部分Java程序都会使用到以下3种类加载器。

  • 启动类加载器 (Bootstrap ClassLoader):这个类加载器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的目录中的,并且是虚拟机识别的(仅按照文件名识别,例如rt.jar)类库加载到虚拟机内存中。 启动类加载器无法被Java程序直接引用,用户在编写自定义加载器时,如果需要把加载请求委托给引导类加载器,直接使用null代替即可。
  • 扩展类加载器(Extension ClassLoader):这个加载器由sun.misc.Launcher$ExtClassLoader实现,他负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
  • 应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$AppClassLoader实现。该类是ClassLoader中的getSystemClassLoader()方法的返回值,因此也称作“系统类加载器”。它负责用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有定义过自己的类加载器,一般情况下这个就是程序的默认类加载器。

应用程序一般由这3中类加载器相互配合加载,如果有必要,还可以加入自己定义的类加载器,集成。

自定义类加载器

自定义类加载器可以直接或间接继承自类java.lang.ClassLoader。在java.lang.ClassLoader类的常用方法中,一般来说,自己开发的类加载器只需要覆写 findClass(String name)方法即可。 java.lang.ClassLoader类的方法 loadClass()封装了代理模式的实现。

  • 该方法会首先调用 findLoadedClass()方法来检查该类是否已经被加载过;
  • 如果没有加载过的话,会调用父类加载器的 loadClass()方法来尝试加载该类;
  • 如果父类加载器无法加载该类的话,就调用 findClass()方法来查找该类。

因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass()方法,而是覆写 findClass()方法。 下面是一个文件系统类加载器的例子:

public class FileSystemClassLoader extends ClassLoader { 
    private String rootDir; 
    public FileSystemClassLoader(String rootDir) { 
        this.rootDir = rootDir; 
    } 
    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 path = classNameToPath(className); 
        try { 
            InputStream ins = new FileInputStream(path); 
            ByteArrayOutputStream baos = new ByteArrayOutputStream(); 
            int bufferSize = 4096; 
            byte[] buffer = new byte[bufferSize]; 
            int bytesNumRead = 0; 
            while ((bytesNumRead = ins.read(buffer)) != -1) { 
                baos.write(buffer, 0, bytesNumRead); 
            } 
            return baos.toByteArray(); 
        } catch (IOException e) { 
            e.printStackTrace(); 
        } 
        return null; 
    } 
    private String classNameToPath(String className) { 
        return rootDir + File.separatorChar 
                + className.replace('.', File.separatorChar) + ".class"; 
    } 
 }

双亲委派模型

下图展示的类加载器之间的层次关系,称为类加载器的“双亲委派模型”。双亲委派模型要求除了顶层的启动类加载器外,其它类加载器必须有自己的父加载器。

这里的类加载器之间的父子关系一般不通过继承(Inheritance)来实现,而是通过组合(Composition)关系来服用父加载器代码。 双亲委派模型并不是一个强制性约束,而是Java设计者推荐给开发者的一种类加载实现方式。

双亲委派模型的工作过程

  • 如果一个类加载器收到了类加载的请求,它不会先自己尝试处理这个请求,而是委派给它的父类加载器,所有的请求最终都会传送到顶层的启动类加载器
  • 只有当父类反馈自己无法完成该请求(它的搜索范围中没有找到所需的类,即抛出ClassNotFoundException)时,子加载器才会尝试自己加载。

为什么使用双亲委派模型?

使用双亲委派模型可以使得Java类随着它的类加载器一起具备了一种带有优先级的层次关系。 例如类java.lang.Object,它存放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给处于模型最顶端的启动类加载器加载,因此Objcet类在程序中的各种类加载器环境中都是同一个类。 如果没有使用双亲委派模型,那么如果用户自己写了一个称为“java.lang.Object”的类,并放在程序的classpath中,那么系统将产生多个不同的Object类,可想而知,程序将一片混乱。

双亲委派模型的实现

双亲委派模型的实现非常简单,几乎所有的代码仅在loadClass()方法中实现,下面是一个简单的例子:

//双亲委派模型的实现源码  
protected synchronized Class<?> loadClass(String name, Boolean resolve)  throws ClassNotFoundException{  
    // 1、首先检查请求的类是否已经被加载过  
    Class c = findLoadedClass(name);  
    if(c == null){  
        try{  
            if(parent != null){    // 2、如果没有则调用父加载器的loadClass()方法  
                c = parent.loadClass(name, false);  

            // 3、如果父加载器为空则默认使用启动类加载器作为父加载器  
            } else{  
                c = findBootstrapClassOrNull(name);  
            }  
        }catch(ClassNotFoundException e){  
            // 4、如果父类加载器加载失败,则先抛出ClassNotFoundException  
        }  
        // 5、然后再调用自己的findClass()方法进行加载  
        if(c == null){  
            c = findClass(name);  
        }  
    }  
    if(resolve){  
        resolveClass(c);  
    }  
    return c;  
}
posted @ 2021-02-25 01:20  JeffreyHu  阅读(115)  评论(0编辑  收藏  举报