JVM之类加载过程

464 阅读9分钟

类加载的触发

一个类从加载到内存开始,一直到被卸载结束,他的整个生命周期包括加载、连接(验证、准备、解析)、初始化、使用、卸载等几个阶段。这几个顺序如下图所示:

类的加载触发:Java虚拟机并没有强制规定类加载是基本,这个情况需要虚拟机自由实现。例如Tomcat在启动的时候,会启动引导类加载器、扩展类加载器、通用类加载器和应用类加载器。这个几个加载器首先会加载和初始化一些类(JVM所需类、Tomcat所需类、以及一些通用类),剩下的类是收到请求的时候才会进行类的加载操作。 虽然虚拟机没有明确的说明类的加载时机。但是对于初始化阶段,虚拟机给予了严格规定,有且只有一下情况才会立即对类进行初始化:

  1. 遇到new、putstatic、getstatic以及invokestatic这4条字节码指令时,如果类还没初始化吗,则立即进行初始化,这4条指令分别代表实例化一个类、设置&读取一个静态字段(没有被final修饰)、调用类的静态方法;
  2. 使用java.lang.reflect包对类进行反射操作时候(在类没初始化的前提下)。
  3. 当初始化一个类,但是其父类没有进行初始化的时候。
  4. 放虚拟机启动的时候,需要将执行启动的主类(有main方法的哪个类)进行初始化。
  5. 当使用动态语言时,如果一个java.lang.invoke.MethodHandle实例最终的解析结果是REF_getStatic、REF_putStatic、REF_invokeStatic句柄时,并且这个句柄对应的类没有初始化。

类加载过程

加载

加载是类加载的一个阶段,是类加载的第一个阶段。加载阶段,虚拟机完成三件事情:

  • 根据类的全限定名获取定义此类的二进制字节流(全限定名是将.替换为/)。
  • 将这个字节流代表的静态存储结构转换为元空间的运行时数据结构(JDK8以前叫做方法区)。
  • 在元空间为这个类生成一个java.lang.Class对象,作为元空间这个类入口。

Java虚拟机规范并没有规定从哪里获取、怎么获取二进制字节流,这个阶段也是用户参与度最高的阶段,用户可以使用不同的形式在自定义类加载器控制字节流的获取方式,比如成熟的二进制获取方式和类加载器有:

  • 从zip包中获取二进制文件,常见的有jar、war、ear包。
  • 从运行时动态生成,如动态代理技术,在ava.lang.reflect.Proxy中,使用 ProxyGenerator.generateProxyClass为各种就接口生成形如 "$Proxy"的代理类的二进制字节流;
  • 从网络中获取,比较常见的是Applet应用。
  • 从其他文件生产,比jsp等。 .......

数组的加载和普通的类不同,因为数组本身并不是通过类记载其加载产生的,数组类是虚拟机自动生成的但是数组的类型是通过类加载器完成加载的,数组的创建遵循如下规则:

  • 如果数组的类型是引用类型,则引用类型需要使用递归来加载,并且数组需要被加载该数组类型的类加载器命名空间上进行标识。
  • 如果数组的类型不是引用类型,是基本数据类型,java虚拟机会将数组标记为与引导类加载器关联。
  • 数组的可见性与数据类型的可见性保持一直,如果数组类型是基本类型,则默认为public。

验证(连接)

文件格式验证

这一阶段主要验证字节流是否符合class文件格式规范,并且能够被虚拟机处理,保证输入的字节流能够被正确解析并且存储在元空间。这个阶段的验证是基于二进制字节流的验证,它的发生时间是在开始加载后,但是还没有在方法区存储之前,所以这个阶段再加载开始后进行,但是和加载阶段是混合进行的,这一阶段的校验包括以下这些验证点:     1、 是否已魔数CAFEBABE开头;     2、 主次版本号是否在当前虚拟机处理范围之内;     3、 常量池中的常量是否有不被支持的类型(使用tag标识校验);     4、 指向常量的索引是否有不存在或者支持的类型;     5、 字符类型是否符合规范;     6、 class文件是否被修改过;     ......

元数据验证

第二阶段主要是进行语法分析,以保证class文件符合Java语法规范,这个阶段的验证点包括:     1、 这个类是否有父类(除java.lang.Object类之外都有父类);     2、 这个类的父类是否继承了不允许被继承的类(final修饰);     3、 如果这个类不是抽象类,是否实现了其父类或者接口中要求实现的所有方法;     4、 类中的字段、方法是否跟父类中的字段、方法冲突;     ......

字节码验证

这个阶段是语义分析,是验证过程中最复杂的一个阶段,主要目的是通过数据流和控制流,确定语义是合法并且符合逻辑的,这个阶段主要是针对方法体进行分析,以保证方法在运行过程中不会出现危害虚拟机的操作,验证点包括:     1、 变量要在使用之前进行初始化;     2、 方法调用与对象引用类型要匹配;     3、 数据和方法的访问要符合权限设置规则;     4、 对本地变量的访问都落在运行时本地方法栈内;     5、 运行时堆栈没有溢出;     ......

符号引用验证

最后阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个过程将会在解析阶段进行,符号引用验证可以看作是对类自身以外的信息进行匹配校验,确保解析动作可以正常执行,这一阶段通常需要校验以下内容:     1、 通过全限定名能否找到对应的类;     2、 在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和字段;     3、 符号引用中的类、方法、字段的访问性(private、protected、public)是否可以被当前类访问到;     ......

对于虚拟机来说,验证阶段是一个非常重要但不是必须进行的阶段,因为对虚拟机运行期没有影响,如果运行的所有代码可以保证能正常执行,不会危害虚拟机的运行,那么可以考虑关闭虚拟机的大部分验证过程,以缩短类加载时间。

准备(连接)

准备阶段是为类变量分配内存并且设置初始化值的阶段,这些变量所使用的内存都在元空间分配,这个阶段初始化的数据只有静态字段,并且是赋值初始化值(final字段除外),不是代码中定义的值。    public static int value = 123; 在准备阶段,value在方法区分配内存,并且设置初始值0,如果value被final修饰,形如:     public static final int value = 123; 则该变量在准备阶段将会被赋值123,并且不会引起类的初始化过程,示例及说明见第二部分(类加载的时机)的示例。

解析(连接)

解析阶段是虚拟机将符号引用转化为直接引用的过程,符号引用在之前已经介绍过了,在class文件中以形如"CONSTANT_Class_info"、"CONSTANT_Fieldref_info"、"CONSTANT_Methodref_info"格式存在。

初始化

初始化阶段是类加载过程的最后一步,这个阶段才开始真正的执行用户定义的Java程序。在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则需要为类变量(非final修饰的类变量)和其他变量赋值,其实就是执行类的()方法。 ()不是一个合法变量,这个方法也不是由用户显示定义的,是由编译器生成的,编译器在编译阶段会自动手机类中的所有类变量的赋值动作和静态语句块(static{})中的语句合并而成的,编译器的顺序是由语句的顺序决定的,静态语句块只能访问到定义在静态语句块之前的变量,定义在静态语句块之后的变量,可以赋值,但是不能访问。(静态变量的初始化和静态代码块的执行顺序是由编写顺序决定的,根据javap我们可以看到)

测试代码:

public class Test {

    private static int a = 100;

    static {
        System.out.println(a);
    }

    public static void main(String[] args) {

    }

}

javap后结果:

()方法与类的构造方法不同,它不需要用户显示的调用,虚拟机会保证父类的()方法先于子类的()执行,java.lang.Object的()方法是最先执行的。 接口总不能使用用静态语句块,所以接口的()只包含类变量,所以接口的()方法执行时,不要求限制性父接口的()方法。 ()方法对于类和接口来说不是必须的,如果类或接口中没有定义类变量,也没有静态语句块,那么编译器将不为这个类或者接口生成()方法,如果类或者接口中生成了()方法,那么这个方法在执行过程中,虚拟机会保证在多线程环境下的线程安全问题。

blog.csdn.net/u010942465/… www.zhiliaotang.net/jishujiaoli… 深入理解Java虚拟机