跨平台的本质

关于 JVM, Java 程序员的最熟悉的一句话就是:一处编码,到处执行,指的就是 Java 语言可以通过 JVM 实现跨平台。而跨平台到底跨越了什么这个问题相信很少有人知道,接下来就跟我一起了解一下吧。

下图展示了两种不同的汇编风格,除此之外还有 ARM 汇编(主要应用于移动平台)。不同平台拥有不同的编译器,寄存器,识别不同的指令。例如图片最后一行将 8 赋值给变量 eax 就有不同的写法。正是因为汇编指令的不同,才造成了平台之间的不兼容性

图文并茂,带你认识 JVM 运行时数据区-LMLPHP

而我们的 JVM 就充当了字节码文件根据不同平台翻译成不同汇编指令的翻译官,解决了跨平台的问题

图文并茂,带你认识 JVM 运行时数据区-LMLPHP

声明:本文首发于博客园,作者:后青春期的Keats;地址:https://www.cnblogs.com/keatsCoder/ 转载请注明,谢谢!

为什么众多语言选择 JVM

目前市面上已经有包括 Java、Kotlin、Groovy 等 10 多种语言都基于 JVM 运行了,不同的语言能够使用其编译器将源代码通通编译成 .class 字节码文件然后交给 JVM。为什么它们都选择 JVM 呢?

  • JVM 拥有优秀的内存管理模型,JDK8 及以前分代内存模型非常成熟、而且可以配合各种各样垃圾回收器做垃圾回收
  • 字节码指令非常精简 + 执行引擎效率非常高
  • 类加载器系统安全、可扩展
  • 高性能 + 低延迟的垃圾回收器

Java 是短暂的,JVM 是永恒的 --- 鲁班学院子牙老师

JVM 内存模型

图文并茂,带你认识 JVM 运行时数据区-LMLPHP

了解 JVM 内存模型,首先要搞清楚这四个概念

  • .class文件:.java 文件通过编译器编译后,存储在硬盘上的文件
  • class content:类加载器将硬盘中的 .class 文件加载到直接内存中的那块区域之后就成为 class content,此时 class content 内容和 class 文件是一样的
  • class 对象:类加载器基于虚拟机规范将内存中的 class content 解析成 class 对象,放入 方法区 中
  • 对象:执行引擎在执行 new 操作的时候,会将 class 对象生成对象,放入 堆 中

方法区

方法区是模型,具体的典型实现有 Hotspot 在 1.8 之前的 永久代实现 和 1.8及以后的 元空间 实现

永久代实现是 HotSpot 的设计团队选择把垃圾回收器的分代设计扩展至方法区。使用永久代来实现方法区。使得 Hotspot 的垃圾回收器能像管理 Java 堆一样管理方法区的这部分内存,省去专门为方法区编写内存管理代码的工作。 因为永久代有最大内存限制,这种设计导致 Java 更容易 oom。而 元空间 策略只要不触及物理内存上限就没多大事

方法区为什么由永久代实现改成元空间实现?

在 JDK1.8 以前,市面上的操作系统大部分还是32位,而32位操作系统最大支持 4GB 内存,这时如果程序出现死循环或其他原因而疯狂创建新对象占用内存空间,则硬件内存很容易被撑爆。因此 JVM 通过 永久代 实现来管理内存,紧紧把我内存的使用权限;随着硬件的发展,64 位操作系统占据主流市场,最大支持 256TB 内存,市面上主流机器的内存也不断增加。另外 Spring 等框架在一启动就会创建很多的 class 对象,JVM 管理起来很吃力。因此 JVM 在 JDK 1.8 之后索性放松这块的限制,变成了 元空间 实现

程序计数器

关键字:线程私有 不会OOM 字节码行号指示器

记录着虚拟机栈中每个方法执行的位置,可以看作是当前线程所执行的字节码文件的行号指示器。 Java 程序中分支、循环、跳转、异常处理、线程恢复等操作都需要程序计数器才能完成。

Java 虚拟机的多线程是由多线程轮流切换、分配处理器执行时间来实现的。在任何一个确定的时刻,一个内核都只会执行一个线程。因此 Java 中每个线程都有自己的程序计数器来确保线程切换后能回到准确的位置

虚拟机栈

关键字:线程私有 生命周期同线程 栈帧

图文并茂,带你认识 JVM 运行时数据区-LMLPHP

每个线程都有自己的虚拟机栈,局部变量是存储在虚拟机栈中的,因此不存在并发问题

每个虚拟机栈又有很多个栈帧,每个方法在执行的时候,虚拟机会同步创建一个栈帧,包含了:局部变量表、操作数栈、动态链接、方法出口/返回地址(恢复现场)、附加信息 这些部分

  • 局部变量表:存储该方法编译期可知的各种 Java 基本类型、对象引用和 returnAddress 类型。这些数据类型在局部变量表中的存储空间以局部变量槽(Slot)来表示,其中 64 位长度的 long、double 类型占 2 个槽,其余的数据类型占 1 个
  • 操作数栈:变量赋值操作等号右边的部分
  • 动态链接:指向该方法在方法区的地址,虚拟机图示中虚拟机栈指向方法区的棕色箭头便是表示的动态链接
  • 返回地址(恢复现场) 该方法弹栈后,恢复现场进行了如下的操作
    • 局部变量表指针重置
    • 操作数栈指针重置
    • 返回值压栈
    • 该方法占用的栈帧内存回收
    • 程序计数器的数值重置
  • 附加信息:HotSpot 并没有对此进行实现

当线程请求的栈深度大于虚拟机允许的深度时,会报 StackOverflowError 异常

当无法申请到足够的内存时,会 OOM

本地方法栈

和虚拟机栈类似,区别在于运行的是 native 方法,HotSpot 中把本地方法栈和虚拟机栈合二为一

Java堆

关键字:线程共享 垃圾分代收集

图文并茂,带你认识 JVM 运行时数据区-LMLPHP

所有的对象实例及数组几乎都在堆中分配

为什么老年代的空间是新生代的 2 倍?

老年代存在两种对象:

  • 在新生代经过 15 次 GC 还没有被回收的对象
  • 大小超过伊甸园(Eden区)的大对象,直接放到老年代。避免了对象在新生代三个区之间的复制、避免了新生代三个区被撑爆

可以看出老年代存储的都是大对象 / 老对象。另外老年代也是一种空间担保机制,避免由于新生代空间的限制导致的内存问题。因此需要更大的内存空间

新生代 Eden 和 From、To 区的内存比例为什么是 8:1:1

新生成的对象放入 Eden 区,经过一次 GC 后存活就会放到下一个区。根据大部分的数据统计,90% - 95% 的对象逃不过第一次 GC (朝生夕死),取最小值 90%,得到的两个区的比例是 9:1,但由于这样回收会产生很多内存碎片,导致内存有空间但却不可用。造成了内存浪费。因此将新生代再分成两个区并使用复制算法,一次释放一片内存。就形成了现在的三个区 8:1:1 的比例

内存碎片是在 Eden 产生的还是 From / To?

只要有垃圾回收,就会有碎片产生。

复制算法的细节是什么?怎样避免内存碎片的?

复制算法是将内存分为等大的区域,比如 from 和 to,每次回收前只使用其中一个。当进行一次垃圾回收后,这个区域的内存被完整释放。而存活的对象就被复制到另一个区域。这样就避免了内存碎片的产生

虚拟机栈和Java堆的联系

当我们在方法中执行这样一行代码:

Person p = new Person();

此时变量 p 会被存储在虚拟机栈栈帧的局部变量表中,而 Person 对象则存放在堆中。虚拟机图示中的虚拟机栈指向堆的黄色箭头则表示 p 到 Person 对象的引用关系

堆和方法区之间的联系

堆中存储的对象的对象头的类型指针存在方法区中

对于静态变量,例如 private static Person staticPerson = new Person(); class 对象存储在方法区中,而他的静态变量 staticPerson 指向的对象则存储在堆中

参考文献

B站鲁班学院视频,子牙老师讲解 JVM: https://www.bilibili.com/video/BV1BC4y187Ti?p=19

《深入理解Java虚拟机》--- 周志明

码字不易,如果你觉得读完以后有收获,不妨点个推荐让更多的人看到吧!

07-30 20:45