前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >JVM:类加载过程

JVM:类加载过程

原创
作者头像
HLee
修改2021-08-31 11:26:53
7680
修改2021-08-31 11:26:53
举报
文章被收录于专栏:房东的猫房东的猫

类加载时机

Java天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接这个特点实现的。

一个类型从被加载到虚拟机内存开始,到卸载出内存为止,它的整个生命周期将会经历加载、验证、准备、解析、初始化、使用和卸载七个阶段,其中验证、准备、解析三个部分统称为连接。

类的生命周期
类的生命周期

加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类型的加载过程必须按照这种顺序按步就班地开始,而解析阶段则不一定:它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定特性。

类的初始化

当一个JVM在我们通过执行Java命令启动之后,其中可能包含的类非常多,是不是每个类都会被初始化呢?答案是否定的。JVM对类的初始化是一个延迟机制,即使用的是lazy的方式,当一个类在首次使用的时候才会被初始化,在同一个运行时包下,一个Class只会被初始化一次。

关于在什么情况下需要开始类加载过程的第一阶段"加载",《Java虚拟机规范》中并没有进行强制约束,这点可以交给虚拟机的具体实现来自由把握。但是对于初始化阶段,《Java虚拟机规范》则是严格规定了有且只有6种情况下必须立即对类进行"初始化"(而加载、验证、准备自然需要在此之前开始):

  • 通过new关键字会导致类的初始化
  • 访问类的静态变量,包括读取和更新会导致类的初始化
代码语言:javascript
复制
public class Simple {

    static {
        System.out.println("I will be init");
    }

    public static int x = 10;
}

这段代码中x是一个静态变量,即使别的类中不new Simple(),直接访问变量x也会导致类的初始化
  • 访问类的静态方法,也会导致类初始化
代码语言:javascript
复制
public class Simple {

    static {
        System.out.println("I will be init");
    }

    /**
     * 静态方法
     */
    public static void test() {
        
    }
}

其他类在调用test的方法时,也会触发Simple类的初始化
  • 初始化子类会导致父类被初始化
代码语言:javascript
复制
public class Parent {

    static {
        System.out.println("I will be init");
    }

    public static int x = 10;
}

public class Child extends Parent{

    static {
        System.out.println("I will be init");
    }

    public static int y = 10;
}

public class LoadTest {

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

注意:通过子类使用父类的静态变量只会导致父类被初始化,子类不会被初始化
public class LoadTest {

    public static void main(String[] args) {
        System.out.println(Child.x);
    }
}
  • 对某个类进行反射操作。会导致类被初始化
代码语言:javascript
复制
Class.forName()
  • 启动类,就是执行main函数所在的类会导致该类被初始化

当一个类在初始化时,要求其父类全部都已经初始化过了,但是一个接口在初始化时,并不要求父接口全部都完成了初始化,只有在真正使用到父接口的时候(如引用接口中定义了常量)才会初始化。

不会导致类的加载和初始化

  • 构造某个类的数组时并不会导致该类的初始化
代码语言:javascript
复制
public class Loader {
    
    public static void main(String[] args) {

        Simple[] simples = new Simple[10];
        System.out.println(simples.length);
    }
}
上边的代码new方法新建了一个Simple类型的数组,但是它并不能导致Simple类的初始化。事实上这个操作只是在堆内存中开辟了一段连续的地址空间4byte*10
  • 引用类的静态常量不会导致类的初始化

类加载过程

ClassLoder的主要职责是负责加载各种class文件到JVM中,ClasLoder是一个抽象的class,给定一个class的二进制文件名,ClassLoder会尝试加载并在JVM中生成这个类的各个数据结构,然后使其分布在JVM对应的内存区域。

类的加载过程分为三个阶段,分别是"加载阶段"、"连接阶段"、"初始化阶段"

加载阶段

主要负责查找并且加载类的二进制数据文件(class文件)。简单来说,类的加载就是查找并加载将class文件中的二进制数据读取到内存之中,然后将该字节流所代表的的静态存储结构转换为方法区中运行时的数据结构,并且在堆内存中生成一个该类的java.lang.Class对象,作为访问方法区数据结构的入口。

  • 通过一个类的全限定名来获取定义此类的二进制字节流
  • 将这个字节流所代表的静态存储结构化为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口

加载阶段结束后,Java虚拟机外部的二进制字节流就按照虚拟机所设定的格式存储在方法区中了,方法区中的数据存储格式完全由虚拟机实现自定义,《Java虚拟机规范》未规定此区域的具体数据结构。类型数据妥善安置在方法区之后,会在Java堆内存中实例化一个java.lang.Class类的对象,这个对象将作为程序访问方法区中的类型数据的外部接口。

连接阶段

1. 验证

主要是确保类文件的正确性,比如class的版本,class文件的魔术因子是否正确

验证是连接阶段的第一步,这一阶段的目的是确保Class文件的字节流中包含的信息符合《Java虚拟机规范》的全部约束要求,保证这些信息被当做代码运行后不会危害虚拟机的自身安全。

验证阶段是非常重要的,这个阶段是否严谨,直接决定了Java虚拟机是否能承受恶意代码的攻击,从代码量和耗费的执行性能的角度上讲,验证阶段的工作量在虚拟机的类加载过程中占了相当大的比重。

  • 文件格式验证:
    • 主、次版本号是否在当前java虚拟机接收范围之内
    • 是否以魔法因子0xCAFEBABE开头
    • 常量池的常量中是否有不被支持的常量类型(检查常量tag标志)
    • ......

该验证阶段的主要目的是保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个Java类型信息的要求。这个阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证之后,这段字节流才会被允许进入Java虚拟机内存的方法去中进行存储,所以后边的这三个验证阶段都是基于方法区的存储结构上进行的,不会再直接读取、操作字节流了。

Class文件并不一定只能由Java源码编译而来,它可以使用包括0和1直接在二进制编辑器中敲出Class文件在内的任何途径产生。所以验证字节码是Java虚拟机保护自身的一项必要措施。

  •  元数据验证:
    • 这个类是否有父类(除java.lang.Object之外,所有的类都应该有父类)
    • 这个类的父类是否进城了不允许集成的类(被final修饰的类)
    • 如果这个类不是抽象类,是否实现了其父类的或者接口之中要求实现的所有方法
    • 类中的字段、方法是否与父类产生矛盾
    • ......
  • 字节码验证:
    • 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作
    • ......
  • 符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候,这个将在连接的第三阶段----解析阶段发生
    • 符号引用中通过字符串描述的全限定名是否能找到对应的类
    • 符号引用中的类、字段、方法的可访问性(private、protected、public、<package>)是否可被当前类访问
    • .......

符号引用验证的主要目的是确保解析行为能正常执行

验证阶段对于虚拟机的类加载机制来说,是一个非常重要的、但却不是必须要执行的阶段,因为验证阶段只有通过或者不通过的差别,只要通过验证,其后就对程序运行期没有任何影响了。

2. 准备

正式为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置初始值的阶段

当一个class的字节流通过了所有的验证过程之后,就开始为该对象的类变量,也就是静态变量,分配内存并且设置初始值了,类变量的内存会被分配到方法区中。

代码语言:javascript
复制
public class Loader {

    private static int a = 10;
    private final static int b = 20;
}

其中static int a = 10在准备阶段不是10,而是初始值0,然而final static int b则会是20,为什么呢?因为final修饰的静态变量(可直接计算得出结果)不会导致类的初始化,是一种被动引用,因此就不存在连接阶段了。
当然了更为严谨的解释是final static int b = 20在类的编一阶段javac会将其value生成一个ConstantValue属性,直接赋予20.

首先是这时进行内存分配的仅包括类变量,而不包括实例变量,实例变量将会在对象实例化时随着对象一起分配在Java堆中。

  • 实例变量声明在一个类中,但在方法、构造方法和语句块之外;
  • 当一个对象被实例化之后,每个实例变量的值就跟着确定;
  • 实例变量在对象创建的时候创建,在对象被销毁的时候销毁;
  • 实例变量对于类中的方法、构造方法或者语句块是可见的;
  • 实例变量具有默认值
  • 实例变量可以直接通过变量名访问

如果类字段的字段属性表中存在ConstantValue属性,那么准备阶段变量值就会被初始化为实际代码的赋值。编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为开发人员在代码的赋值。

3. 解析

Java虚拟机将常量池内的符号引用替换为直接引用的过程

  • 符号引用:以一组符号来描述所有引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义地定位到目标即可。
  • 直接引用:是可以直接指向目标的指针、相对偏移量或者是一个能间接定位到目标的句柄。如果有了直接引用,那引用的目标必定已经在虚拟机的内存中存在。
  • 类和接口的解析
  • 字段解析
  • 方法解析

初始化阶段

直到初始化阶段,Java虚拟机才真正开始执行类中的编写Java程序代码,将主导权移交给了应用程序。进行准备阶段时,变量已经赋值过一次系统要求的初始零值,而在初始阶段,则会根据程序员通过程序编码指定的主观计划去初始化类变量和其他资源。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 类加载时机
  • 类的初始化
  • 类加载过程
    • 加载阶段
      • 连接阶段
        • 1. 验证
        • 2. 准备
        • 3. 解析
      • 初始化阶段
      相关产品与服务
      数据保险箱
      数据保险箱(Cloud Data Coffer Service,CDCS)为您提供更高安全系数的企业核心数据存储服务。您可以通过自定义过期天数的方法删除数据,避免误删带来的损害,还可以将数据跨地域存储,防止一些不可抗因素导致的数据丢失。数据保险箱支持通过控制台、API 等多样化方式快速简单接入,实现海量数据的存储管理。您可以使用数据保险箱对文件数据进行上传、下载,最终实现数据的安全存储和提取。
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档