Java Classloader原理分析

来源:互联网 发布:个人网站导航页源码 编辑:程序博客网 时间:2024/05/13 12:46

类的加载过程指通过一个类的全限定名来获取描述此类的二进制字节流,并将其转化为方法区的数据结构,进而生成一个java.lang.Class对象作为方法区这个类各种数据访问的入口。这个过程通过Java中的类加载器(ClassLoader)来完成。

  类装载器是用来把类(class)装载进JVM的JVM规范定义了两种类型的类装载器:启动内装载器(bootstrap)和用户自定义装载器(user-defined class loader)

一、Java默认提供的三个ClassLoader

JVM在运行时会产生三个ClassLoader:Bootstrap ClassLoader、Extension ClassLoader和AppClassLoader(System ClassLoader)。

1、 Bootstrap ClassLoader(启动类加载器)负责将%JAVA_HOME%/lib目录中或-Xbootclasspath中参数指定的路径中的,并且是虚拟机识别的(按名称)类库加载到JVM中。

     也可以通过-Xbootclasspath参数定义。该ClassLoader不能被Java代码实例化,因为它是JVM本身的一部分。

2、Extension ClassLoader(扩展类加载器)负责加载%JAVA_HOME%/lib/ext中的所有类库;

   只要jar包放置这个位置,就会被虚拟机加载。一个常见的、类似的问题是,你将mysql的低版本驱动不小心放置在这儿,但你的Web应用程序的lib下有一个新的jdbc驱动,但怎么都报错,譬如不支持JDBC2.0的 DataSource,这时你就要当心你的新jdbc可能并没有被加载。这就是ClassLoader的delegate现象。常见的有log4j、 common-log、dbcp会出现问题,因为它们很容易被人塞到这个ext目录,或是Tomcat下的common/lib目录

3、Application ClassLoader:也称为System ClassLoaer(加载%CLASSPATH%路径的类库)以及其它自定义的ClassLoader。缺省情况下,它是用户创建的任何ClassLoader的父ClassLoader。

    我们创建的standalone应用的main class缺省情况下也是由它加载(通过Thread.currentThread().getContextClassLoader()查看)。实际开发中用ClassLoader更多时候是用其加载classpath下的资源,特别是配置文件,如ClassLoader.getResource(),比FileInputStream直接。

类加载器 classloader 是具有层次结构的,也就是父子关系。其中,Bootstrap 是所有类加载器的父亲。如下图所示:

注意: 除了Java默认提供的三个ClassLoader之外,用户还可以根据需要定义自已的ClassLoader,而这些自定义的ClassLoader都必须继承自java.lang.ClassLoader类,也包括Java提供的另外二个ClassLoader(Extension ClassLoader和App ClassLoader)在内,但是Bootstrap ClassLoader不继承自ClassLoader,因为它不是一个普通的Java类,底层由C++编写,已嵌入到了JVM内核当中,当JVM启动后,Bootstrap ClassLoader也随着启动,负责加载完核心类库后,并构造Extension ClassLoader和App ClassLoader类加载器。

二、双亲委托模型 

Java中ClassLoader的加载采用了双亲委托机制,采用双亲委托机制加载类的时候采用如下的几个步骤:

1、当前ClassLoader首先从自己已经加载的类中查询是否此类已经加载,如果已经加载则直接返回原来已经加载的类;

2、当前classLoader的缓存中没有找到被加载的类的时候,委托父类加载器去加载,父类加载器采用同样的策略,首先查看自己的缓存,然后委托父类的父类去加载,一直到bootstrp ClassLoader.

3、当所有的父类加载器都没有加载的时候,再由当前的类加载器加载,并将其放入它自己的缓存中,以便下次有加载请求的时候直接返回。

  说到这里大家可能会想,Java为什么要采用这样的委托机制?理解这个问题,我们引入另外一个关于Classloader的概念“命名空间”, 它是指要确定某一个类,需要类的全限定名以及加载此类的ClassLoader来共同确定。也就是说即使两个类的全限定名是相同的,但是因为不同的 ClassLoader加载了此类,那么在JVM中它是不同的类。明白了命名空间以后,我们再来看看委托模型。采用了委托模型以后加大了不同的 ClassLoader的交互能力,比如上面说的,我们JDK本生提供的类库,比如hashmap,linkedlist等等,这些类由bootstrp 类加载器加载了以后,无论你程序中有多少个类加载器,那么这些类其实都是可以共享的,这样就避免了不同的类加载器加载了同样名字的不同类以后造成混乱。

    JVM中类加载的机制——双亲委派模型。这个模型要求除了Bootstrap ClassLoader外,其余的类加载器都要有自己的父加载器。子加载器通过组合来复用父加载器的代码,而不是使用继承。在某个类加载器加载class文件时,它首先委托父加载器去加载这个类,依次传递到顶层类加载器(Bootstrap)。如果顶层加载不了(它的搜索范围中找不到此类),子加载器才会尝试加载这个类。

     当JVM请求某个ClassLoader实例使用这种模型来加载某个类时,首先检查该类是否已经被当前类加载器加载,如果没有被加载,则先委托给她的父类加载器即调用parent.loadClass()方法,这样一直请求调用到请求顶层类加载ClassLoader#findBootStrapClassOrNull,如果这个方法依然加载不了,则会调用ClassLoader#findClass()方法,这个方法再找不到则会抛出ClassNotFoundException异常,但是这里的异常会被捕获,然后返回给委托发起者,最后由当前类加载器的findClass()方法类加载类,如果找不到则抛出ClassNotFoundException异常。

   Class查找的位置和顺序依次是:Cache、parent、self

 三、ClassLoader加载类的原理

1、原理介绍

    ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。当一个ClassLoader实例需要加载某个类时,它会试图亲自搜索某个类之前,先把这个任务委托给它的父类加载器,这个过程是由上至下依次检查的,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。否则将这个找到的类生成一个类的定义,并将它加载到内存当中,最后返回这个类在内存中的Class实例对象

2、为什么要使用双亲委托这种模型呢?

   因为这样可以避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

3、JVM在搜索类的时候,如何判断两个class相同呢?

   JVM在判定两个class是否相同时,不仅要判断两个类名是否相同,而且要判断是否由同一个类加载器实例加载的。只有两者同时满足的情况下,JVM才认为这两个class是相同的。就算两个class是同一份class字节码,如果被两个不同的ClassLoader实例所加载,JVM也会认为它们是两个不同class。

    比如网络上的一个Java类org.classloader.simple.NetClassLoaderSimple,javac编译之后生成字节码文件NetClassLoaderSimple.class,ClassLoaderA和ClassLoaderB这两个类加载器并读取了NetClassLoaderSimple.class文件,并分别定义出了java.lang.Class实例来表示这个类,对于JVM来说,它们是两个不同的实例对象,但它们确实是同一份字节码文件,如果试图将这个Class实例生成具体的对象进行转换时,就会抛运行时异常java.lang.ClassCaseException,提示这是两个不同的类型。

   在一个单虚拟机环境下,标识一个类有两个因素:class的全路径名、该类的ClassLoader。

4、ClassLoader 体系架构

1、先检查需要加载的类是否已经被加载,这个过程是从下->上;

2、如果没有被加载,则委托父加载器加载,如果加载不了,再由自己加载, 这个过程是从上->下

四、自定义ClassLoader

 为什么我们需要自定义类加载?

 主要原因:1、需要加载外部的Class,JVM提供的默认ClassLoader只能加载指定目录下的.jar和.class,如果我们想加载其它位置的class或者jar时,这些默认的类加载器是加载不到的(如果是文件格式必须配置到classpath)。例如:我们需要加载网络上的一个class字节流;

                2、需要实现Class的隔离性。目前我们常用的Web服务器,如tomcat、jetty都实现了自己定义的类加载,这些类加载主要完成以下三个功能:

                    A.实现加载Web应用指定目录下的jar和class

                    B.实现部署在容器中的Web应用程共同使用的类库的共享

                    C.实现部署在容器中各个Web应用程序自己私有类库的相互隔离

如何自定义类加载?

  • 继承java.lang.ClassLoader
  • 覆写父类的findClass()方法

   Java除了上面所说的默认提供的classloader以外,它还容许应用程序可以自定义classloader,那么要想自定义classloader我们需要通过继承java.lang.ClassLoader来实现,接下来我们就来看看再自定义Classloader的时候,我们需要注意的几个重要的方法:

1.loadClass 方法

loadClass method declare

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

 上面是loadClass方法的原型声明,上面所说的双亲委托机制的实现其实就实在此方法中实现的。下面我们就来看看此方法的代码来看看它到底如何实现双亲委托的。

loadClass method implement

public Class<?> loadClass(String name) throws ClassNotFoundException {  return loadClass(name, false);}

从上面可以看出loadClass方法调用了loadcClass(name,false)方法,那么接下来我们再来看看另外一个loadClass方法的实现。

Class loadClass(String name, boolean resolve)

复制代码
protected synchronized Class<?> loadClass(String name, boolean resolve)  throws ClassNotFoundException    {  // First, check if the class has already been loaded  Class c = findLoadedClass(name);//检查class是否已经被加载过了  if (c == null) {      try {      if (parent != null) {          c = parent.loadClass(name, false); //如果没有被加载,且指定了父类加载器,则委托父加载器加载。      } else {          c = findBootstrapClass0(name);//如果没有父类加载器,则委托bootstrap加载器加载      }      } catch (ClassNotFoundException e) {          // If still not found, then invoke findClass in order          // to find the class.          c = findClass(name);//如果父类加载没有加载到,则通过自己的findClass来加载。      }  }  if (resolve) {      resolveClass(c);  }  return c;}
复制代码

   上面的代码,通过注释可以清晰看出loadClass的双亲委托机制是如何工作的。 这里我们需要注意一点就是public Class<?> loadClass(String name) throws ClassNotFoundException没有被标记为final,也就意味着我们是可以override这个方法的,也就是说双亲委托机制是可以打破的。另外上面注意到有个findClass方法,接下来我们就来说说这个方法到底是做什么的。

2.findClass

 我们查看java.lang.ClassLoader的源代码,我们发现findClass的实现如下:

 protected Class<?> findClass(String name) throws ClassNotFoundException {     throw new ClassNotFoundException(name);  }

  我们可以看出此方法默认的实现是直接抛出异常,其实这个方法就是留给我们应用程序来override的。那么具体的实现就看你的实现逻辑了,你可以从磁盘读取,也可以从网络上获取class文件的字节流,获取class二进制了以后就可以交给defineClass来实现进一步的加载。defineClass我们再下面再来描述。通过上面的分析,我们可以得出如下结论:

3.defineClass

 我们首先还是来看看defineClass的源码:

 defineClass

protected final Class<?> defineClass(String name, byte[] b, int off, int len)  throws ClassFormatError{      return defineClass(name, b, off, len, null);}

 从上面的代码我们看出此方法被定义为了final,这也就意味着此方法不能被Override,其实这也是jvm留给我们的唯一的入口,通过这个唯 一的入口,jvm保证了类文件必须符合Java虚拟机规范规定的类的定义。此方法最后会调用native的方法来实现真正的类的加载工作。

五、不遵循“双亲委托机制”的场景

   上面说了双亲委托机制主要是为了实现不同的ClassLoader之间加载的类的交互问题,被大家公用的类就交由父加载器去加载,但是Java中确实也存在父类加载器加载的类需要用到子加载器加载的类的情况。下面我们就来说说这种情况的发生。

   Java中有一个SPI(Service Provider Interface)标准,使用了SPI的库,比如JDBC,JNDI等,我们都知道JDBC需要第三方提供的驱动才可以,而驱动的jar包是放在我们应用程序本身的classpath的,而jdbc 本身的api是jdk提供的一部分,它已经被bootstrp加载了,那第三方厂商提供的实现类怎么加载呢?这里面JAVA引入了线程上下文类加载的概 念,线程类加载器默认会从父线程继承,如果没有指定的话,默认就是系统类加载器(AppClassLoader),这样的话当加载第三方驱动的时候,就可 以通过线程的上下文类加载器来加载。
另外为了实现更灵活的类加载器OSGI以及一些Java app server也打破了双亲委托机制。

另:启动时如果加上如下系统参数,即可跟踪JVM类的加载

    -XX:+TraceClassLoading