类加载的过程

类加载的过程分为三个步骤(五个阶段) :加载 -> 连接(验证、准备、解析)-> 初始化。
加载、验证、准备和初始化这四个阶段发生的顺序是确定的,而解析阶段可以在初始化阶段之后发生,也称为动态绑定或晚期绑定。

1. 加载

加载:查找并加载类的二进制数据的过程。
加载的过程描述:

  1. 通过类的全限定名定位.class文件,并获取其二进制字节流。
  2. 把字节流所代表的静态存储结构转换为方法区的运行时数据结构。
  3. 在Java堆中生成一个此类的java.lang.Class对象,作为方法区中这些数据的访问入口。

    2. 连接

    连接:包括验证、准备、解析三步。

    a). 验证

    验证:确保被加载的类的正确性。用于确保Class文件的字节流中的信息是否符合虚拟机的要求。
    具体验证形式:
  • 文件格式验证:验证字节流是否符合Class文件格式的规范;例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型。
  • 元数据验证:对字节码描述的信息进行语义分析(注意:对比javac编译阶段的语义分析),以保证其描述的信息符合Java语言规范的要求;例如:这个类是否有父类,除了java.lang.Object之外。
  • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。
  • 符号引用验证:确保解析动作能正确执行。

    b). 准备

    准备:为类的静态变量分配内存,并将其初始化为默认值。准备过程通常分配一个结构用来存储类信息,这个结构中包含了类中定义的成员变量方法接口信息等。
    具体行为:

  • 这时候进行内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中。

  • 这里所设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式赋值。

    c). 解析

    解析:把类常量池内的符号引用转换为直接引用。
    解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符等7类符号引用进行。
    • 符号引用(Symbolic References):符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用和虚拟机实现的内存布局无关,引用的目标不一定已经加载到内存中。各种虚拟机实现的内存布局可以各不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义在Java虚拟机规范的Class文件格式中。
    • 直接引用(Direct References):直接引用可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。直接引用和虚拟机实现的内存布局相关的,同一个符号引用能在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。

3. 初始化

初始化:主要操作是初始化静态变量和执行静态代码块。
在准备阶段,类变量(static)已经被赋过一次系统要求的初始值,而在初始化阶段,则是根据程序员通过程序指定的主观计划去初始化类变量和其他资源,或者可以从另一个角度来表达:初始化阶段是执行类构造器**<clinit>()**方法的过程。

在编译生成的Class文件时,会自动产生两个方法,一个是类的初始化方法<clinit>,另一个是实例的初始化方法<init>。可以简单理解为<clinit>是用户初始化静态变量和静态块的,且只执行一次,而<init>是用于对象实例化的构造器方法,每次对象实例化时都会调用。 1、 <clinit>方法在JVM第一次加载Class文件时调用(所以静态块只会执行一次),包括静态块的执行和静态变量初始化。 2、<init>方法在实例(对象)创建时候调用, 就是生成对象的时候, 例如new ,反射等等。

这里简单说明下()方法的执行规则:

  1. 1、<clinit>()方法是由编译器自动收集类中的所有类变量(static)的赋值动作和静态语句块中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句中可以赋值,但是不能访问。
  2. 2、<clinit>()方法与实例构造器<init>()方法(类的构造函数)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕,所以父类的静态块等要优先于子类的操作。因此,在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object
  3. 3、<clinit>()方法对于类或接口来说并不是必须的,如果一个类中没有静态语句块,也没有对类变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
  4. 4、接口中不能使用静态语句块,但仍然有类变量(final static)初始化的赋值操作,因此接口与类一样会生成<clinit>()方法。但是接口鱼类不同的是:执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法,只有当父接口中定义的变量被使用时,父接口才会被初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。
  5. 5、虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁和同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,那就可能造成多个线程阻塞,在实际应用中这种阻塞往往是很隐蔽的。

初始化的时机:
类的初始化也是延迟的,直到类第一次被主动使用(active use),JVM才会初始化类。

  1. 创建类的实例(new关键字);
  2. 对类的静态变量进行访问或赋值;
  3. 访问调用类的静态方法;
  4. 反射(如:Class.forName(“xxx”));
  5. 初始化一个类的子类,父类本身也会被初始化;
  6. 作为程序的启动入口,包含main方法(如:SpringBoot入口类)。

类加载器

类加载器简言之,就是用于把.class文件中的字节码信息转化为具体的java.lang.Class对象的过程的工具。

类加载器可以大致划分为以下三类:

  1. 启动类加载器:Bootstrap ClassLoader

它负责加载存放在JDK\jre\lib(JDK代表JDK的安装目录,下同)下,或被-Xbootclasspath参数指定的路径中的,并且能被虚拟机识别的类库(如rt.jar,所有的java.*开头的类均被Bootstrap ClassLoader加载)。启动类加载器是无法被java程序直接引用的。

  1. 扩展类加载器:Extension ClassLoader

该加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载JDK\jre\lib\ext目录中,或者由java.ext.dirs系统变量指定的路径中的所有类库(如javax.*开头的类),开发者可以直接使用扩展类加载器。

  1. 应用程序类加载器:Application ClassLoader

该类加载器由sun.misc.Launcher$AppClassLoader来实现,它负责加载用户类路径(ClassPath)所指定的类,开发者可以直接使用该类加载器,如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

应用程序都是由这三种类加载器互相配合进行加载的,如果有必要,我们还可以加入自定义的类加载器。因为JVM自带的ClassLoader只是懂得从本地文件系统加载标准的java class文件,因此如果编写了自己的ClassLoader,便可以做到如下几点:

  1. 在执行非置信代码之前,自动验证数字签名。
  2. 动态地创建符合用户特定需要的定制化构建类。
  3. 从特定的场所取得java class,例如数据库中和网络中。

这几种类加载器的层次关系如下图所示:
JVM类加载机制 - 图1

双亲委派机制

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

使用双亲委派模型来组织类加载器之间的关系,有一个很明显的好处,就是防止类被重复加载,保证了核心库的安全。例如,类java.lang.Object类存放在JDK\jre\lib下的rt.jar之中,因此无论是哪个类加载器要加载此类,最终都会委派给启动类加载器进行加载,这边保证了Object类在程序中的各种类加载器中都是同一个类。

对象的初始化顺序

静态变量/静态代码块 -> 普通代码块 -> 构造函数

  1. 父类静态变量和静态代码块(先声明的先执行);
  2. 子类静态变量和静态代码块(先声明的先执行);
  3. 父类普通成员变量和普通代码块(先声明的先执行);
  4. 父类的构造函数;
  5. 子类普通成员变量和普通代码块(先声明的先执行);
  6. 子类的构造函数。

  7. 【JVM】类加载机制

  8. JVM类加载机制