0%

类加载器

类加载器

源代码经过编译器编译之后生成了一个字节码文件(class文件),字节码文件中存储的是jvm指令,而不是机器指令

Java虚拟机的启动是通过引导类加载器创建一个初始类来完成的,这个类由虚拟机的具体实现指定,Java虚拟机链接这个初始类,初始化并调用它的main方法,之后整个执行过程都是由对此方法的调用开始。执行main方法中的java虚拟机指令可能会导致Java虚拟机链接另外一些类或接口,也可能会调用另外的方法

类加载器子系统负责从文件系统或者网络中加载class文件,class文件在文件开头有特定的文件标识,将class字节码文件在类装载器子系统(Class Loader SubSystem)中有三个阶段

  • 第一阶段:加载 找到class文件并把这个文件包含的字节码加载到内存
  • 第二阶段:链接 链接中包含 字节码验证、准备和解析
  • 第三阶段:初始化,类中的静态属性和初始化赋值以及静态块的执行等

加载阶段

加载阶段是根据特定名称查找类或接口类型的二进制表示,将java字节码数据从不同的数据源加载到JVM中

ClassLoader只负责class文件的加载,将java类的字节码文件加载到机器内存,是否可以运行是由Execution Engine决定,加载的类信息存放于一块称为方法区的内存空间。除了类的信息外,方法区中还会存放运行时常量池信息(包括字符串字面量和数字常量,是class文件中常量池部分的内存映射)

类加载过程

  • 通过一个类的全类名获取定义该类的二进制字节流
  • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在堆内存中生成一个代表这个类的java.lang.Class类的实例对象,作为方法区这个类的各种数据的访问入口,java.lang.Class类的构造方法是私有的,只有JVM能够创建

类加载的方式

  • 从本地系统中加载
  • 通过网络获取
  • 从zip压缩包中加载(jar、war是在此基础上做的)
  • 运行时计算生成,如动态代理
  • 其他文件生成,如jsp
  • 从加密文件中获取,为了防止class文件被反编译的保护措施

链接阶段

链接阶段是为了让类或接口可以被java虚拟机执行,而将类或接口并入虚拟机运行时状态的过程

又分为了三个步骤,验证、准备、解析

验证(Verify)

有四种验证:文件格式类型,元数据验证,字节码验证,符号引用验证

验证的目的是为了确保class文件的字节流中包含的信息符合当前虚拟机的要求,保证被加载的类不会危害虚拟机自身的安全

  • 格式检查 进行魔术检查、版本检查、长度检查 是在加载过程检查的
  • 语义检查 检查是否继承final类、是否有父类、抽象方法是否有实现
  • 字节码验证 跳转指令是否指向正确位置、操作数类型是否合理、函数调用是否传入了正确类型的参数
  • 符号引用检查 检查符号引用的直接引用是否存在,Class文件在其常量池会通过字符串记录自己将要使用的其他类或方法,所以需要检查这些类或者方法是否存在(常量池中描述的类是否存在),并且当前类有权限访问这些数据(访问的方法或字段是否存在足够的权限),类无法找到会报NoClassDefFoundError 是在解析阶段执行的

准备(Prepare)

准备阶段主要是为静态变量分配内存并且设置该静态变量的默认初始值(基本数据类型和字符串字面量final修饰的static变量在编译的时候就会分配了默认值,在准备阶段会进行显式的赋值;实例变量也不会分配初始化,是会在对象实例化的时候和对象一起分配在堆中),类变量会分配在方法区

如果是final static修饰的引用数据类型的话,会在中进行赋值

1
2
3
4
// 在准备阶段值是0,而不是44
public static int COUNT = 44;
// 在准备阶段值是200,默认初始值是在编译时设置的
public static final int MAX = 200;

解析(Resolve)

解析实际上是在初始化之后才执行

解析其实是将常量池内的符号引用转换为直接引用的过程,解析动作主要针对于类或接口、字段、类方法、接口方法、方法类型、方法句柄等。对应常量池中的CONSTANT_Class_info、CONSTANT_Fieldref_info、CONSTANT_Methodref_info等

符号引用是一组符号来描述所引用的目标,符号引用的字面量形式定义在虚拟机规范的class文件格式中

直接引用是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄

初始化阶段

初始化阶段是执行类初始化方法()的过程,来为类的静态变量进行赋值,该方法是由类静态成员的赋值语句,以及静态代码块内的语句合并而来的,是根据源代码的书写顺序来执行的。

()不是类的构造器(类的构造器在jvm中是()方法),如果该类有父类,会保证子类的()方法在父类的()方法执行完毕后再进行执行

有些类是不会产生()初始化方法的

  • 一个类中并没有声明任何的类变量,也没有静态代码块
  • 一个类中声明类变量,但是没有明确的使用类变量的初始化语句以及静态代码块来执行初始化操作(即没有进行显式的赋值)
  • 一个类中包含static final修饰的基本数据类型的字段或者字符串字面量,不管是否进行显式赋值,都不会生成,是在准备阶段进行赋值的

需要注意的是:由于虚拟机会保证一个类的方法在多线程内可以正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的方法,其他线程阻塞等待,所以static可以用来做单例模式,但是也注意不要写出来死锁的代码

类初始化方法的触发

类的使用有两种,一种是主动使用,一种是被动使用

主动使用

类在第一次主动使用前会触发类初始化方法

  • 当创建一个类的实例时,比如使用new关键字,或者通过反射(Class.forName)、克隆、序列化
  • 当调用类的静态方法时
  • 当使用类、接口的静态字段时(如果是被static final修饰的基本数据类型和字符串字面量,则不会触发,因为是在准备阶段进行赋值的)
  • 当初始化子类时,如果发现父类还没有进行初始化,则需要先触发父类的初始化,但是并不会初始化它所实现的接口,只有在程序首次使用特定接口的静态字段时,才会导致接口的初始化
  • 如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化
  • 当虚拟机启动时,用户需要指定一个要执行的主类,会先初始化这个主类
  • 当初次调用MethodHandle实例时,初始化该MethodHandle指向的方法所在的类
被动使用

类的被动使用不会触发类初始化方法

  • 当访问一个静态字段时,只有真正声明这个字段的类才会被初始化,当通过子类去引用父类的静态变量时,只会触发父类的初始化而不会触发子类的初始化,但是此时子类是被加载了的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    public class TestInit {
    public static void main(String[] args) {
    // super init
    // com.zhanghe.study.jvm.init.AA@5ca881b5
    System.out.println(Sub.a);
    }
    }

    class Super{
    static {
    System.out.println("super init");
    }

    public static AA a = new AA();
    }

    class Sub extends Super{
    static {
    System.out.println("sub init");
    }
    }

    class AA{

    }
  • 通过数组定义类引用,不会触发此类的初始化,因为数组定义时,其内的元素默认都是null

  • 基本数据类型和字符串字面量常量不会触发此类或接口的初始化,因为常量在链接过程的准备阶段就已经被显式赋值了

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    public class TestClinit {
    public static void main(String[] args) {
    // 此时不会触发 AAA类初始化
    // System.out.println(AAA.i);
    // 此时会触发 AAA类初始化
    System.out.println(AAA.bb);
    }
    }

    class AAA {
    public static final int i = 10;
    public static final BBB bb = new BBB();
    static{
    System.out.println("AAA类初始化");
    }
    }

    class BBB {

    }
  • 调用ClassLoader类的loadClass方法加载一个类,不会导致类的初始化

加载类错误分析

在执行java程序时经常会遇到ClassNotFoundException和NoClassDefFoundError异常,这两种异常是不一样的

ClassNotFoundException

该异常是由于在JVM要加载指定文件的字节码到内存时,并没有找到这个文件对应的字节码,也就是这个文件并不存在,可以检查一下classpath目录下有没有指定文件存在

NoClassDefFoundError

该异常通常是由于使用new关键字、属性引用某个类、继承了某个接口或类,以及方法的某个参数中引用了某个类,触发JVM隐式加载这些类时发现这些类不存在的异常