写点什么

快速构建 JVM 整体认知 -JVM 的生命周期

用户头像
刘绍
关注
发布于: 1 小时前
快速构建JVM整体认知-JVM的生命周期

JVM 的生命周期

JVM 规范系列断断续续已经写了半年,最近结合一些小伙伴们的建议进行了一次复盘,决定在写法上进行一些演进优化:

  1. 更多地体现对内容的结构化分析,并以结构化的方式来组织内容。

  2. 类 PPT 式地展现方法,突出重点。这种方式的另一个好处是:将来可以方便地作为视频的原始素材。


这次我们从宏观视角梳理 JVM 的生命周期。这是我 JVM 系列文章中的其中一篇,相对宏观,其它部分主要讲解 JVM 规范的局部细节:字节码分析、面向 JVM 的编译、方法调用与异常处理的底层原理、控制流程、运行时内存区域、数据类型等应有尽有。完整的 JVM 系列已发布在公众号:码神手记,也会陆续发布到 InfoQ,想要尽快获取的朋友马上关注公众号吧!



01 启动


一切始于 main 方法!JVM 通过触发一个初始类的加载而启动,这个初始类中应当包含 main 方法。JVM 会调用 main 方法从而驱动后续所有的执行。在 JVM 的实现中,可以通过命令行参数指定初始类,也可以通过设置一个类加载器来指定初始类。

02 加载


关于类加载,我们使用 4W+1H 的方式进行理解:

What:加载什么?

加载的是用二进制形式表示的类,也就是编译后的字节码文件中的内容。

When:何时加载?

一个类被加载的时机分为两种:

  1. 启动类:当前类被指定为初始类,那么 JVM 在启动时会首先对其进行加载。

  2. 引用:一个正要被加载的类直接或间接引用了当前类,比如:方法调用、字段访问、反射调用、继承、接口实现。

Who:谁来加载?

负责加载工作的通常是类加载器,所有的类加载器都继承自 ClassLoader 抽象类,它定义了类加载器的基本行为和步骤,子类可以进行自己的定制

类加载器有如下分类:

  1. 引导类加载器:用于加载标准的二进制形式的字节码文件。在 HotSpot 虚拟中还有扩展类加载、应用类加载器,这是 HotSpot 虚拟机在实现 JVM 规范时的扩展。

  2. 自定义类加载器:可以根据需求自由发挥,加载来自网络、加密文件或者动态生成的字节码文件。

有一种特殊情况,当类型是数组时,将直接由虚拟机本身负责加载,数组中的组件类型依然由类加载器加载。

Where:加载到哪里去?

加载之后的类将会存放在方法区当中,当然这是 JVM 规范的规定,实际在实现 JVM 时会有所差异。比如:HotSpot 虚拟机 1.8 版本(通常所说的 JDK1.8)开始把方法区叫做 MetaSpace。

How:如何加载?

类加载器可以自己完成对一个类的加载,也可以委托其它类加载器来加载。由此,出现了以下两个概念:

  1. 发起加载的加载器被称为被加载类的初始加载器

  2. 实际完成加载的加载器被称为被加载类的定义加载器

初始加载器和定义加载器并不一定是同一个

这里涉及到另外一个问题:在运行时,如何区分一个类或接口?类或接口的区分并不仅仅通过名字,而是通过它的二进制名字和定义类加载器共同区分。

03 链接


链接是在加载之后执行的,是获取创建好的类或接口,将其与虚拟机运行时状态相结合的过程。


完整的链接过程包含以下三个步骤:

  1. 验证:验证字节码结构是否正确,是否符合约束条件。

  2. 准备:创建静态字段并设置默认值,默认值与类型相关,比如:int 类型的默认值是 0,Integer 等引用类型的默认值是 null。

  3. 解析:确认符号引用指向的真实地址。任何指令的执行都需要对符号引用进行解析,也包括本地方法的调用。

许多网文中提到:链接过程是解析符号引用,将其指向真实地址的过程。实际上这个解析是链接过程中的可选部分。JVM 的实现者可以选择使用饿汉(eager)模式在这个阶段一次性全部解析,也可以使用懒汉(lazy)模式在使用时单独解析每个符号引用。 

04 初始化


初始化过程就是执行类或接口的初始化方法(<clinit>)的过程。类在初始化之前,必须被链接,即:验证、准备且正常解析。


因为 JVM 是多线程的,类或接口的初始化需要谨慎的同步(采用等待-通知机制)。因为其它线程可能同时尝试初始化相同的类或接口,还可能递归地请求类或接口的初始化,递归会作为类或接口初始化的一部分。

在初始化的过程中,假设类对象已经被验证并准备好,类对象处于以下四种状态之一

  1. 未初始化:类被验证且准备好,但尚未初始化。当前线程将 Class 标记为正在初始化,然后释放锁,再按 Class 结构中的字段顺序初始化 final static 字段。若父类尚未初始化则会进入递归,先初始化其父类。初始化完成后再获取锁,并标记状态为已完成,准备投入使用,随后释放锁通知其它等待线程。

  2. 正在初始化:这个类正在由某个线程初始化,当前线程被阻塞,直到接收到初始化完成的通知。若在初始化过程中发生了错误,则获取锁,并标记状态为错误,随后释放锁通知其它线程,并抛出 ExceptionInInitializerError 或 OutOfMemoryError(由于内存不足而发生错误时)。

  3. 准备投入使用:这个类已完全初始化并准备投入使用。当前线程不再进行任何操作,释放锁并通知其它等待线程。

  4. 错误:这个类处于错误状态,可能是因为初始化的失败。当前线程不可能再进行任何操作,释放锁并抛出 NoClassDeFoundError。 

05 退出


JVM 的退出有三种情况:

  1. System 或 Runtime 类的 exit 方法,这是非强制的退出方式,所有子线程都执行完成后才会退出。

  2. Runtime 类的 halt 方法,这是强制性的退出方式,立即退出。

  3. JNI(Java Native Interface)规范指出:当使用 JNI 调用 API 加载或卸载 JVM 时会终止正在运行的 JVM。

JNI 是一个标准的编程接口,用于编写 Java 本地方法并将 JVM 嵌入到本地应用中。例如使用 C/C++编写的程序调用 JVM。 

发布于: 1 小时前阅读数: 4
用户头像

刘绍

关注

所谓系统,没有局部完美,唯有全局平衡。 2018.02.23 加入

公众号:码神手记,我们的故事从一个关注开始。 资深工程师,一线开发团队Leader。文能PPT,武能撸代码。

评论

发布
暂无评论
快速构建JVM整体认知-JVM的生命周期