2023-08-09 21:08:47
相信许多开发同学看过《深入理解java虚拟机》,也阅读过java虚拟机规范,书籍和文档给人的感觉不够直观,本文从一个简单的例子来看看jvm是如何工作的吧。
本文所有操作均在mac上进行。
示例代码采用最常见的双重检索单例模式:
package interview.desginpattern.singletion.doublecheck; import java.io.Serializable; public class Singleton implements Serializable { private static volatile Singleton instance = null; private Singleton() { } public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
经过编译后,我们得到class文件,然后用javap命令查看相关指令
javap -c Singleton:
public class interview.desginpattern.singletion.doublecheck.Singleton implements java.io.Serializable { public static interview.desginpattern.singletion.doublecheck.Singleton getInstance(); Code: 0: getstatic #2 // get static instance 3: ifnonnull 37 // instance is null,jump 37 6: ldc #3 // push class Singleton to operand stack 8: dup // Duplicate the top (class Singleton) operand stack value 9: astore_0 // Store reference(class Singleton) into local variable 10: monitorenter // synchronized start 11: getstatic #2 14: ifnonnull 27 // instance is null,jump 27 17: new #3 // new class interview/desginpattern/singletion/doublecheck/Singleton 20: dup 21: invokespecial #4 // Method "<init>":()V 24: putstatic #2 27: aload_0 // Load reference (class Singleton) from local variable 28: monitorexit // synchronized end 29: goto 37 32: astore_1 33: aload_0 34: monitorexit // synchronized end (exception) 35: aload_1 36: athrow // throw exception 37: getstatic #2 40: areturn Exception table: from to target type 11 29 32 any 32 35 32 any static {}; // static code Code: 0: aconst_null // Push the null object reference onto the operand stack 1: putstatic #2 // set static instance 4: return }
有以下几点需要说明:
operand stack 即操作数栈,区别于jvm虚拟机栈,是属于栈帧(frame)中的数据结构
local variable 即本地变量,也属于栈帧中的结构
synchronized 被解析成monitorenter 和 monitorexit两条指令,并且需要处理异常情形
我们知道一般来讲jvm 运行时数据包括,pc寄存器,stack(虚拟机栈),heap,method area, 运行时常量池,本地方法栈。stack 中的每一帧包括,operand stack, local variable , 和指向常量池的指针。
上一节中只是展示了java代码编译成class文件,包含了哪些指令,但是class文件包含的信息远远不止这些。
我们在IDEA 中使用插件 jclasslib bytecode viewer 查看 class文件具体包含哪些信息:
这里省略了class 文件的魔法数标志,0xCAFEBABE
<init> 方法对应Singletion的构造方法
<clinit> 方法对应Singletion.class的构造方法
我们还可以查看getInstatnce编译后的指令 (chapter 3):
上述数据结构(4.1, 4.2, 4.3)不会存字符串字面量,而是指向常量池的引用:
加载即根据class文件创建对象/接口.
有两种加载器,分别是bootstrap class loader,和user-defined class cloader,我们可以自定义加载器,比如从网络,或者从加密的class文件中加载对象。
任何一个class/inteface 被限定名 + 类加载器唯一确定,所以jvm实现者在并发的情况下需要确保此唯一性约束。
一般而言,加载流程如下
看该文件有没有被对应的加载器进行加载
验证该文件是不是class文件,major or minor version 是否被支持
检查父类是否被加载
检查接口是否被加载
验证:确保class文件结构是否正确,是否打破jvm规范
准备:给class或者interface的静态属性设置默认值(区别于显式赋值)
解析:给 symbolic references 赋予确定的值(除了 invokedynamic,其他都可以唯一确定)
初始化:调用class或者interface的<init>, <cinit> 方法 (4.3)
本文概述了java代码是如何加载到jvm中的,jvm有各种不同的实现(我们最熟悉的hotspot虚拟机),其中细节可能不尽相同。加载到jvm中并不代表程序生命周期的结束,运行时的情况也值得关注。