深入理解JVM运行时内存结构(续)

JVM 以方法作为执行的基本单位,栈帧(Stack Frame)则是用于支持 JVM 进行方法调用和执行的数据结构。每个方法在执行的同时都会创建一个栈帧用于存储方法的局部变量表、操作数栈、动态连接、方法返回地址和一些额外的附加信息。每一个方法从调用开始至执行结束,都对应着一个栈帧在虚拟机栈里面从入栈到出栈的过程。当方法调用结束,栈帧即被销毁,局部变量表、操作数栈等也随之消失。

Java 代码在编译时就已经计算出栈帧需要多大的局部变量表,需要多深的操作数栈,并把它们写入到方法表的 Code 属性之中。换句话说,一个栈帧需要分配多少内存,在编译时已经确定,并不会受到程序运行期间变量数据的影响,仅取决于源码和具体的虚拟机是如何实现栈帧的。

深入理解 JVM 对象模型

深入理解JVM运行时内存结构 中,我们宏观地探讨了 JVM 运行时的内存布局。本文将深入分析每个内存区域的细节。由于不同虚拟机的内存管理实现不同,具体的讨论需要聚焦于特定虚拟机和内存区域。基于实用优先的原则,我们以最常用的 HotSpot 虚拟机和 Java 堆内存为例,深入探讨在 HotSpot 虚拟机里:

  1. 对象在堆内存中是如何布局的?
  2. JVM 是如何实现 Java 对象?
  3. 对象是如何创建出来的?

对象的内存布局

在虚拟机里,对象由三部分构成,分别是 对象头实例数据对齐填充。对象头的结构复杂,下面会详细介绍。实例数据是对象真正存储的有效信息,包含所有我们在代码中定义的各种类型的字段的内容,无论是继承自父类还是在子类中定义的。最后是对齐填充,由于HotSpot虚拟机的自动内存管理系统要求对象起始地址必须是8字节的整数倍,换句话说就是任何对象的大小都必须是8字节的整数倍。对象头部分已经被精心设计成正好是8字节的倍数,如果对象实例数据部分没有对齐的话,就需要通过对齐填充来补全。

接下来,我们详细说说对象头。HotSpot 虚拟机对象的对象头由三部分构成,分别是存储对象自身运行时数据的 Mark Word、指向类型元数据的指针、当对象是数组时,记录数组长度的 length。在 64 位系统中,对象头的大小是 16 个字节,可以通过指针压缩的方式,压缩到 12 个字节。当 JVM 中存在大量对象的时候,通过指针压缩减少对象内存占用是一个提升性能的手段。

需要注意的是,并不是所有虚拟机在实现对象时,都会在对象头中保留类型指针,还可以通过其它方式来查找对象类型的元数据信息,这点会在最后的「对象的访问」小节中补充讨论。

深入理解 JVM 垃圾回收机制 - 何为垃圾?

随着编程语言的发展,GC 的功能不断增强,性能也不断提高,作为语言背后的无名英雄,GC 离我们的工作似乎越来越远。作为 Java 程序员,对这一点也许会有更深的体会,我们不需要了解太多与 GC 相关的知识,就能很好的完成工作。那还有必要深入了解 GC 吗?学习 GC 的意义在哪儿?

何为垃圾题图

不管性能提高到何种程度,GC 都需要花费一定的时间,对于实时性要求较高的场景,就必须尽量压低 GC 导致的最大暂停时间 (GC 会导致应用线程处于暂停状态),举两个例子:

  • 实时对战游戏:如果因为 GC 导致玩家频繁卡顿,任谁都会想摔手机吧。
  • 金融交易:在某些对价格非常敏感的交易(比如:外汇)场景下,如果因为 GC 导致没有按照投资者指定的价格进行交易,相信我,这些投资者非生吃了你。

但也有许多场景,GC 的最大暂停时间没那么重要,比如,离线分析、视频网站等等。因此,知道 这个 GC 算法有这样的特征,所以它适合这个场景,对程序员来说非常有价值,这也是我们学习 GC 最重要的意义。

接下来,我们将一步步走进 GC 的世界。

深入理解JVM运行时内存结构

得益于 JVM 的自动内存管理机制,开发者在写代码时,很少再去关注内存分配与释放。多数情况下,应用不会出现内存泄漏和溢出问题。不过,由于开发者把内存的控制权交给了 JVM,一旦出现内存泄露和溢出问题,如果不了解 JVM 是怎样使用内存的,将很难排查和修正错误。本文从概念上介绍 JVM 运行时内存的各个区域及其作用。

JVM 在执行程序时会把其所管理的内存划分成多个不同的数据区域,每个区域的创建时间、销毁时间以及用途都各不相同。比如有的内存区域是所有线程共享的,而有的内存区域是线程隔离的。线程隔离的区域就会随着线程的启动和结束而创建和销毁。JVM 所管理的内存将会包含以下几个运行时数据区域,如下图所示。
JVM运行时内存数据区示意图

深入理解JVM常量池与字节码

在 JVM 中,常量池可以分成 Class 文件常量池、运行时常量池、字符串常量池三类。

Class 文件常量池

Java 源文件经编译后得到存储字节码的 Class 文件,Class 文件是一组以 8 位字节为基础单位的二进制流,各个数据项目严格按照顺序紧凑地排列在 Class 文件中。也就是说,哪个字节代表什么含义,长度多少,先后顺序如何都是被严格限定的,是不允许改变的。比如:开头的 4 个字节存放魔数,用于确定这个文件是否能够被 JVM 接受,接下来的 4 个字节用于存放版本号,再接着存放的就是常量池。常量池的长度是不固定的,所以,在常量池的入口存放着常量池容量的计数值。

常量池主要用于存放两大类常量:字面量和符号引用量,字面量相当于 Java 语言层面常量的概念,比如:字符串常量、声明为 final 的常量等等。符号引用是用一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。