互联网科技程序员

JVM类生命周期概述:加载时机与加载过程

2019-04-08  本文已影响2人  a13ed6c7cc5e

写在前面

本文概述了JVM加载类的时机和生命周期,并结合典型案例重点介绍了类的初始化过程,揭开了JVM类加载机制的神秘面纱。

JVM类加载机制主要包括两个问题:类加载的时机与步骤类加载的方式

一个Java对象的创建过程往往包括两个阶段:类初始化阶段类实例化阶段

注意,本文内容是以HotSpot虚拟机为基准的。

类加载机制概述

我们知道,一个.java文件在编译后会形成相应的一个或多个Class文件(若一个类中含有内部类,则编译后会产生多个Class文件),但这些Class文件中描述的各种信息,最终都需要加载到虚拟机中之后才能被运行和使用。事实上,虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验,转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型的过程就是虚拟机的类加载机制

类加载的时机

Java类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载(Loading)、验证(Verification)、准备(Preparation)、解析(Resolution)、初始化(Initialization)、使用(Using) 和 卸载(Unloading)七个阶段。其中准备、验证、解析3个部分统称为连接(Linking),如图所示:

JVM类生命周期概述:加载时机与加载过程

类加载时机

什么情况下虚拟机需要开始加载一个类呢?虚拟机规范中并没有对此进行强制约束,这点可以交给虚拟机的具体实现来自由把握。

类初始化时机

1)遇到new、getstatic、putstatic或invokestatic这四条字节码指令(注意,newarray指令触发的只是数组类型本身的初始化,而不会导致其相关类型的初始化,比如,new String[]只会直接触发String[]类的初始化,也就是触发对类[Ljava.lang.String的初始化,而直接不会触发String类的初始化)时,如果类没有进行过初始化,则需要先对其进行初始化。生成这四条指令的最常见的Java代码场景是:

  1. 使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先触发其初始化。

  2. 当初始化一个类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化。

  3. 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。

  4. 当使用jdk1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getstatic,REF_putstatic,REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行初始化,则需要先出触发其初始化

被动引用的几种经典场景

通过子类引用父类的静态字段,不会导致子类初始化

public class SSClass {
    static {
        System.out.println( "SSClass" );
    }
}

public class SClass extends SSClass {
    static {
        System.out.println( "SClass init!" );
    }

    public static int value = 123;

    public SClass()
    {
        System.out.println( "init SClass" );
    }
}

public class SubClass extends SClass {
    static {
        System.out.println( "SubClass init" );
    }

    static int a;

    public SubClass()
    {
        System.out.println( "init SubClass" );
    }
}

public class NotInitialization {
    public static void main( String[] args )
    {
        System.out.println( SubClass.value );
    }
}

通过数组定义来引用类,不会触发此类的初始化

public class NotInitialization{
 public static void main(String[] args){
 SClass[] sca = new SClass[10];
 }
}

常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化

public class ConstClass{
 static{
 System.out.println("ConstClass init!");
 }
 public static final String CONSTANT = "hello world";
}
public class NotInitialization{
 public static void main(String[] args){
 System.out.println(ConstClass.CONSTANT);
 }
}

类加载过程

JVM类生命周期概述:加载时机与加载过程

加载(Loading)

在加载阶段(可以参考java.lang.ClassLoader的loadClass()方法),虚拟机需要完成以下三件事情:

  1. 通过一个类的全限定名来获取定义此类的二进制字节流(并没有指明要从一个Class文件中获取,可以从其他渠道,譬如:网络、动态生成、数据库等);
  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构;
  3. 在内存中(对于HotSpot虚拟就而言就是方法区)生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口;

验证(Verification)

验证阶段是非常重要的,但不是必须的,它对程序运行期没有影响。如果所引用的类经过反复验证,那么可以考虑采用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

准备(Preparation)

准备阶段是正式为类变量(static 成员变量)分配内存并设置类变量初始值(零值)的阶段,这些变量所使用的内存都将在方法区中进行分配

解析(Resolution)

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。

初始化(Initialization)

类初始化阶段是类加载过程的最后一步。在前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的java程序代码(字节码)。

虚拟机会保证一个类的类构造器<clinit>()在多线程环境中被正确的加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的类构造器<clinit>(),其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕

在同一个类加载器下,一个类型只会被初始化一次

上一篇 下一篇

猜你喜欢

热点阅读