JVM 类加载机制及双亲委派模型

10,377 阅读10分钟

概述

 java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这就是虚拟机的类加载机制。
 
  上面这段是《深入理解java虚拟机》中对类加载的描述,其实简单点说就是程序从最开始的.java文件到.Class文件,Class文件中包java虚拟机指令集和符号表以及其他辅助信息,而这些信息最终加载到虚拟机才能被使用。接下来我们就一起讨论这些Class文件如何被加载以及被加载后变成了什么。

类的生命周期

如上图所示,描述了类的生命周期。其中加载、验证、准备、初始化、卸载这五个动作是存在先后顺序的,而解析阶段有可能在初始化之后完成的。这些动作中通常都是互相交叉混合进行的。下面我们主要探讨加载、验证、准备、解析、初始化这五个步骤。

加载

  • 何时加载

    1.预加载:在虚拟机启动的时候加载,加载的是JAVA_HOME/lib/下的rt.class下的.class文件,是java程序运行时经常要用到的一些类,比如java.lang.⁎以及 java.util.⁎等
    2.运行时加载:虚拟机在用到一个.class文件时,首先会去内存中查找这个.class文件有没有被加载,没有被加载会根据这个类的全限定名去加载。
    
  • 加载阶段虚拟机做了什么

    1. 通过一个类的全限定名获取定义此类的二进制字节流。
    2. 将这个字节流所代表的静态存储结构转化为方法区的运行数据结构。
    3. 在内存中生成一个唯一代表此类的java.lang.Class对象,作为方法区中这个类的各种数据的访问入口。(一般这个class对象会存储在堆中,不过HotSpot虚拟机比较特殊,这个Class对象是放在方法区中的。)
    
虚拟机对上述三点的要求并不算具体,例如第一条,根本没指明二进制字节流从哪里来,怎么来,包括以下几点:
     从zip包中获取,这就是以后jar、ear、war格式的基础
     从网络中获取,典型应用就是Applet
     运行时计算生成,典型应用就是动态代理技术
     由其他文件生成,典型应用就是JSP,即由JSP生成对应的.class文件
     从数据库中读取,这种场景比较少见...

验证

 顾名思义,是对Class文件字节流的验证,而验证的目的则是为了确保当前的Class文件符合java虚拟机的要求,并且不会危害虚拟机自身的安全。通常主要包括以下几点验证内容:
   1. 文件格式验证
      其实就是验证字节流是否符合Class文件规范,符合规范通过验证才能保证输入的字节流能正确的被解析并存储到方法区。
   2. 元数据验证
      对类的元数据信息进行语义校验。
   3. 字节码验证
    最为复杂的校验阶段,校验程序语义是否符合规范,符合逻辑,对类的方法体进行校验。  
   4. 符号引用验证
     发生在将符号引用转换为直接引用的时候,可以看做是对类自身以外(常量池中各种符号的应用)信息的匹配校验,如:符号引用中通过字符串描述的全限定名是否能找到对应的类;符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被 当前类访问...

准备

 正式为类变量分配内存并赋初值。
 需要注意两点
   1. 只为类变量,即被static修饰的变量分配内存,实例变量在实例初始化的时候会随对象一起分配在堆中。
   2. 这个阶段赋初始值的变量指的是那些不被final修饰的static变量,比如”public static int value = 123;”,value在准备阶段过后是0而不是123,给value赋值为123的动作将在初始化阶段才进行;比如”public static final int value = 123;”就不一样了,在准备阶段,虚拟机就会给value赋值为123。
   基本数据的零值如下表:

解析

 解析是虚拟机将常量池中的符号引用转换为直接引用的过程。
   1. 符号引用
   这个其实是属于编译原理方面的概念,符号引用包括了下面三类常量:
     类和接口的全限定名
     字段的名称和描述符
     方法的名称和描述符

看到Constant Pool也就是常量池中有22项内容,其中带”Utf8″的就是符号引用。比如#2,它的值是”com/xrq/test6/TestMain”,表示的是这个类的全限定名;又比如#5为i,#6为I,它们是一对的,表示变量时Integer(int)类型的,名字叫做i;#6为D、#7为d也是一样,表示一个Double(double)类型的变量,名字为d;#18、#19表示的都是方法的名字。

那其实总而言之,符号引用和我们上面讲的是一样的,是对于类、变量、方法的描述。符号引用和虚拟机的内存布局是没有关系的,引用的目标未必已经加载到内存中了。

   2. 直接引用
     直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引用在不同的虚拟机示例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经存在在内存中了。

初始化

 开始真正的执行类中定义的java代码,初始化过程就是执行类构造器<clinit>()的过程,还记得之前的准备阶段是给类变量分配内存赋初值,这里就是将类变量赋予用户指定的值
 。
 虚拟机规范定义了“有且仅有”5中会触发初始化的场景:
    1. 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果累没有进行初始化,则要先触发初始化;
    2. 使用java.lang.reflect包中的方法对类进行反射调用的时候;
    3. 初始化类时,若发现其父类还没有初始化,则先触发父类的初始化;
    4. 虚拟机启动的时候,虚拟机会先初始化用户指定的包含main()方法的那个类
    5. 当使用JDK 1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。

双亲委派模型

 介绍双亲委派模型之前先说下类加载器。对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立在jvm中的唯一性,每一个类加载器,都有一个独立的类名称空间。类加载器就是根据指定全限定名称将class文件加载到JVM内存,转为Class对象。
 从jvm角度来看只存在两种类加载器
  • 启动类加载器(Bootstrap ClassLoader),是虚拟机自身的一部分,用来加载JAVA_HOME/lib/目录中的,或者被-Xbootclasspath参数所指定的路径中并且被虚拟机识别的类库。

  • 其他类加载器:由Java语言实现,继承自抽象类ClassLoader:

    1. 扩展类加载器(Extension ClassLoader):负责加载<JAVA_HOME>\lib\ext目录或java.ext.dirs系统变量指定的路径中的所有类库。
    2. 应用程序类加载器(Application ClassLoader)。负责加载用户类路径(classpath)上的指定类库,我们可以直接使用这个类加载器。一般情况,如果我们没有自定义类加载器默认就是用这个加载器。

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去加载这个类,而是把这个请求委派给父类加载器去完成,每一层的类加载器都是如此,这样所有的加载请求都会被传送到顶层的启动类加载器中,只有当父加载无法完成加载请求(它的搜索范围中没找到所需的类)时,子加载器才会尝试去加载类。下面举一个大家都知道的例子说明为什么要使用双亲委派模型。

黑客自定义一个java.lang.String类,该String类具有系统的String类一样的功能,只是在某个函数稍作修改。比如equals函数,这个函数经常使用,如果在这这个函数中,黑客加入一些“病毒代码”。并且通过自定义类加载器加入到JVM中。此时,如果没有双亲委派模型,那么JVM就可能误以为黑客自定义的java.lang.String类是系统的String类,导致“病毒代码”被执行。

而有了双亲委派模型,黑客自定义的java.lang.String类永远都不会被加载进内存。因为首先是最顶端的类加载器加载系统的java.lang.String类,最终自定义的类加载器无法加载java.lang.String类。

或许你会想,我在自定义的类加载器里面强制加载自定义的java.lang.String类,不去通过调用父加载器不就好了吗?确实,这样是可行。但是,在JVM中,判断一个对象是否是某个类型时,如果该对象的实际类型与待比较的类型的类加载器不同,那么会返回false。

举个简单例子:

ClassLoader1、ClassLoader2都加载java.lang.String类,对应Class1、Class2对象。那么Class1对象不属于ClassLoad2对象加载的java.lang.String类型。

如何实现双亲委派模型

    双亲委派模型的原理很简单,实现也简单。每次通过先委托父类加载器加载,当父类加载器无法加载时,再自己加载。其实ClassLoader类默认的loadClass方法已经帮我们写好了,我们无需去写。

破坏双亲委派模型

 双亲委派模型并不是一个强制性约束,而是java设计者推荐给开发者的类加载器的实现方式,在一定条件下,为了完成某些操作,可以“破坏”模型。
     1.重新loadClass方法
     2.利用线程上下文加载器(Thread Context ClassLoader)。这个类加载器可以通过java.lang.Thread类的 setContextClassLoaser()方法进行设置,如果创建线程时还未设置,它将会从父线程中继承 一个,如果在应用程序的全局范围内都没有设置过的话,那这个类加载器默认就是应用程序 类加载器。
     3.为了实现热插拔,热部署,模块化,意思是添加一个功能或减去一个功能不用重启,只需要把这模块连同类加载器一起换掉就实现了代码的热替换。

本文参考《深入理解java虚拟机》

[深度分析Java的ClassLoader机制(源码级别)](http://www.hollischuang.com/archives/199)

[Java类的加载、链接和初始化](http://www.hollischuang.com/archives/201)