Java高并发学习笔记(三):类加载

1 来源

  • 来源:《Java高并发编程详解 多线程与架构设计》,汪文君著
  • 章节:第9、10、十一章

本文这三章的笔记整理。java

2 类加载简介

类加载的过程能够简单分为三个阶段:sql

  • 加载阶段:主要负责查找而且加载类的二进制数据文件
  • 链接阶段:能够细分为验证、准备、解析三个阶段,验证就是确保类文件的正确性,准备就是为类的静态变量分配内存,而且为其初始化默认值,解析就是把类中的符号引用转换为直接引用
  • 初始化阶段:为类的静态变量赋予正确的初始值

3 主动使用与被动使用

JVM规范规定了每一个类或接口在首次主动使用的时候都须要进行初始化,规定了如下六种主动使用类的场景:数据库

  • 经过new关键字会致使类的初始化
  • 访问类的静态变量
  • 访问类的静态方法
  • 对某个类进行反射操做
  • 初始化子类会致使父类初始化
  • 启动类(就是包含main()的类)也会初始化

除了以上六种状况外,其他的都叫被动使用,不会致使类的加载和初始化,好比引用类的静态常量不会致使类的初始化。编程

4 类加载详解

前面也说了类加载能够简单分为三个阶段:安全

  • 加载阶段
  • 链接阶段
  • 初始化阶段

下面先来看一下加载阶段。网络

4.1 加载阶段

加载阶段就是将class文件中的二进制数据读取到内存之中,而后将该字节流表明的静态存储结构转换为方法区中运行时数据结构,而且在堆中生成一个该类的java.lang.Class对象,做为访问方法区数据结构的入口。数据结构

类加载的最终产物就是堆内存中的class对象,JVM规范中指出类加载是经过一个全限定名去获取二进制数据流,来源包括:多线程

  • class文件:这是最多见的格式,就是加载javac编译后的字节码文件
  • 运行时动态生成:好比ASM能够动态生成,或者能够经过动态代理java.lang.Proxy生成等
  • 经过网络获取:好比RMI
  • 读取压缩文件:好比JARWAR
  • 从数据库读取:好比读取MySQL中的BLOB字段类型的数据
  • 运行时生成class文件而且动态加载:好比ThriftAvro等序列化框架,将某个schema生成若干个class文件并进行加载

类加载阶段结束后,JVM会将这些二进制字节流按照JVM定义的格式存放在方法区中,造成特定的数据结构后再在堆内存中实例化一个java.lang.Class对象。架构

4.2 链接阶段

该阶段能够分为三个小阶段:并发

  • 验证
  • 准备
  • 解析

须要注意的是这三个小阶段其实不是顺序进行的,而是交叉着进行的,也就是解析的时候其实也会有验证的过程。

4.2.1 验证

验证是为了确保字节流所包含的内容符合JVM规范,而且不会出现危害JVM自身安全的代码,当字节流信息不符合要求的时候,会抛出VerifyError这样的异常或其子异常,验证的信息包括:

  • 文件格式
  • 元数据
  • 字节码
  • 符号引用

4.2.1.1 验证文件格式

包括:

  • 魔数(0xCAFEBABE
  • 主次版本号
  • 是否存在残缺或附加信息
  • 常量池常量类型是否支持
  • 常量池引用是否指向不存在常量或不支持类型常量
  • 其余

4.2.1.2 验证元数据

元数据验证实际上是进行语义分析的过程,语义分析是为了确保字节流符合JVM规范要求,包括:

  • 检查某个类是否存在父类,是否继承某个接口,这些父类或接口是否合法,或是否存在
  • 检查是否继承了final的类
  • 检查抽象类,检查是否实现了父类的抽象方法或接口方法
  • 检查重载,好比相同的方法名称、相同的参数可是返回类型不一样,这是不容许的

4.2.1.3 验证字节码

字节码验证主要是验证程序的控制流程,包括:

  • 保证当前线程在程序计数器中的指令不会跳转到不合法的字节码指令中去
  • 保证类型的转换是合法的
  • 保证任意时刻虚拟机栈中的操做栈类型与指令代码都能正确被执行
  • 其余验证

4.2.1.4 验证符号引用

验证符号引用转换为直接引用的合法性,保证解析动做的顺利执行,包括:

  • 经过符号引用描述的字符串全限定名称是否可以顺利找到相关的类
  • 符号引用中的类、字段、方法是否对当前类可见
  • 其余

4.2.2 准备

通过验证后,就开始了准备阶段,这阶段比较简单,就是对对象的静态变量分配内存而且设置初始值,类变量的内存会被分配到方法区中。设置初始值就是为相应的类变量给定一个相关类型在没有被设置时的默认值,好比Int的初始值为0,引用的初始值为null

4.2.3 解析

解析就是在常量池中寻找类、字段、接口和方法的符号引用,而且将这些符号引用替换成直接引用的过程。解析主要针对类接口、字段、类方法和接口方法进行的,包括:

  • 类接口解析
  • 字段解析
  • 类方法解析
  • 接口方法解析

4.3 初始化阶段

初始化阶段主要就是执行<clinit>方法的过程,该方法是编译阶段生成的,也就是说包含在字节码文件中,该方法包含了全部类变量的赋值动做和静态语句块的执行代码。另外一方面,<clinit>与构造方法不一样,不须要显式调用父类构造器,虚拟机会保证父类的<clinit>方法最早执行。

还须要注意的是<clinit>只能被虚拟机执行,虚拟机还会保证多线程下的安全性,所以,若是在静态代码块中若是包含了加载其余类的操做可能会引发死锁,例子能够看这里

5 类加载器

5.1 JVM中的三类核心类加载器

JVM中有三类核心类加载器,分别是:

  • 启动类加载器:启动类加载器是最顶层的类加载器,没有父加载器,由C++编写,负责JVM核心类库的加载,好比加载整个java.lang包中的类
  • 扩展类加载器:扩展类加载器的父加载器是启动类加载器,主要加载jre/lib/ext子目录下的类库,纯Java实现,是URLClassLoader的子类
  • 应用类加载器:也叫系统类加载器,负责加载classpath下的类库,应用类加载器的父加载器为扩展类加载器,同时它也是自定义类加载器的默认父加载器

5.2 双亲委派机制

一个类加载器加载一个类的时候,并不会尝试直接加载该类,而是先交给父加载器尝试加载,一直到顶层的父加载器(启动类加载器),若是父加载器加载失败,则会本身尝试加载,图示以下:

在这里插入图片描述

6 线程上下文类加载器

JDK中提供了不少SPIService Provider Interface),好比JDBC等,JDBC只规定了这些接口之间的逻辑关系,但不提供具体的实现,换句话说,JDBC彻底透明了应用程序和第三方厂商数据库驱动的具体实现,应用程序只须要面向接口编程便可。但问题是:

  • java.lang.sql中的全部接口都是由JDK提供的,加载这些接口的类加载器是启动类加载器
  • 第三方厂商的类库驱动由系统类加载器加载

因为双亲委派机制,ConnectionsStatement等都是由启动类加载器加载,而第三方JDBC驱动包中的实现不会被加载。解决这个问题的关键,就是使用了线程上下文类加载器打破了双亲委派机制。

好比MySQL驱动的加载过程,就是经过线程上下文类加载器加载的,

private static Connection getConnection(String url, Properties info, Class<?> caller) throws SQLException {
        //...
        if (callerCL == null || callerCL == ClassLoader.getPlatformClassLoader()) {
            callerCL = Thread.currentThread().getContextClassLoader();
        }
        while(true) {
            //...
            if (isDriverAllowed(aDriver.driver, callerCL)) {
            }
        }
        //...
}
private static boolean isDriverAllowed(Driver driver, ClassLoader classLoader) {
    //...
    try {
        aClass = Class.forName(driver.getClass().getName(), true, classLoader);
    } catch (Exception var5) {
        result = false;
    }
    //...
    return result;
}

经过线程上下文类加载器,就变成了启动类加载器去委托子类加载器去加载实现的方式,也就是JDK本身亲自打破了双亲委派机制这种方式,这种加载方式几乎涉及全部的SPI加载,包括JAXBJCEJBI等。

相关文章
相关标签/搜索