java面试之ClassLoader类加载器

1.1、概念

类加载器(class loader)用来加载 Java 类到 Java 虚拟机中。一般来说,Java虚拟机使用 Java 类的方式如下:Java 源程序(.java 文件)在经过 Java 编译器编译之后就被转换成 Java 字节代码(.class 文件)。类加载器负责读取Java字节代码,并转换成 java.lang.Class类的一个实例。每个这样的实例用来表示一个Java 类。通过此实例的 newInstance()方法就可以创建出该类的一个对象。

1.2、类加载器结构

Java 中的类加载器大致可以分成两类:

一类是系统提供的:

  • 引导类加载器(bootstrapclass loader):它用来加载 Java 的核心库,是用原生代码而不是java来实现的,并不继承自java.lang.ClassLoader,除此之外基本上所有的类加载器都是java.lang.ClassLoader类的一个实例
  • 扩展类加载器(extensionsclass loader):它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录(一般为%JRE_HOME%/lib/ext)。该类加载器在此目录里面查找并加载 Java 类。
  • 系统类加载器(systemclass loader或 App class loader):它根据当前Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader() 来获取它。

另外一类则是由 Java 应用开发人员编写的:

  • 开发人员可以通过继承java.lang.ClassLoader 类的方式实现自己的类加载器,以满足一些特殊的需求

1.3、双亲委托模型(Parentdelegation模型)

从1.2版本开始,Java引入了双亲委托模型,从而更好的保证Java平台的安全。在此模型下,当一个装载器被请求装载某个类时,它首先委托自己的parent去装载,若parent能装载,则返回这个类所对应的Class对象,若parent不能装载,则由parent的请求者去装载。(为什么更加安全?因为在此模型下用户自定义的类装载器不可能装载应该由父亲装载器装载的可靠类,从而防止不可靠甚至恶意的代码代替由父亲装载器装载的可靠代码。)

具体示例:

假如loader2的parent为loader1,loader1的parent为system class loader。假设loader2被要求装载类MyClass,在parent delegation模型下,loader2首先请求loader1代为装载,loader1再请求系统类装载器去装载MyClass。若系统装载器能成功装载,则将MyClass所对应的Class对象的reference返回给loader1,loader1再将reference返回给loader2,从而成功将类MyClass装载进虚拟机。若系统类装载器不能装载MyClass,loader1会尝试装载MyClass,若loader1也不能成功装载,loader2会尝试装载。若所有的parent及loader2本身都不能装载,则装载失败。

1.4、加载类过程

类加载器的双亲委托模式会首先代理给其它类加载器来尝试加载某个类,这就意味着真正完成类的加载工作的类加载器和启动这个加载过程的类加载器,有可能不是同一个。真正完成类的加载工作是通过调用 defineClass来实现的;而启动类的加载过程是通过调用 loadClass来实现的。前者称为一个类的定义加载器(definingloader),后者称为初始加载器(initiatingloader)。 Java虚拟机判断两个类是否相同的时候,使用的是类的定义加载器。也就是说,哪个类加载器启动类的加载过程并不重要,重要的是最终定义这个类的加载器。两种类加载器的关联之处在于:一个类的定义加载器是它引用的其它类的初始加载器。如类 com.example.Outer 引用了类 com.example.Inner,则由类 com.example.Outer 的定义加载器负责启动类 com.example.Inner 的加载过程。

类加载器在成功加载某个类之后,会把得到的 java.lang.Class 类的实例缓存起来。下次再请求加载该类的时候,类加载器会直接使用缓存的类的实例,而不会尝试再次加载。也就是说,对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass 方法不会被重复调用。

1.5、java.lang.ClassLoader抽象类

几乎所有的类加载器都是继承自java.lang.ClassLoader类,所以有必要看一下ClassLoader中几个比较重要的方法:

  • 供用户调用接口,加载制定名称类(二进制)

public Class<?> loadClass(String name)throws ClassNotFoundException{//…}

protected synchronized Class<?>loadClass(String name,boolean resolve)throws ClassNotFoundException{//…}

  • 被loadClass方法调用去加载指定名称类:

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

  •  一般在findClass方法中读取到对应字节码后调用来定义类型(无需重写):

protected finalClass<?> defineClass(String name,byte[] b,intoff,intlen) throws ClassFormatError{//…}

1.6、通过ClassLoader方法分析双亲委派机制

public Class<?> loadClass(String name)throws ClassNotFoundException{

    return loadClass(name,false);

 }

protectedsynchronized Class<?>loadClass(String name,boolean resolve)throws ClassNotFoundException {       

    //首先判断该类型是否已经被加载       

    Class c = findLoadedClass(name);       

    if (c ==null) {

        //如果没有被加载,就委托给父类加载或者委派给启动类加载器加载           

        try {

            if (parent !=null) {

                //如果存在父类加载器,就委派给父类加载器加载                    

                c =parent.loadClass(name,false);                

            }else {

                //如果不存在父类加载器,就检查是否是由启动类加载器加载的类,通过调用本地方法native ClassfindBootstrapClass(String name)

                c = findBootstrapClass0(name);

            }

        }catch (ClassNotFoundExceptione) {

            //如果父类加载器和启动类加载器都不能完成加载任务,才调用自身的加载功能                

            c = findClass(name);            

        }       

     }

    if (resolve) {

        resolveClass(c);        

    }

    return c;   

 }

一般来说,自己开发的类加载器只需要覆写findClass(String name) 方法即可。java.lang.ClassLoader 类的方法 loadClass() 封装了前面提到的代理模式的实现。该方法会首先调用 findLoadedClass() 方法来检查该类是否已经被加载过;如果没有加载过的话,会调用父类加载器的 loadClass() 方法来尝试加载该类;如果父类加载器无法加载该类的话,就调用 findClass() 方法来查找该类。因此,为了保证类加载器都正确实现代理模式,在开发自己的类加载器时,最好不要覆写 loadClass() 方法,而是覆写 findClass()方法。

1.7、线程上下文类加载器

前面提到的类加载器的代理模式并不能解决Java 应用开发中会遇到的类加载器的全部问题。Java 提供了很多服务提供者接口(Service Provider Interface,SPI),允许第三方为这些接口提供实现。常见的 SPI 有 JDBC和JNDI等。这些 SPI 的接口由 Java 核心库来提供,但是这些接口的实现很可能是作为引入的jar包被包含进来的。SPI接口中的代码经常需要加载具体的实现类,问题就出现了:SPI的接口是Java核心库的一部分,由引导类加载器来加载,而SPI的实现类一般由系统类加载器加载的,根据双亲委托模型机制,因为引导类加载器是系统类加载器的祖先类加载器,所以引导类加载器加载SPI接口类之后无法找到SPI的实现类。

线程上下文类加载器正好解决了这个问题。如果不做任何的设置,Java 应用的线程的上下文类加载器默认就是系统上下文类加载器。在 SPI 接口的代码中使用线程上下文类加载器,就可以成功的加载到 SPI 实现的类。线程上下文类加载器在很多 SPI 的实现中都会用到。

1.8、类加载器与Web容器

对于运行在 Java EE 容器中的 Web 应用来说,类加载器的实现方式与一般的 Java 应用有所不同。不同的 Web 容器的实现方式也会有所不同。以 Apache Tomcat 来说,每个 Web 应用都有一个对应的类加载器实例。该类加载器也使用代理模式,所不同的是它是首先尝试去加载某个类,如果找不到再代理给父类加载器。这与一般类加载器的顺序是相反的。这是 Java Servlet 规范中的推荐做法,其目的是使得 Web 应用自己的类的优先级高于 Web 容器提供的类。这种代理模式的一个例外是:Java 核心库的类是不在查找范围之内的。这也是为了保证 Java 核心库的类型安全。

1.9、其他加载类的方法——Class.forName

Class.forName 是一个静态方法,同样可以用来加载类。该方法有两种形式:Class.forName(String name, boolean initialize, ClassLoader loader) 和Class.forName(String className)。第一种形式的参数 name 表示的是类的全名;initialize 表示是否初始化类;loader 表示加载时使用的类加载器。第二种形式则相当于设置了参数 initialize 的值为 true,loader 的值为当前类的类加载器。Class.forName 的一个很常见的用法是在加载数据库驱动的时候。如Class.forName("org.apache.derby.jdbc.EmbeddedDriver").newInstance() 用来加载 ApacheDerby 数据库的驱动。

1.10、总结图



  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值