深入JVM一:类加载过程


首先来看一个示例:

public class PrintTest {

    public static void main(String[] args) {
        System.out.println("hello");
    }
}

我们通常使用的idea、eclipse和myeclipse等开发工具,启动程序只需触发按钮即可,其最终会调用到JAVA_HOME/bin/java.exe来启动虚拟机,java.exe调用底层的jvm.dll文件创建虚拟机实例,虚拟机实例会创建引导类加载器引导类加载器会创建JVM启动器sun.misc.Launcher,该类使用单例模式实现,sun.misc.Launcher实例创建时会创建其他类加载器(ExtClassLoader和AppClassLoader),当sun.misc.Launcher实例创建完成后,底层实现会调用sun.misc.Launcher的getClassLoader获取到类加载器加载启动类PrintTest。当类加载完成后,底层会调用PrintTest的main方法,从而开始程序。其流程如下:
在这里插入图片描述
那么这个过程中类的加载过程是什么样的呢?一般地,多个java文件经过编译打包生成可运行jar包,最终由java命令运行某个主类的main函数启动程序,这里首先需要通过类加载器把主类加载JVM,如上图。 主类在运行过程中如果使用到其它类,会逐步加载这些类。
注意:jar包里的类不是一次性全部加载的,是使用到时才加载。

类的加载分为以下几个步骤:
类加载分为一下几个步骤:
加载>>验证>>准备>>解析>>初始化>>使用>>卸载
在这里插入图片描述
接下来就对主要的加载、验证、准备、解析和初始化来了解。

加载

需要区分的是“加载”和“类加载的区别”,这里的“加载”是“类加载”的一个阶段。主要是将类的二进制数据加载到方法区的运行时数据机构中。
该步骤主要进行以下三件事情:

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

从上面的三个步骤可以了解到,我们经常使用的类的类对象Class就是在这一阶段产生的。

需要查看类加载信息可以设置一下虚拟机参数(两个参数均可):

-XX:+TraceClassLoading
-verbose:class

类加载的时机

在了解了类加载的“加载”阶段过程后,那么会想到什么时候才会触发类的加载?
首先来探讨一下类是在什么时机被加载,也就是类加载的时机
其实JVM规范没强制的指定类在什么时候被加载,不同的实现加载时机不同,但是规定在指定情况下必须进行初始化。这几种情况如下:

  1. 遇到new、getstatic、putstatci或invokestatic这四个字节码指令是,如果类没有初始化则进行类的初始化。对应着四个字节码指令,最常见的Java代码为:

    a.使用new关键字来实例化类的对象;
    b.读取或者设置一个静态变量;
    c.调用一个类的静态方法;

    如下代码:

    Person person = new Person();
    
    public class Test{
     public static void main(String[] args) {
        	 Person.type = "China";
        	 Person.say();
     }
    }
    
    class Person {
     public static String cpunttry= "America";
    
      public static void say(){
       	 System.out.println("hello");
     }
    }
    

    需要注意的是,被final修饰的静态变量(也就是常量)是不会触发类的初始 化的,final类型已在编译器将值放入常量池中。

  2. 使用java.lang.reflect包的方法对类进行反射调用时,如果类没有初始化则需要先将类初始化。

  3. 当一个类需要初始化时。如果该类的父类还没有初始化则先触发器父类的初始化。

  4. 当虚拟机启动时,指定的启动主类(包含main方法的那个类)会触发器初始化。

  5. 当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄对应的类没有初始化则先触发其初始化。

以上5中情况均是主动引用,除此之外其他的被称之为被动引用,是不会触发类的初始化的,被动应用示例可见JVM类加载时机之被动引用。动态语言相关内容可参考:Java动态语言

验证

该步骤主要是根据JVM规范校验二进制字节码的语法正确性。
验证过程主要包括以下几个步骤:

  1. 文件格式的验证。主要包括以下几点:

    a. 是否以0xCAFEBABE开头;
    b. 主、次版本好是否在当前虚拟机范围内;
    c. 常量池中是否就有不支持的常量类型;
    d. …

  2. 元数据验证。只要是验证类的元数据信息,包括继承关系,重载是否合法等雁阵。

  3. 字节码验证。主要目的是通过数据流和控制流分析, 确定程序语义是合法的、符合逻辑的。

  4. 符号引用验证。最后一个阶段的校验发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将
    在连接的第三阶段——解析阶段中发生。符号引用验证可以看做是对类自身以外(常量池中
    的各种符号引用)的信息进行匹配性校验,通常需要校验下列内容:

    a.符号引用中通过字符串描述的全限定名是否能找到对应的类。
    b.在指定类中是否存在符合方法的字段描述符以及简单名称所描述的方法和
    字段。
    c.符号引用中的类、字段、方法的访问性(private、protected、public、
    default)是否可被 当前类访问。

准备

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些变量所使用的内存 都将在方法区中进行分配。该阶段是为类的变量(也就是静态变量)进行内存的分配,而不是实例变量,实例变量是在类实例创建后在堆中分配的。同时在该阶段是设置初始值,也就是数据类型的零值。下表展示了各个数据类型的零值:

数据类型零值数据类型零值
int0booleanfalse
long0Lfloat0.0f
short(short)0double0d
char‘\u0000’referencenull
byte(byte)0

需要注意的是
关于静态常量在准备阶段的处理。

public static final int value=123

编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据 ConstantValue的设置将value赋值为123。所以说静态常量在准备阶段是分配空间并赋值的。

解析

解析阶段是虚拟机将常量池内的符号引用替换为直接引用的过程。该阶段会把一些静态方法(符号引用,比如 main()方法)替换为指向数据所存内存的指针或句柄等(直接引用),这是所谓的静态链接过程(类加载期间完成)。动态链接是在程序运行期间完成的将符号引用替换为直接引用。
在了解解析之前先来看下两个概念:

符号引用:符号引用以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。符号引用与虚拟机实现的 内存布局无关,引用的目标并不一定已经加载到内存中。各种虚拟机实现的内存布局可以各 不相同,但是它们能接受的符号引用必须都是一致的,因为符号引用的字面量形式明确定义 在Java虚拟机规范的Class文件格式中。

public class SymbolReferenceTest {

    public static void main(String[] args) {
        Demo demo = new Demo();
        demo.test();
    }
}

通过对SymbolReferenceTest 。class反编译后可以看到常量池中的一些字面量信息:
在这里插入图片描述
这是编译后,将必要引用存入常量池中

直接引用:直接引用可以是直接指向目标的指针、相对偏移量或是 一个能间接定位到目标的句柄。直接引用是和虚拟机实现的内存布局相关的,同一个符号引 用在不同虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经在内存中存在。

在上面了解到类的二进制文件被jvm加载到方法区中以指定的数据格式存在内存中,那么在调用到指定的类的方法或者变量时,该如何查找到指定的指令呢?就是通过在解析阶段,将编译时生成的符号引用转换为直接引用,那么在调用时可以直接定位到内存中的指令,进行执行。在解析阶段主要是解析的类的一些静态方法或者变量。

初始化

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值