大家好,我是沉默王二,一个和黄家驹一样身高,和刘德华一样颜值的程序员。本篇文章通过我和三妹对话的形式来谈一谈“究竟什么是 JVM”。
教妹学 Java,没见过这么有趣的标题吧?“语不惊人死不休”,没错,本篇文章的标题就是这么酷炫,接受不了的同学就别点进来看了,所谓好奇心害死猫;能够接受的同学我只能说你赚到了,你不仅能在阅读的过程中感受到思维的乐趣,还真的能学习到知识。下面就由我来介绍一下故事的背景吧。
我有一个漂亮如花的妹妹(见上图),她叫什么呢?我想聪明的读者朋友们都能猜得出:沉默王三,没错,年方三六。父母正考虑让她向我学习,做一名正儿八经的 Java 程序员。我期初是想反抗的,因为程序员这行业容易掉头发。但家命难为啊,与其反抗,不如做点更积极的事情,写点有趣的文章,教妹妹如何更快地掌握 Java 这门编程语言。毕竟程序员还算得上高薪(都是拿命换的啊)。
(铺垫结束,正文开始)
“二哥,最近疫情闹得人心惶惶,无心学习了,怎么办?”
“三妹啊,你要知道,历史上经历过无数次的动荡与不安,但最后,都挺过去了。有人破坏,有人修复。安安心心学习,疫情过后肯定能够派上大用场,到时候经济复苏,人才亟需,懂吗?”
“二哥,说话就是不一样,一句话就安抚了我不安的心情。”
“那还不开始今天的主题?”
“二哥,上一篇文章中你给我解释了什么是 JDK,JRE 和 JVM,但我想知道 JVM 究竟是什么,它能干什么事。”
“三妹啊,搬个凳子坐我旁边,听二哥来给你慢慢说啊。”
01、什么是 JVM
再来回顾一下。JVM(Java Virtual Machine)俗称 Java 虚拟机。之所以称为虚拟机,是因为它实际上并不存在。它提供了一种运行环境,可供 Java 字节码在上面运行。
02、JVM 能做什么
JVM 提供了以下操作:
- 加载字节码
- 验证字节码
- 执行字节码
- 提供运行时环境
JVM 定义了以下内容:
- 存储区
- 类文件格式
- 寄存器组
- 垃圾回收堆
- 致命错误报告等
03、JVM 的内部结构
我们来尝试理解一下 JVM 的内部结构,它包含了类加载器(Class Loader)、运行时数据区(Runtime Data Areas)和执行引擎(Excution Engine)。
1)类加载器
类加载器是 JVM 的一个子系统,用于加载类文件。每当我们运行一个 Java 程序,它都会由类加载器首先加载。Java 中有三个内置的类加载器:
-
启动类加载器(Bootstrap Class-Loader),加载
jre/lib
包下面的 jar 文件,比如说常见的 rt.jar(包含了 Java 标准库下的所有类文件,比如说java.lang
包下的类,java.net
包下的类,java.util
包下的类,java.io
包下的类,java.sql
包下的类)。 -
扩展类加载器(Extension or Ext Class-Loader),加载
jre/lib/ext
包下面的 jar 文件。 -
应用类加载器(Application or App Clas-Loader),根据程序的类路径(classpath)来加载 Java 类。
一般来说,Java 程序员并不需要直接同类加载器进行交互。JVM 默认的行为就已经足够满足大多数情况的需求了。不过,如果遇到了需要和类加载器进行交互的情况,而对类加载器的机制又不是很了解的话,就不得不花大量的时间去调试
ClassNotFoundException
和 NoClassDefFoundError
等异常。
对于任意一个类,都需要由它的类加载器和这个类本身一同确定其在 JVM 中的唯一性。也就是说,如果两个类的加载器不同,即使两个类来源于同一个字节码文件,那这两个类就必定不相等(比如两个类的 Class 对象不 equals
)。
三妹啊,是不是有点晕,来来来,通过一段简单的代码了解下。
public class Test {
public static void main(String[] args) {
ClassLoader loader = Test.class.getClassLoader();
while (loader != null) {
System.out.println(loader.toString());
loader = loader.getParent();
}
}
}
每个 Java 类都维护着一个指向定义它的类加载器的引用,通过 类名.class.getClassLoader()
可以获取到此引用;然后通过 loader.getParent()
可以获取类加载器的上层类加载器。
上面这段代码的输出结果如下:
sun.misc.Launcher$AppClassLoader@18b4aac2
sun.misc.Launcher$ExtClassLoader@4617c264
第一行输出为 Test 的类加载器,即应用类加载器,它是 sun.misc.Launcher$AppClassLoader
类的实例;第二行输出为扩展类加载器,是 sun.misc.Launcher$ExtClassLoader
类的实例。那启动类加载器呢?
按理说,扩展类加载器的上层类加载器是启动类加载器,但在我这个版本的 JDK 中, 扩展类加载器的 getParent()
返回 null
。所以没有输出。
2)运行时数据区
运行时数据区又包含以下内容。
-
PC寄存器(PC Register),也叫程序计数器(Program Counter Register),是一块较小的内存空间,它的作用可以看做是当前线程所执行的字节码的信号指示器。
-
JVM 栈(Java Virtual Machine Stack),与 PC 寄存器一样,JVM 栈也是线程私有的。每一个 JVM 线程都有自己的 JVM 栈,这个栈与线程同时创建,它的生命周期与线程相同。
-
本地方法栈(Native Method Stack),JVM 可能会使用到传统的栈来支持 Native 方法(使用 Java 语言以外的其它语言[C语言]编写的方法)的执行,这个栈就是本地方法栈。
-
堆(Heap),在 JVM 中,堆是可供各条线程共享的运行时内存区域,也是供所有类实例和数据对象分配内存的区域。
-
方法区(Method area),在 JVM 中,被加载类型的信息都保存在方法区中。包括类型信息(Type Information)和方法列表(Method Tables)。方法区是所有线程共享的,所以访问方法区信息的方法必须是线程安全的。
-
运行时常量池(Runtime Constant Pool),运行时常量池是每一个类或接口的常量池在运行时的表现形式,它包括了编译器可知的数值字面量,以及运行期解析后才能获得的方法或字段的引用。简而言之,当一个方法或者变量被引用时,JVM 通过运行时常量区来查找方法或者变量在内存里的实际地址。
3)执行引擎
执行引擎包含了:
-
解释器:读取字节码流,然后执行指令。因为它一条一条地解释和执行指令,所以它可以很快地解释字节码,但是执行起来会比较慢。
-
即时(Just-In-Time,JIT)编译器:即时编译器用来弥补解释器的缺点,提高性能。执行引擎首先按照解释执行的方式来执行,然后在合适的时候,即时编译器把整段字节码编译成本地代码。然后,执行引擎就没有必要再去解释执行方法了,它可以直接通过本地代码去执行。执行本地代码比一条一条进行解释执行的速度快很多。编译后的代码可以执行的很快,因为本地代码是保存在缓存里的。
04、鸣谢
本篇文章为《教妹学Java》专栏的第七篇文章,是不是有趣得很?我相信你能感受的到,这可是全网独一份,我看到已经有人在模仿了。现在定价只需 9.9 元,9.9 元你连一杯奶茶都买不到,但却能买下二哥精心制作的专栏,据说 CSDN 已经考虑涨价了,毕竟已经卖出一百多份了。
我知道,购买专栏的同学都是冲着二哥的名声来的,毕竟二哥是 CSDN 的明星博主,哈哈。为表谢意,我再附送上个人微信(qing_gee),你有什么问题都可以来咨询。
上一篇回顾:教妹学Java(六):JDK,JRE和JVM之间有什么区别?
PS:本篇文章中的示例代码已经同步到 GitHub,地址为 itwanger.JavaPoint,欢迎大家 star 和 issue。
原创不易,喜欢就点个赞,因为你一个小小的举动,就会让这个世界多一份美好。