1. 前言

​ 虚拟机就是一款用来执行虚拟计算机指令的计算机软件。它相当于一台虚拟计算机。大体上,虚拟机分为系统虚拟机和程序虚拟机。系统虚拟机就相当于一台物理电脑,里面可以安装操作系统;程序虚拟机是为了执行单个计算机程序而设计出来的虚拟机。其中 Java 虚拟机就是执行 Java 字节码指令的虚拟机

JVM 是什么?

java 虚拟机是运行在各大平台的执行字节码文件的虚拟计算机。如下图所示

这样的设计可以让编译后的代码在各个操作平台上运行。

JVM 的位置

JVM 在 Java 体系结构中的位置

从用户操作角度看 JVM 所处的位置

JVM 与实际的计算机硬件没有交互,它们中间还有个操作系统,调用硬件需要通过操作系统来实现。

JVM 的结构

JVM 的整体运行结构

本文主要针对于 Hotspot VM 来进行,其结构如下所示:

JVM 的指令结构

JVM 是基于栈的指令集架构,跟基于寄存器的指令集不同。它是一个步骤一个一条指令。

//基于栈的指令集
iconst_2 //常量2入栈
istore_1
iconst_3 // 常量3入栈
istore_2
iload_1
iload_2
iadd //常量2/3出栈,执行相加
istore_0 // 结果5入栈
//基于寄存器的指令集
mov eax,2 //在eax寄存器中的值设为2
add eax,3 //将eax寄存器中的值加3   

举例说明:

/**
 * @author wjw
 * @date 11/3/2021
 */
public class StackStruTest {
    public static void main(String[] args) {
        int i = 2 + 3;
        /**
         *0: iconst_5
         *1: istore_1
         *2: return
         */
        int i = 2;
        int j = 3;
        int k = i + j;
        /**
         * 0: iconst_2
         * 1: istore_1
         * 2: iconst_3
         * 3: istore_2
         * 4: iload_1
         * 5: iload_2
         * 6: iadd
         * 7: istore_3
         * 8: return
         */
        try {
            Thread.sleep(6000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("hello");
    }
}

JVM 的生命周期

JVM 的启动

Java 的启动是通过引导类加载器(BootStrap ClassLoader)创建一个初始类(java.lang.class)来实现的,也就是后面要提到的子类加载器过程。

JVM 的执行

执行 Java 的字节码文件中的各个方法和类的过程。

JVM 的退出

  • 程序正常执行结束
  • 操作系统终止 JVM 进程终止
  • 某线程调用 Runtime.exit() 或 System.halt() 方法
  • 程序执行过程异常或错误而终止退出

2. 类加载子系统的主要流程

让我们先看看一个 java 程序执行的流程是怎样的:

  • 首先一个 java 文件编写完后,经过 java 编译器生成 .class 文件
  • 这个 .class 文件经过类加载器,加载各类的 jar 包,以及该程序所需要的三方类的 .class 文件。并生成 java.lang.class 对象,这个对象将作为程序访问方法区中的这些类型数据的外部接口
  • . class 文件经过解析执行和 JIT 编译器编译执行,并调用操作系统相关指令来完成 java 程序。

而类加载子系统涉及到的过程有类加载器、字节码校验器和翻译字节码这几个过程:

加载阶段(Loading)

加载阶段(Loading)是类加载阶段(Class Loading)的一部分。在加载阶段中,.class 文件将经过三个类加载器的加载后才能进入下一个阶段。该阶段主要的作用(也就是上面 java 程序执行中的类加载器阶段)有:

  • 将各种各样格式的 .class 文件(jar 包,网络中,动态代理等等)读取并生成一个字节流,并转换成方法区中的运行时数据结构
  • 在内存中生成一个 java.lang.Class 对象,这个对象将作为程序访问方法区中的这些类型数据的外部接口

在类加载器中,主要分为两类:引导类加载器自定义类加载器

引导类加载器(Bootstrap ClassLoader)
  • 使用的是 C/C++ 来实现的,无法访问到,调用getClassLoader() 函数是为null。

  • 加载的类是一些核心类库,lib/home 下的类库。比如说是 Object 、String 等等 JVM 自身运行需要的类

  • 是其他加载器的父类吗?是的,但是无父类加载器

自定义类加载器(User-Defined ClassLoader)

派生于抽象类 ClassLoader 的类加载器

  • (1)拓展类加载器(Extension ClassLoader)

    • 父类加载器为 BootStrap ClassLoader

    • 主要加载的类是ext 文件夹下的类库

  • (2)应用程序类加载器(AppClassLoader)

    • 父类加载器为 Extension ClassLoader

    • 主要加载的是环境变量 classpath 或系统属性 java.class.path 指定路径下的类库

    • 程序中默认的类加载器

  • (3) 自定义类加载器

    • extCassLoader 和 AppClassLoader 是 sun.misc.Launcher 的两个内部类

为了防止虚拟机内部的核心类库的修改和非法调用,JVM 的设计者们在类加载过程中设计出了双亲委派机制沙箱安全机制

双亲委派机制
  • 当前类加载器先不加载,先委托其父类加载器,依次到启动加载器
  • 如果启动加载器还没找到所要的类,然后向下查找,到返回到最初的子类加载器
  • 类加载器之间的关系不是继承,而是通过组合来复用父加载器的代码。

举例

  • 首先在 Bootstrap ClassLoader 中加载核心 rt.jar 包中的核心类
  • 核心类中的一些三方接口在反向委派到应用程序加载器加载第三方的 jar 包
  • 是通过当前线程加载器(就是系统类加载器)加载到 jdbc.jar 包中的 SPI 接口实现类

双亲委派机制的优点:

  • 避免类的重复加载
  • 保护程序安全,防止核心 API 被随意篡改
沙箱安全机制

引导类加载器在加载时会首先加载 JDK 中自带的文件。报错信息中没有相应方法时的机制。保证对 java 核心源代码的保护。

链接阶段(Linking)
验证(Verify)

验证字节流文件中的相关信息,确认当前文件符合虚拟机的相关格式(文件、元数据、符号等等)。

准备(Prepare)

在方法区中为类变量分配内存并设置类变量初始值

  • 这里的类变量是指被 static 修饰的变量,不包括实例变量(实例变量在对象实例化时随着对象一起分配)
  • 初始值指的是数据类型的零值
解析(Resolve)
  • 虚拟机将常量池中的符号引用转换成直接引用(class 文件很小,先存放指向所需要类的符号,解析时再引用)

  • 针对类、接口、字段方法等7类符号引用进行转换

  • 没有规定解析阶段发生的具体时间,也可能在初始化阶段的后面

初始化阶段(Initiation)
  • 初始化过程实际上就是执行类构造器方法 ( ) 的过程

    • 它是 javac 编译器自动收集类中的所有类变量的赋值动作和静态代码块中的语句合并而来,不需要定义。
    public class ClassInitTest {
        private static int num = 1;
    
        static {
            num = 2;
            number = 20;
            System.out.println(num);//因为在linking 阶段中的 Prepare 阶段已经给静态变量赋过初始值了。
            //非法的前向引用;在声明变量之前,可以去给这些变量进行赋值,但是不能够调用它。
            //System.out.println(number);//会报错误
        }
    
        /**也就是在 linking 中的 prepare 中对变量进行默认赋值为0
         * 此时initial 给予其一个覆盖从 20 到 10
         *
         */
        private static int number = 10;
    
        public static void main(StringDemo[] args)
        {
            System.out.println(ClassInitTest.num);
            System.out.println(ClassInitTest.number);
        }
    }
    
    • 它不需要显示调用父类构造器,在子类的( ) 执行前,父类的 ( ) 方法早已执行完毕。

  • 构造器方法执行是按语句顺序来执行的。

  • 类构造器方法 ( ) 和类构造器( ) 并不是一回事

    • ( ) 方法不需要定义,而( ) 需要定义(不定义但是会有个默认为空的构造器方法)
    • 只有当代码中包含 static 变量时,才会执行 ( ) 方法,而( ) 所有类的字节码中都有


  • 在多线程时,初始化只执行一次,否则会被上锁

3.其他小结

  • 确定两个类是否相同
    • 包名、类名完全相同
    • 所用的类加载器也要相同
  • 类的主动使用和被动使用
    • 也就是是否有初始化过程,是否调用 clinit<>() 方法
Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐