文章目录
  1. 1. 类加载机制
    1. 1.1. 开发环境
    2. 1.2. Class加载流程
      1. 1.2.1. 加载
      2. 1.2.2. 验证
      3. 1.2.3. 准备
      4. 1.2.4. 解析
      5. 1.2.5. 初始化
    3. 1.3. 类加载器
      1. 1.3.1. ClassLoader的重要方法
      2. 1.3.2. Java中的类加载器
      3. 1.3.3. 双亲委派模式
    4. 1.4. 破坏双亲委派模式

类加载机制

本文将介绍 class装载验证过程什么是类装载器ClassLoaderJDK中ClassLoader默认设计模式以及热替换

开发环境


Class加载流程

类装载过程分为加载、链接、初始化三个过程,其中验证、准备、解析3个部分统称为连接,具体如下图:

类的生命周期

加载、验证、准备、初始化和卸载这5个阶段的顺序是确定的,也就是这些阶段是有依赖性的。

加载

加载是类加载过程的一个阶段,主要完成三件事情:

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

验证

验证是连接的第一步,这一阶段的目的是为了确保CLass文件的字节流中包含的信息符合当前虚拟机的要求。分为四个步骤:

  • 文件格式验证
    • 文件格式验证包含:是否以魔数0xCAFFBABE开头、主、次版本号是否在当前虚拟机处理范围之内等
    • 该阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。
  • 元数据验证
    • 元数据验证点:这个类是否有父类(除了jva.lang.Object之外,所有的类应当有父类)、这个类的父类是否继承了不允许被继承的类(被final修饰的类)等
    • 该阶段的目的是对类的元数据信息进行语义校验
  • 字节码验证(非常复杂)
    • 该阶段主要目的是通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
  • 符号引用验证

    • 常量池中描述类是否存在
    • 访问性(private、protected、public、default)是否可以被当前类访问
    • 如果无法通过符号引用验证,则会抛出java.lang.IncompatibleClassChangError异常的子类,如java.lang.IllegalAccessErrorjava.lang.NoSuchFieldErrorjava.lang.NoSuchMethodError

准备

  • 正式为类变量分配内存并设置类变量初始值的阶段,注意这里并不包括实例变量的创建。

    例如定义一个静态变量public static int value=345;那变量value在准备阶段过后的初始值为0而不是345。这个阶段不会执行任何Java方法,设置静态变量的值的命令存放于<clinit>()方法中。但是如果是ConstantValue属性,那在准备阶段就会赋值为给定的值,假设上面的value定义如下:

    public static final int value=123;

    那在准备阶段就会将value的设置为123

解析

解析阶段是JVM将常量池内的符号引用替换为直接引用的过程,符号引用是以一串符号来表示引用目标的,而直接引用则是指向目标的指针、相等偏移量或者是一个间接定位到目标的句柄。

初始化

类初始化是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与外,其余阶段完全由虚拟机主导和控制。初始化阶段执行<clinit>()方法。

  • <clinit>()方法是由编译器自动收集类中的所有类的变量的赋值语句与静态语句块合并产生的,收集顺序按照语句出现在源文件中的顺序决定。
  • 子类的<clinit>()调用前保证父类的<clinit>()被调用
  • 接口和类不一定有<clinit>()方法,如果没有静态语句或语句块(接口不能使用静态语句块),也就没有对类变量的赋值操作,也就不会生成<clinit>()方法
  • <clinit>()方法是线程安全的,会造成线程阻塞

类加载器

通过一个类的全限定名来获取描述此类的二进制字节流的这个动作放到JVM外部去实现,以便让应用程序自己决定如何去获取所需要的类,这个动作的代码模板称为类加载器

ClassLoader的重要方法

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

    • 载入并返回一个Class
  • protected final Class<?> defineClass(String name, byte[] b, int off, int len)throws ClassFormatError

    • 定义一个类不公开调用
  • protected Class<?> findClass(String name) throws ClassNotFoundException

    • loadClass方法回掉该方法,自定义ClassLoader的推荐方法
  • protected final Class<?> findLoadedClass(String name)

    • 寻找已经加载的类

Java中的类加载器

  • 启动类加载器(Bootstrap ClassLoader):这个类负责将存放在JAVA_HOME\lib目录中的,或被-Xbootclasspath参数指定的路径,并且是虚拟机识别的类库(仅按照文件名识别,如rt.jar)加载到虚拟机内存中。
  • 扩展类加载器(Extension ClassLoader):负责加载JAVA_HOME\lib\ext目录中的,或被java.ext.dirs系统变量所指定的路径中的所有类库
  • 应用程序类加载器(Application ClassLoader):负责加载用户类路径上(ClassPath)所指定的类库,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序的默认类加载器

双亲委派模式

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

如下图所示:

类加载器的双亲委派模型

破坏双亲委派模式

通过使用Thread. setContextClassLoader()上下文加载器,用以解决顶层ClassLoader无法访问底层ClassLoader的类的问题。

基本思想是:在顶层ClassLoader中,传入底层ClassLoader的实例。

那什么时候开始类加载过程的第一阶段加载 ,Java虚拟机规范中没有给出明确定义。但是给出了初始化阶段的5中情况

  • 遇到newgetstaticputstaticinvokestatic 这4条字节码指令时,如果类没有进行初始化,则需要先触发初始化过程
  • 使用java.lang.reflect 包的方法对类进行反射调用时,如果类没有进行初始化,则需要先触发初始化过程
  • 当初始化一个类,如果其父类没有进行初始化,则需要先触发初始化过程
  • 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会初始化该类
  • 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle 实例最后解析的结果是REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且方法句柄对应的类没有初始化,则需要先触发其初始化

对于上面5种触发类初始化的场景,虚拟机规范中给出了有且只有的语气,这5中场景中的行为称为对一个类的主动引用。除此之外,所有引用类的方式不会触发初始化,称为被动引用。具体可以看下面被动引用的示例:

1
2
3
4
5
6
7
public class SuperClass {
static {
System.out.println("SuperClass init");
}
public static int value = 12;
}
1
2
3
4
5
public class SubClass extends SuperClass {
static {
System.out.println("SubClass init");
}
}
1
2
3
4
5
public class NotInitialization {
public static void main(String[] args) {
System.out.println(SubClass.value);
}
}

输出结果是

1
2
SuperClass init
12

通过输出结果,我可以知道对应静态字段,只有直接定义该字段的类才会被初始化,因此子类引用父类的静态属性,只会触发父类的初始化操作而不会触发子类的初始化。

1
2
3
4
5
6
7
8
/**
*通过数组定义来引用类,不会触发此类的初始化
**/
public class NotInitialization {
public static void main(String[] args) {
SuperClass[] sca = new SuperClass[10];
}
}

其他代码不变,修改NotInitialization类的代码。该段代码没有任何输出,表示没有对SuperClass进行初始化

1
2
3
4
5
6
7
8
9
/**
*常量在编译阶段会存入调用类的常量池中,本质上并没有引用到定义常量的类,因此不会触发定义常量的类的初始化
**/
public class ConstClass {
static {
System.out.println("ConstClass init");
}
public static final String HELLOWORD="hello world";
}
1
2
3
4
5
public class NotInitialization {
public static void main(String[] args) {
System.out.println(ConstClass.HELLOWORD);
}
}

输出结果:

1
hello world
文章目录
  1. 1. 类加载机制
    1. 1.1. 开发环境
    2. 1.2. Class加载流程
      1. 1.2.1. 加载
      2. 1.2.2. 验证
      3. 1.2.3. 准备
      4. 1.2.4. 解析
      5. 1.2.5. 初始化
    3. 1.3. 类加载器
      1. 1.3.1. ClassLoader的重要方法
      2. 1.3.2. Java中的类加载器
      3. 1.3.3. 双亲委派模式
    4. 1.4. 破坏双亲委派模式