学习JVM需要一定的编程经验和计算机基础知识,适用于从事Java开发、系统架构设计、性能优化、研究学习等领域的专业人士和技术爱好者。
学习本专栏以及本章内容的前提和适用人群如下:
每位Java开发者都了解到Java字节码是在Java运行时环境(JRE)上执行的。JRE包含了最为关键的组成部分:Java虚拟机(JVM),它负责分析和执行Java字节码。通常情况下,大多数Java开发者无需深入了解虚拟机的内部运行原理。即使对虚拟机的运行机制不甚了解,也不会对开发工作产生太多影响。然而,对JVM有一定了解的话,将更有助于深入理解Java语言,并解决一些看似困难的问题。
本专栏全面系统地剖析了特定虚拟机产品(即HotSpot,Oracle官方虚拟机)的实现,本人不仅深刻地讲解了看似深奥的原理,还提供了大量易于上手的实践案例,下面是总体的JVM相关的知识拓扑架构。
tips:当然还有一些最新的JVM特性未在这张图并非展示本专栏的全部内容,另外还包含了最新的JVM特性。
类加载是Java中一个核心的概念,它主要指的是将类的 .class 文件中的二进制数值读取到内存中,对其进行处理,并储存在运行时数据区的方法区内。
这个过程还会在堆区创建一个 java.lang.Class 类的对象,这个对象的主要作用是去封装那些已存储在方法区内的类相关数据结构,如下图案例所示。
类加载的最终就是在堆区中形成这样一个 Class 对象,这个Class对象不仅对类在方法区内的数据结构进行了封装,而且,它还为开发者提供了访问接口,使得程序员能访问到方法区内的数据结构,这个标准化的接口是在开发中还是至关重要的,因为它为我们使用和理解Java类内部数据结构提供了极大的便利性和可操作性。
类加载器并不必须等到某个类被首次主动使用时再加载它。根据JVM的规范,类加载器可以预测某个类将要被使用时之前加载它。
如果在预先加载的过程中发现某个类的.class文件缺失或存在错误,类加载器会等到程序首次主动使用该类时报告LinkageError错误,但如果这个类一直没有被程序主动使用,类加载器则不会报告错误。
类加载过程包括加载、验证、准备、解析和初始化这五个阶段,如下图所示。
加载阶段是整个类生命周期的第一个步,此阶段主要是为了查找并加载Java类的class字节码文件对应的二进制数据,JVM需要完成以下三件事情:
加载阶段具有最高的可控性,开发人员可以选择使用系统提供的类加载器来进行加载,也可以自定义自己的类加载器来实现加载操作。
一旦加载阶段完成,外部的二进制字节流按Java虚拟机所需的格式存储在方法区,同时在Java堆中创建了一个java.lang.Class对象。通过该对象,可以方便地访问方法区中的数据。
JVM类加载系统中的连接阶段可以大致描述为以下三个步骤:验证、准备和解析。
验证是连接阶段的首要步骤,其主要目的是确保Class文件中的信息符合虚拟机的要求,并且不会对虚拟机的安全性造成威胁。
验证阶段通常包括以下四个检查动作:
验证阶段虽然至关重要,但并非必需,因为它并不会直接影响程序运行。
在某些场合,例如:如果一个类已经被验证过多次,那么我们可以考虑使用 -Xverify:none
参数来关闭大部分验证过程。这样做的目的是为了缩减JVM在类加载过程中所需的时间,从而提升运行效率。
注意,关闭类验证可能会带来安全性风险,因此,在实际操作时要谨慎选择。
准备阶段是类加载过程中的一个阶段。在这个阶段,会为类变量分配内存并设置初始值,这些操作发生在方法区。以下是需要注意和优化的几个方面:
仅类变量(static)会在准备阶段进行内存分配,实例变量则会在对象实例化时,随着对象一起分配在Java堆中。
类变量的初始值通常是数据类型的默认值。而这些默认值是在Java代码中没有显式赋值的情况下自动赋予的。
例如,整数类型的默认值是0,长整型的默认值是0L,引用类型的默认值是null,布尔类型的默认值是false。
假设有一个类变量的定义:
public static int value = 3;
注意,在准备阶段结束后,变量value的初始值实际上是0,而不是3。这是因为在准备阶段只进行了内存分配和默认初始值的设置,并没有执行具体的赋值操作。
上面的赋值操作将在初始化阶段执行,通过生成类构造器(())方法来实现。在该方法中,会执行将value赋值为3的指令,这个初始化阶段会在实际执行Java方法之前进行。
对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。
public static final int value = 3;
在准备阶段,变量value会被初始化为3,因为它同时被final和static修饰。
编译时Javac将会为value生成ConstantValue属性,在准备阶段虚拟机就会根据ConstantValue的设置将value赋值为3。可以理解为static final常量在编译期就将其结果放入了调用它的类的静态常量池中。
注意,不是一定被static final 定义的变量一定会被准备阶段就被放到静态常量池中。例如:static final a = getAConfig(),这种场景就不会放入常量池,因为只有在初始化阶段(Clinit)才能知道,真正执行出它的结果。
解析阶段,JVM会将静态常量池中的符号引用替换为直接引用,目的是为了提高代码的执行效率和减少运行时的解析开销。
解析动作主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行。符号引用就是一组符号来描述目标,可以是任何字面量。
符号引用:用于标识被引用内容的抽象引用形式,符号引用是在编译期间产生。
直接引用:直接引用就是直接指向目标的指针、相对偏移量或一个间接定位到目标的句柄,可以直接访问到所引用的内容,直接引用是在解析阶段产生。
将符号引用替换为直接引用也可以减少运行时的解析开销,JVM可以在执行过程中直接访问到所引用的内容,而不需要再进行符号解析和查找过程,从而提高了代码的执行效率,提升程序的运行性能。
其中加载、验证、准备和初始化的顺序是确定的,而解析阶段的开始时间可以延迟到初始化阶段之后,解析阶段的延迟开始是为了支持Java语言的运行时绑定(也称为动态绑定或晚期绑定)。
注意,这些阶段是按顺序开始,并不是按顺序进行或完成,因为它们通常是相互交叉混合进行的,在一个阶段执行的过程中可能会调用或激活另一个阶段。
初始化,为类的静态变量赋予真正的初始值,JVM负责对类进行初始化,主要对类变量进行初始化。在Java中对类变量进行初始值设定有两种方式:
类的声明过程中直接为静态变量赋予初始值。例如:
public class MyClass {
public static int myVariable = 10; // 直接赋值为10
}
静态代码块是在类加载过程中执行的代码块,在类被加载时自动执行。可以在静态代码块中为静态变量赋予初始值。例如:
public class MyClass {
public static int myVariable;
static {
myVariable = 10; // 在静态代码块中赋值为10
}
}
当对类的主动使用的时候才会导致类的初始化,类的主动使用包括以下六种:
new
关键字来实例化一个类的对象。Class.forName("com.xxx.ClassName")
来获取类对象。在如下几种情况下,Java虚拟机将结束生命周期