JVM类加载机制

89 阅读8分钟

类加载过程

类从被加载到虚拟机内存(方法区)到卸载内存为止,经历了以下几个阶段,加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using)、卸载(Unloading),其中验证、准备、解析三个部分成为链接(link):

load.png

如图,其中加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段再开始,这是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。

  1. 加载

“加载”是“类加载”过程的一个阶段,在加载阶段,虚拟机需要完成以下3个步骤:

1.1 通过一个类的全限定名来获取定义此类的二进制字节流;
1.2 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
1.3 在内存中生成一个代表这个类的Java.lang.Class对象,作为方法区这个类的各种数据的访问入口;

  1. 验证

验证时链接阶段的第一个步,目的是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全;验证阶段有个4个检验动作:文件格式验证、元数据验证、字节码验证、符号引用验证:

2.1 文件格式验证

这一阶段主要验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理:

  • 是否以魔数0xCAFEBABE开头
  • 主、次版本号是否在当前虚拟机处理范围之内
  • 常量池的常量中是否有不被支持的常量类型(检验常量tag标志)
  • 指向常量的各种索引值是否有指向不存在的常量或不符合类型的常量
  • CONSTANT_Utf8_info型的常量中是都有不符合UTF8编码的数据
  • Class文件中各个部分及文件本身是否有被删除的或附加的其他信息

2.2 元数据验证

第二阶段是对字节码描述的信息进行语义分析,以保证其描述的信息符合java语言规范的要求:

  • 这个类是否有父类(除了java.lang.Object之外,所有类都应当有父类)
  • 这个类的父类是否继承了不允许被继承的类(被final修饰的类)
  • 如果这个类不是抽象类,是否实现了其父类或接口中要求实现的所有方法
  • 类中的字段、方法是否与父类产生矛盾(例如覆盖了父类的final字段,或者出现不符合规则的方法重载,例如方法参数都一致,却返回值类型不同等)

2.3 字节码验证

第三阶段是整个验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。

  • 保证任意时刻操作数栈的数据类型与指令代码序列痘能配合工作,例如不会出现类似的这样的情况:在操作栈放置了一个int类型的数据,使用时却按long类型来加载入本地变量表中;
  • 保证跳转指令不会跳转到方法体以外的字节码指令上;
  • 保证方法体中的类型转换是有效的,例如可以把一个子类对象赋值给父类数据类型,这是安全的,但是把父类对象赋值给子类数据类型,甚至把对象赋值给与它毫无继承关系,完全不相干的一个数据类型,则是危险和不合法的。

2.4 符号引用验证

最后一个阶段的检验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在链接的第三阶段——解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验:

  • 符号引用中通过字符串描述的全限定名是否能找到对应的类
  • 在指定类中是否存在符合方法的字段描述符以及简单名称所能描述的方法和字段
  • 符号引用中的类、字段、方法的访问性(private、protected、public、default)是否可被当前类访问
  1. 准备

准备阶段是为类变量正式分配内存并设置变量初始值的阶段,仅包含类变量,被static修饰的变量,不包含实例变量,如:

public static int value = 123

要将value赋值为123的动作将在初始化阶段才执行; 如果类字段的字段属性表中存在ConstantValue属性,在准备阶段就会被初始化为指定的值:

public static final int value = 123;
  1. 解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程,解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行;

  • 类或接口的解析
  • 字段解析
  • 类方法解析
  • 接口方法解析
  1. 初始化

类初始化阶段是类加载过程的最后一步,初始化阶段是执行类构造器()方法的过程;

双亲委派模型

从虚拟机的角度看,只存在两种不同的类加载器:一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分;另一种就是所有其他的类加载器,这些类加载器都由Java语言实现,独立于虚拟机外部,并且全都继承自抽象类java.lang.ClassLoader

从开发人员的角度看,类加载器划分为:

  • 启动类加载器(Bootstrap ClassLoader):这个类加载器将负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如:rt.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被java程序直接引用,用户在编写自定义类加载器时,如果需要加载请求委派给引导类加载器,那直接使用null代替即可;

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

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

shuangqin.png

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载。

java.lang.ClassLoader中的loadClass(),先检查是否已经被加载过,若没有加载则调用父加载器的loadClass()方法,若父加载器为空则默认使用启动类加载器作为父加载器,如果父类加载器失败,抛出ClassNotFoundException异常后,再调用自己的findClass()方法进行加载。


protected Class<?> loadClass(String name, boolean resolve)
    throws ClassNotFoundException
{
    synchronized (getClassLoadingLock(name)) {
        // First, check if the class has already been loaded
        Class<?> c = findLoadedClass(name);
        if (c == null) {
            long t0 = System.nanoTime();
            try {
                if (parent != null) {
                    c = parent.loadClass(name, false);
                } else {
                    c = findBootstrapClassOrNull(name);
                }
            } catch (ClassNotFoundException e) {
                // ClassNotFoundException thrown if class not found
                // from the non-null parent class loader
            }

            if (c == null) {
                // If still not found, then invoke findClass in order
                // to find the class.
                long t1 = System.nanoTime();
                c = findClass(name);

                // this is the defining class loader; record the stats
                sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
                sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
                sun.misc.PerfCounter.getFindClasses().increment();
            }
        }
        if (resolve) {
            resolveClass(c);
        }
        return c;
    }
}

破坏双亲委派 如:TOMCAT

  • 第一次被破坏发生在JDK1.2之前
    由于双亲委派模型在JDK1.2之后才引入,而类加载器和抽象类java.lang.ClassLoader则在JDK1.0时代就已经存在,面对已经存在的用户自定义类加载器的实现代码,Java设计者引入双亲委派模型时不得不做出一些妥协;

  • JNDI JDBC等需要加载器PI接口实现类的情况

  • 实现热部署插件

  • tomcat 等Web容器的出现

  • OSGI、Jigsaw等模块化技术的使用

Tomcat的类加载机制:为了实现隔离性,优先加载 Web 应用自己定义的类,所以没有遵照双亲委派的约定,每一个应用自己的类加载器——WebAppClassLoader负责加载本身的目录下的class文件,加载不到时再交给CommonClassLoader加载,这和双亲委派刚好相反。