万字概览 Java 虚拟机

为什么要学习 JVM

在很多 Java 程序员的开发生涯里,JVM 一直是黑盒子一般的存在,大家只知道运行 Java 程序需要依靠 JVM,千篇一律的配置几个类似 -Xms 和 -Xmx 的参数,可能到最后都不知道自己配置的参数有什么具体的意义。在我周围的 Java 程序员里面,甚至还有一部分有数年 Java 开发经验的人至今都不知道该怎么开启 JVM 的 GC 日志。但是,这一切并不妨碍我们开发出令人惊艳的产品。所以,我们为什么要学习 JVM?

从功利性的角度来讲,越来越多的公司在面试时都会针对 JVM 提问,学习 JVM 可以提高自己的面试通过率,当然这属于面向面试学习了。从实践的角度来讲,学习 JVM 可以帮助我们写出更优质的代码,比如你不会写出超过 8000 字节的巨型方法,因为你知道 JIT 不会编译它,每次只能解释执行,这是由 -XX:HugeMethodLimit 参数控制的;你也不会在 Metaspace OOM 时一头雾水,会首先定位是否是反射太频繁导致产生的类加载器过多而引发的。总的来说,学习 JVM 是提升我们 Java 内功的一种方式。

JVM 的作用是什么

万字概览 Java 虚拟机

如图所示,我们编写的 Java 代码通过 Java 编译器编译后成为字节码,即我们常说的 .class 文件。这些字节码文件和 JDK 类库的字节码文件分别通过 JVM 提供的基本的三个类加载器被加载到 JVM 之中,JVM 就是负责解析、执行这些字节码并管理和协调整个执行生命周期各项事件的平台。字节码是一种中间代码,通过 JVM 的解释、编译,最终通过操作系统与硬件达成交互。

JVM 的运行时区域

JVM Runtime Area 也可以称作 JVM 运行时内存模型。JVM 本质上是操作系统上的一个进程,它的运行需要内存空间,而 JVM 运行时内存模型描述的就是 JVM 怎么划分和管理这些内存。

以 Hotspot VM 为例,我们将整个内存模型简化为下图所示:

万字概览 Java 虚拟机

图中上方区域是受 JVM 直接管理的内存区域,其中 Stack Area 可以分为 JVM Stack 和 Native Method Stack,但在 Hotspot VM 中两者并合并到了一个区域。

下方的 Direct Memory 是不受 JVM 管理的内存,这些内存是属于操作系统的,JVM 通过在 Heap 中保存一个指向这些内存区域的引用来进行内存操作。当 Heap 中的引用被 GC 回收时,Direct Memory 中被使用的区域也自动被操作系统回收。这块内存通常用于 JDK1.4 开始提供的 NIO API 上,通过 ByteBuffer#allocateDirect() 方法实现直接内存的分配,而这个 ByteBuffer 就是这块分配出来的 Direct Memory 在 Heap 里存在的引用。基于 Direct Memory 进行 IO 操作的好处是避免了数据在 Direct Memory 和 Java Heap 之间的来回拷贝,也可以进一步实现「零拷贝」,避免数据在内核态与用户态之间来回拷贝。

当 Direct Memory 不足以分配新的空间时也会抛出 OOM。

PC Register

在多核处理器上,一个时刻只能有一个核进行工作,而多线程情况下,线程可能分布在不同的核心上。当 A 线程任务还没有处理完时,所在核心失去了 CPU 执行权,此时就需要使用程序计数器记录当前程序执行的位置,等下次获得执行权后继续执行。这个区域是 JVM 规范中唯一一个不会出现 OOM 的区域。

Stack Area

Stack Area 可以分为 JVM Stack 和 Native Method Stack。顾名思义,前者是 JVM 在调用 Java 方法时存储栈帧的空间,而后者用于在 JVM 调用 Native 方法时存储产生的栈帧。在 Hotspot VM 的实现中将两者合并成了一块内存区域。在 JVM 中,一个方法从调用开始到调用结束就是一个栈帧入栈和出栈的过程。每个线程栈的大小使用参数 -Xss 进行指定,默认是 1M。绝大部分情况下用不了这么大,建议配置为 256kb 即可。栈空间越大,则 JVM 能容纳的线程数越少;栈空间越小,可递归深度越低。

栈帧包含有局部变量表,存储编译期可知的基本数据类型以及对象的符号引用等信息,还包含有操作数栈、动态链接、方法出口等信息。

Stack 为什么是线程私有的,这涉及到「栈上分配」和「TLAB」两个概念。

栈上分配

所谓栈上分配就是允许将对象直接分配在栈上,而不用分配到 Heap 中。这样对象会随着栈帧的出栈自动销毁,不用等待 GC 进行回收,从而提高性能。但是要实现栈上分配是非常复杂的,涉及到「逃逸分析」和「标量替换」两项技术。

逃逸分析简单来说就是判断一个对象的存活范围是否有可能越出当前方法的执行范围,也就是说当前栈帧出栈的时候这个对象是否还会存活。如果分析结果是不会存活,那么这个对象就可以安全的分配在栈上,随着栈帧的出栈而被销毁。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zygjjy.html