深入浅出 JVM 之 Java 虚拟机

深入浅出 JVM 之 Java 虚拟机

文章目录

  !版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。

系统环境:

  • JDK 1.8

参考地址:

深入浅出 JVM 系列文章

一、Java 虚拟机是什么

Java 虚拟机 简称 JVM (Java Virtual Machine),它是支撑 Java 语言的基石,是运行 Java 应用程序的关键,同时也是实现 Java 语言平台无关性的核心组成部分。

Java 虚拟机听名字就知道是一个虚拟机,可以简单认为它是一台抽象的计算机,是对真实的计算机进行的组成进行了模拟,它拥有着自己的指令集以及各种运行时内存区域,不同的区域实现不同的功能,比如堆、栈、程序计数器等。

并且,Java 虚拟机还拥有 一次编译到处运行自动内存关联自动垃圾回收 等特性。

二、为什么需要了解 Java 虚拟机

目前 Java 语言可以说是世界上使用率最高的语言之一,Java 开发者拥有着众多的数量,然而大部分开发者在使用 Java 语言开发过程中比较注重于基本语法和开源工具的使用,而并不太关注于 Java 底层的实现原理。

那么说到这里,相信每个人都想过一个问题,那就是学习 Java 虚拟机有必要么?这点笔者不好回答,这个必要性其实跟开发者的环境有关,如果你是一个整天忙碌于编写业务,无暇顾及这些底层知识,使用现有的知识便足以解决开发过程中遇到的问题,那么确实,说实在话必要性不是很大。

但是如果笔者认为,学习一门语言,尤其是想学好一门语言,不能只知其然而不知其所以然,而且我们在日常 Java 编码过程中,无时无刻都在跟 Java 虚拟机打交道,比如:

  • 启动 IDE 工具,那么这时候工具也需要 Java 环境,运行在 Java 虚拟机中;
  • 通过 Maven、Gradel 等工具对 Java 代码编译过程中,同样需要使用 Java 虚拟机执行任务;
  • 运行 Java 应用;
  • ....

所以,我们 Java 开发者时时刻刻都在与 Java 虚拟机打交道,如果不懂其概念,那么在和同事讨论问题过程中,人家提到 JVM 内存划分,新生代和老年代,以及 GC 垃圾回收等等,听的一脸懵逼~

再谈一些实际情况,笔者在过往的面试过程中,所面过的大部分公司中的面试官,几乎都对 Java 虚拟机的相关内容比较感兴趣,有的面试官可能会简单问些概念,但是有的甚至能夺命 N 连问,所以如果对 Java 虚拟机相关知识没有了解,很可能会丧失市场竞争力,尤其是中国 Java 开发者人数众多,竞争压力大,而且还比较卷,所以,如果别人了解这些内容而你不了解,你咋能竞争过别人呢?

总之来说,学习并了解 Java 虚拟机对我们 Java 开发者来说是非常有必要的,毕竟 Java 虚拟机是 Java 语言运行的基石。

三、Java JDK 的迭代历史

了解 Java 虚拟机那肯定先了解 JDK 各个版本的迭代过程,每个版本的 JDK 都有各自的变化,了解这些他们的历史更有助于我们对其知根知底。

这里本人简单汇总了下 JDK 各个版本的变化(资料收集于网络):

JDK 1.0 ~ JDK 1.4:

  • 1995 年 Java (原名Oak) 诞生,并且在 SunWorld 大会上正式发表了 Java 1.0 版本。
  • 1996 年 JDK 1.0 发布,提供了纯解释执行的 Java 虚拟机实现 Sun Classic VM。
  • 1997 年 JDK 1.1 发布,虚拟机没有做变更,依然使用 Sun Classic VM 作为默认的虚拟机。
  • 1998 年 JDK 1.2 发布,提供了运行在 Solaris 平台的 Exact VM 虚拟机,但此时还是用 Sun Classic VM 作为默认的 Java 虚拟机。
  • 2000 年 JDK 1.3 发布,默认的 Java 虚拟机由 Sun Classic VM 改为 Sun HotSopt VM,而 Sun Classic VM 则作为备用虚拟机。
  • 2002 年 JDK 1.4 发布,Sun Classic VM 退出商用虚拟机舞台,直接使用 Sun HotSpot VM 作为默认虚拟机一直到现在,并且新增了正则表达式、异常链、NIO、日志类、XML 解析器等功能。

JDK 5 ~ JDK 17:

  • 2004 年 JDK 5 发布,Sun 公司从这个版本开始放弃了 JDK 1.x 的命名方式,将产品版本号修改成了 JDK x 这种命名。并且在 Java 语法易用性上做出了非常大的改进,自动装箱、泛型、动态注解、枚举、可变长参数、遍历循环(foreach循环)等语法特性都是在 JDK 5 中加入的。除此之外,还改进了 Java 的内存模型,以及提供了 java.util.concurrent 并发包等。
  • 2006 年 JDK 6 发布,并且 Sun 公司还把 Java 开源了,创建了 OpenJDK 项目,OpenJDK 和 JDK 的代码基本一致。这个版本对 Java 虚拟机内部做了大量改进,包括锁与同步、垃圾收集、类加载等方面的实现都有相当多的改动。
  • 2011 年 JDK 7 发布,并且在发布前 Sun 公司被 Oracle 收购,提供了新的 G1 垃圾收集器。
  • 2014 年 JDK 8 发布,提供了 Lambda 表达式,并且彻底移除了 HotSpot 的永久代,使用元空间替代。
  • 2017 年 JDK 9 发布,支持 Jigsaw 模块和 Java Shell 等一些新工具,并且 Oracle 宣布每 6 个月发布一个版本,有些版本会被划为长期支持 (LTS) 版,并且只有 LTS 版的 JDK 才能够获得为期 3 年的支持和更新,普通版的 JDK 就只有短短六个月的生命周期。JDK 8、JDK 11 和 JDK 17 属于最近的 LTS 版。
  • 2018 年 3 月 JDK 10 发布,这版本的主要研发目标是内部重构,诸如统一源仓库、统一垃圾收集器接口、统一即时编译器接口。
  • 2018 年 9 月 JDK 11 发布,该版本是一个 TLS 版本,提供了新的 ZGC 垃圾收集器。
  • 2019 年 3 月 JDK 12 发布,改进了 Switch 表达式,并且提供了新的 Shenandoah 垃圾收集器。
  • 2019 年 9 月 JDK 13 发布,继续改进了 Switch 表达式,并且对现有的字符串文本进行了优化,提供了"""xxxx"""这种文本块用法等。
  • 2020 年 3 月 JDK 14 发布,确定了 Switch 表达式,新增了 instanceof 模式匹配和记录类型 (record),并且删除了 CMS 垃圾回收器。
  • 2020 年 9 月 JDK 15 发布,新增了封闭类、隐藏类,并且准备禁用和废除偏向锁,优化了集合,以及标记 ZGC 和 Shenandoah 可以在生产环境使用。
  • 2021 年 3 月 JDK 16 发布,确定了 record 和 instanceof 模式匹配等功能,提供了 jpackage 打包工具,可用于打包独立的 Java 应用程序。
  • 2022 年 9 月 JDK 17 发布,该版本是一个 TLS 版本,增强 TreeMap,弃用 Socket 实现工厂机制,新增 javadoc 工具,统一日志支持异步日志刷新等。

四、Java 虚拟机发展史与种类

我们再看一下 Java 虚拟机的发展史,以及都有哪些比较知名的虚拟机(资料收集于网络):

① Sun Classic VM

1996 年 1 月,Sun 公司发布 JDK 1.0 版本,这时支持 Java 语言运行的就是 Classic VM。

Classic VM 只能使用纯解释执行的方式来运行 Java 代码,执行效率非常差,与传统的 C/C++ 程序执行效率有很大差距,也正是这个时期给大众留下了 "Java 语言很慢" 这种印象。

② Sun Exact VM

在 JDK 1.2 版本时,Sun 团队曾在 Solaris 平台上发布过一款名为 Exact VM 的虚拟机,该虚拟机解决了 Classic VM 中许多的问题,提升了 Java 应用的整体的运行效率。

Exact VM 使用准确式内存管理 (Exact Memory Management) 而得名,该技术实现了虚拟机可以区分内存中某个位置的数据具体是什么类型,减少了每次定位对象间接查找的开销,提升执行性能。并且 Exact VM 执行系统已经具备现代高性能虚拟机的雏形,比如支持两级即时编译器、编译器与解释器混合工作模式等,具备现代高性能虚拟机的维形。

③ Sun HotSpot VM

在 JDK 1.3 版本时,Sun 将默认的虚拟机从 Classic VM 替换为了 HotSpot VM,该虚拟机的使用已经延伸到了现在,也是目前来说使用范围最广的虚拟机。不过,HotSpot VM 在最初并非由 Sun 公司开发,而是由 Longview Technologies 公司设计,Sun 公司注意到了这款虚拟机在 JIT 编译上有许多优秀的理念和实际效果,在 1997 年收购了 Longview Technologies 公司,从而获得了 HotSpot VM。

HotSpot VM 既继承了 Sun 之前两款商用虚拟机的优点,也有许多自己新的技术优势。如热点代码探测技术,可以通过执行计数器找出最具有编译价值的代码,然后通知 JIT 编译器以方法为单位进行编译。

在 2006 年的 JavaOne 大会上,Sun 公司宣布将 Java 进行开源,其中也包括了 HotSpot VM (基于GPL协议),并在此基础上建立了 OpenJDK。不过在 2009 年 Oracle 收购 Sun 公司,并极力的对 HotSpot VM 进行改造,使其性能更优。

当下我们提及 Java 虚拟机时,大多数指的都是 HotSpot 虚拟机,因此深入了解 HotSpot 虚拟机,对我们开发者来说是非常有必要的。

其它虚拟机

除了 Sun 公司以外,其他组织/公司也研发过不少虚拟机的实现,比如:

  • JRockit: BEA System 公司的 JRockit,BEA 公司后来被 Oracle 公司收购,JRockit 虚拟机也不再继续发展。
  • OpenJ9: IBM 公司的 J9,后来开源捐献给了 Eclipse 基金会,更名为 OpenJ9。
  • ...

五、Java 虚拟机规范

Java 虚拟机规范根据名字就知道,它是一种 Java 虚拟机实现的规范,也可以说它是一种约定,需要实现 Java 虚拟机的开发者们共同遵守并维护。

简而言之就是说,Java 虚拟机规范约定了 Java 虚拟机中要实现的大致功能,但是 Java 虚拟机规范中并没有规划具体实现的细节,这些实现细节部分将留给开发者们来实现。这就好比我们 Java 开发中的抽象类,抽象类中定义了抽象方法,开发者们只需要实现这些抽象方法就能实现具体的功能。所以,抽象方法的定义者并不关心这些抽象方法实现的细节,只要求实现这些抽象方法即可。

实际上,在 Java 虚拟机规范中规定了虚拟机组成结构、字节码文件结构、虚拟机编译器、加载类的阶段、异常、以及字节码指令集等内容,没有细节和具体功能的描述。

我们只要浏览一遍 Java 虚拟机规范,才能对其内容有所了解,这里给出 Oracle 官网的 Java 虚拟机规范地址,如下:

感兴趣的朋友可以自行查看,相信可以加强你对 Java 虚拟机整体设计的认知。

当然,如果对英文理解不好的博友,也可以关注本人公众号【小豆丁技术栈】,获取 JVM 1.8 虚拟机规范 (中文版)

六、Java 虚拟机语言无关性

在正常情况下,我们肯定会认为 Java 虚拟机是用于运行 Java 语言的,其实这么说并不能说是错误,但是也不完全对,这是因为 Java 虚拟机实际上运行的是特定的二进制字节码文件。实际上 Java 虚拟机和某种语言并没有必然的关联,因为不管什么语言只要能够编译成字节码文件,并符合 Java 虚拟机规范,那么 Java 虚拟机就可以加载该字节码文件,并且执行字节码文件中的指令。

JVM 支持的语言: ClojureJRubyGroovyScalaJavaScriptKotlin、...

因此,每种语言只要编译成符合 Java 虚拟机规范的字节码文件,就可以使用 Java 虚拟机运行指定语言的代码。

七、Java 虚拟机的组成

虚拟机的组成可以简单那划分为 类加载器执行引擎,以及运行时数据区里面的 方法区程序计数器虚拟机栈本地方法栈 等。

字节码文件

我们编写的 Java 代码在经过编译后就会转换为 Java 虚拟机可以执行的字节码文件,字节码文件中包含了 魔数编译的版本号常量池 以及 字节码指令 等;

类加载器

在 Java 中执类初始化时,需要执行 加载链接初始化 三个阶段,其中类加载器就是在 加载阶段 中用于将本地或网络中的 字节码文件 加载到 Java 虚拟机中的工具。

堆是在 JVM 启动时创建的,是 JVM 运行时数据区中的一块线程共享的内存区域,其内存空间在逻辑上连续,但物理上不一定连接,其主要用于存储 Java 中的 成员变量类变量 以及 引用类型的局部变量 等。

方法区

方法区是在 JVM 启动时创建的,是 JVM 运行时数据区中的一块线程共享的内存区域,其内存空间在逻辑上连续,但物理上不一定连接,主要用于存储 类信息方法信息域信息JIT代码缓存运行时常量池

虚拟机栈

虚拟机栈是 JVM 运行时数据区中的一块线程私有的内存区域,每当我们创建一个 线程 时就会创建一个 虚拟机栈。虚拟机栈主要用于存储线程的 栈帧,每当线程调用方法时就会创建一个栈帧,线程会在栈帧中执行代码计算逻辑,存储计算过程中的中间变量,和方法中的局部变量,当线程调用方法结束后就会使栈帧出栈,销毁栈帧中的数据。

本地方法栈

本地方法栈和虚拟机栈一样,都是用于存储线程执行 方法 时的一些数据,不过虚拟机栈只会在线程调用 Java 中的方法时起作用,而本地方法栈则是线程调用 Native 类型的方法时起作用。

程序计数器

程序计数器是 JVM 运行时数据区中的一块线程私有的内存区域,每当我们创建一个线程时就会创建一个程序计数器,其主要用于记录线程执行到的 字节码位置,这样可以保证线程在获得 CPU 时间片后执行时,能够恢复到正确的执行位置。

八、JVM 的生命周期

JVM 生命周期主要有三个部分,主要是虚拟机 启动运行退出 几个过程。

启动

  • Java 虚拟机的启动是通过引导类加载器 (Bootstrap Class Loader) 创建一个初始类 (initial class) 来完成的,这个类是由虚拟机的具体实现指定的。
  • 自定义的类是由系统类加载器加载的。自定义类的顶级父类都是 Object,Object 作为核心 API 中的类,是需要被引导加载器 (bootstrap class loader) 加载的。父类的加载是优先于子类加载的,所以要加载自定义类之前,会就加载 Object 类。

执行

  • JVM 运行启动以后有一个清晰的任务,那就是只执行 Java 程序,并且 Java 程序执行时 JVM 才会运行,Java 程序结束时 JVM 就会停止。
  • 在执行一个 Java 程序时,实际上底层真正在执行的是 Java 虚拟机的进程。

退出

JVM 在执行过程中,如果遇到以下几种情况就会退出:

  • ① 程序正常执行结束;
  • ② 程序在执行过程中遇到了异常或错误而异常终止;
  • ③ 由于操作系统用现错误而导致 Java 虚拟机进程终止;
  • ④ 某线程调用 Runtime 类或 system 类的 exit 方法,或 Runtime 类的 halt 方法,并且 Java 安全管理器也允许这次 exit 或 halt 操作;
  • ⑤ 还有一些其它情况,比如在 JNI(Java Native Interface)规范中描述了使用 JNI Invocation API 来加载或卸载 Java 虚拟机时,就存在 Java 虚拟机退出的情况;

---END---


  !版权声明:本博客内容均为原创,每篇博文作为知识积累,写博不易,转载请注明出处。