类加载器

类加载器

 

Java类加载器

 

  1. Bootstrap ClassLoader:根类加载器,负责加载java的核心类,它不是java.lang.ClassLoader的子类,而是由JVM自身实现;
  2. Extension ClassLoader:扩展类加载器,扩展类加载器的加载路径是JDK目录下jre/lib/ext,扩展类的getParent()方法返回null,实际上扩展类加载器的父类加载器是根加载器,只是根加载器并不是Java实现的;
  3. System ClassLoader:系统(应用)类加载器,它负责在JVM启动时加载来自java命令的-classpath选项、java.class.path系统属性或CLASSPATH环境变量所指定的jar包和类路径。程序可以通过getSystemClassLoader()来获取系统类加载器;

 

一。类加载器的过程

 

  七个步骤:

 类加载器

 

 

 

1、加载
  加载:查找并加载类的二进制数据的过程。
  加载的过程描述:

  通过类的全限定名定位.class文件,并获取其二进制字节流。
  把字节流所代表的静态存储结构转换为方法区的运行时数据结构。
  在Java堆中生成一个此类的java.lang.Class对象,作为方法区中这些数据的访问入口。
2、连接
  连接:包括验证、准备、解析三步。
  a). 验证
    验证:确保被加载的类的正确性。验证是连接阶段的第一步,用于确保Class字节流中的信息是否符合虚拟机的要求。具体验证形式:

    文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
    元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
    字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
    符号引用验证:确保解析动作能正确执行。
  b). 准备
    准备:为类的静态变量分配内存,并将其初始化为默认值。准备过程通常分配一个结构用来存储类信息,这个结构中包含了类中定义的成员变量,方法和接口信息等。

    具体行为:这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式赋值。

  c). 解析
    解析:把类中对常量池内的符号引用转换为直接引用。
    解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符等7类符号引用进行。

3、初始化
    初始化:对类静态变量赋予正确的初始值 (注意和连接时的解析过程区分开)。
二。类加载器的种类

  Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。 

  1.引导类加载器(bootstrap class loader):

    它用来加载 Java 的核心库(jre/lib/rt.jar),是用原生C++代码来实现的,并不继承自java.lang.ClassLoader。

    加载扩展类和应用程序类加载器,并指定他们的父类加载器,在java中获取不到。 

  2.扩展类加载器(extensions class loader):

    它用来加载 Java 的扩展库(jre/ext/*.jar)。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。 

  3.系统类加载器(system class loader):

    它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。

  4.自定义类加载器(custom class loader):

    除了系统提供的类加载器以外,开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。

 

以下测试代码可以证明此层次结构:

public class testClassLoader {
    @Test
    public void test(){
        //application class loader
        System.out.println(ClassLoader.getSystemClassLoader());
        //extensions class loader
        System.out.println(ClassLoader.getSystemClassLoader().getParent());
        //bootstrap class loader
        System.out.println(ClassLoader.getSystemClassLoader().getParent().getParent());
    }
}

 

输出为:

类加载器

 

 

 可以看出ClassLoader类是由AppClassLoader加载的。他的父亲是ExtClassLoader,ExtClassLoader的父亲无法获取是因为它是用C++实现的。

三。类加载机制(重点)

 

  类加载要做什么

 

    类的加载需要做以下三件事:

 

    •   通过类的全限定名获取此类的二进制字节流。

    •   将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

    •   在内存中生成一个此类的Class对象,作为方法区中类的各种数据访问入口。

 

 

  类的加载是由类加载器加载进内存的,类加载器虚拟机分为两大类,一类是启动类加载器,一类是其他加载器。但是如果细分的话,类加载器可以分为以下三类:

 

    •   启动类加载器(Bootstrap ClassLoader):这个类将器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。
      加载Java程序运行所需核心包下的类

    •   扩展类加载器(Extension ClassLoader):这个加载器sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
      加载java程序运行所需扩展包下的类

    •   应用程序类加载器(Application ClassLoader):这个类加载器由sun.misc.Launcher$App-ClassLoader实现。由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般也称它为系统类加载器。它负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。
      加载我们自己编写的类

 

    除了以上三种类加载器外,我们也可以实现自己的类加载器,只需要继承ClaasLoad类并重写loadClass方法即可。

  1.双亲委派机制

 

    双亲委派模型

 

    上面写的三个类加载器其实是有一定的层级关系的,但又不是简单的继承关系,而是通过组合模式实现的层级关系,除了*的启动类加载器外,每个加载器都要有自己的父类加载器,类加载器的这种模型就叫做双亲委派模型。

类加载器

 

 

 

      双亲委派机制

 

      虚拟机自1.2以后,开始提出一直类的加载机制,任何类加载器再接收到加载类请求时,会先请求自己的父类加载器先去加载类,如何父类加载器无法加载再向下传递。所以一个类的加载过程最多要经过一次向上传递请求和一次向下请求才能完成类的加载。

    双亲委派机制并不是一个强约束,只是虚拟机团队推荐使用双亲委派机制。

双亲委派模型的作用

  作用:保证JDK核心类的优先加载;

 

  

破双亲委派模型

 

  1. 自定义类加载器,重写loadClass方法;
  2. 使用线程上下文类加载器;

 

双亲委派模型破坏史

  1.第一次破坏

 

    由于双亲委派模型是在JDK1.2之后才被引入的,而类加载器和抽象类java.lang.ClassLoader则在JDK1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协。在此之前,用户去继承java.lang.ClassLoader的唯一目的就是为了重写loadClass()方法,因为虚拟机在进行类加载的时候会调用加载器的私有方法loadClassInternal(),而这个方法唯一逻辑就是去调用自己的loadClass()。

 

  2.第二次破坏

 

    双亲委派模型的第二次“被破坏”是由这个模型自身的缺陷所导致的,双亲委派很好地解决了各个类加载器的基础类的同一问题(越基础的类由越上层的加载器进行加载),基础类之所以称为“基础”,是因为它们总是作为被用户代码调用的API,但世事往往没有绝对的完美。

 

如果基础类又要调用回用户的代码,那该么办?

    一个典型的例子就是JNDI服务,JNDI现在已经是Java的标准服务

    它的代码由启动类加载器去加载(在JDK1.3时放进去的rt.jar),但JNDI的目的就是对资源进行集中管理和查找,它需要调用由独立厂商实现并部署在应用程序的ClassPath下的JNDI接口提供者的代码,但启动类加载器不可能“认识”这些代码。

 

    为了解决这个问题,Java设计团队只好引入了一个不太优雅的设计:线程上下文类加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的setContextClassLoader()方法进行设置,如果创建线程时还未设置,他将会从父线程中继承一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序类加载器。

 

    有了线程上下文加载器,JNDI服务就可以使用它去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载的动作,这种行为实际上就是打通了双亲委派模型层次结构来逆向使用类加载器,实际上已经违背了双亲委派模型的一般性原则,但这也是无可奈何的事情。Java中所有涉及SPI的加载动作基本上都采用这种方式,例如JNDI、JDBC、JCE、JAXB和JBI等。

 

  3.第三次破坏

 

    双亲委派模型的第三次“被破坏”是由于用户对程序动态性的追求导致的,这里所说的“动态性”指的是当前一些非常“热门”的名词:代码热替换、模块热部署等,简答的说就是机器不用重启,只要部署上就能用。
    OSGi实现模块化热部署的关键则是它自定义的类加载器机制的实现。每一个程序模块(Bundle)都有一个自己的类加载器,当需要更换一个Bundle时,就把Bundle连同类加载器一起换掉以实现代码的热替换。在OSGi幻境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为更加复杂的网状结构,当受到类加载请求时,OSGi将按照下面的顺序进行类搜索:
      1)将java.*开头的类委派给父类加载器加载。
      2)否则,将委派列表名单内的类委派给父类加载器加载。
      3)否则,将Import列表中的类委派给Export这个类的Bundle的类加载器加载。
      4)否则,查找当前Bundle的ClassPath,使用自己的类加载器加载。
      5)否则,查找类是否在自己的Fragment Bundle中,如果在,则委派给Fragment Bundle的类加载器加载。
      6)否则,查找Dynamic Import列表的Bundle,委派给对应Bundle的类加载器加载。
      7)否则,类加载器失败。

 

 

 

上一篇:JVM学习笔记之类装载器-ClassLoader


下一篇:jvm-learning-类加载器分类