跳至正文
来两杯美式
返回

一文拿下 JVM(上):内存区域全景

By 来两杯美式
发布于

写在 2017 年的笔记。当年 Java 8 已经普及,方法区从「PermGen」换成了「Metaspace」,G1 也终于能稳定扛生产了。这一篇先把 JVM 内存区域讲清楚。

一、JVM 是什么

JVM(Java Virtual Machine),一种用于计算设备的规范。它是虚构出来的计算机——通过在真实计算机上仿真模拟各种计算机功能来实现。引入 JVM 后,Java 语言在不同平台上运行时不需要重新编译,Java 语言编译程序只需要生成在 JVM 上运行的目标代码(字节码)就可以在多种平台运行。

换句话说:JVM 是 Java 跨平台能力的核心,也是 Java 程序员必须搞懂的基础。

二、JVM 模块构成

JVM 大致可以拆成五块:

  1. 类加载子系统:把 .class 文件加载进内存
  2. 运行时数据区(内存空间):线程共享 + 线程私有两块
  3. 执行引擎:对字节码指令进行解释执行 / JIT 编译执行
  4. 本地方法接口(JNI):调用 native 方法(C/C++ 实现)
  5. 垃圾收集模块(GC):自动管理堆内存

这一篇重点讲「运行时数据区」——它是我们排查内存问题(OOM / 栈溢出 / 性能瓶颈)的核心。

三、运行时数据区全景

JVM 运行时数据区与执行引擎、本地库接口的关系

线程共享:堆(Heap)、方法区(Method Area / Metaspace) 线程私有:PC 寄存器、虚拟机栈、本地方法栈

这是排查 JVM 问题时第一张要画的图——问题出在哪个区,决定了你用什么工具

四、堆(Heap)

堆区是所有线程共享的区域。分为 Young 区 和 Old 区:

「S0/S1 同时只有一个有数据」是为了解决 Survivor 区碎片化问题。GC 时把存活对象从 S0 复制到 S1,然后清空 S0——下个周期反过来。这种算法叫「复制算法」,对应「年轻代」的场景。

五、虚拟机栈(JVM Stack)

每当启动一个新线程时,Java 虚拟机都会为它分配一个 Java 栈。Java 栈以帧(Stack Frame)为单位保存线程的运行状态。

虚拟机只会直接对 Java 栈执行两种操作:以帧为单位的压栈和出栈

5.1 栈帧的组成

每个方法从调用到执行完成,对应一个栈帧的入栈到出栈过程。一个栈帧包含四部分:

部分作用
局部变量表(Local Variable Table)存放方法参数、方法内定义的局部变量
操作数栈(Operand Stack)字节码指令的工作区,方法的参数传递、运算都通过它
动态链接(Dynamic Linking)指向运行时常量池的方法引用,支持多态
方法返回地址(Return Address)方法正常返回或异常退出时,回到调用方的位置

5.2 局部变量表

存放基本数据类型、对象引用、returnAddress 类型(指向一条字节码指令的地址)。

局部变量表与操作数栈的位置关系

其中:

5.3 操作数栈

操作数栈的工作流程

也被称为操作栈。Java 没有寄存器,所有的参数传递都是用操作数栈。

同局部变量表一样,也被组织成一个以字长为单位的数组。但与局部变量表不同,它不是通过索引来访问,而是通过标准的栈操作:压栈和出栈

比如某一指令将值压到操作数栈,另一指令就能弹栈获取这个值。虚拟机在操作数栈中存储数据的方式和在局部变量区中是一样的:如 intlongfloatdoublereferencereturnType 等。

一个小贴士:i++ 和 ++i 的字节码差异仅在 iload(压栈)和 iinc(自增)的顺序上——前者先 iloadiinc,后者先 iinciload两者都不是原子操作,在多线程下都需要同步(synchronizedAtomicInteger)。它们之间不存在”可见性差异”——这是网上流传的一个误解。

5.4 动态链接

栈帧内部的一个引用,指向运行时常量池中该栈帧所属方法的引用。支持多态调用——子类重写父类方法时,invokevirtual 指令通过动态链接决定调用哪个版本。

六、程序计数器(PC Register)

程序计数器在内存中的位置

程序计数器是一块较小的内存,可以看做是当前线程所执行的字节码行号指示器

字节码解释器工作的时候,就是通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。

七、本地方法栈(Native Method Stack)

与 Java 虚拟机栈相似,为虚拟机使用的 Native 方法服务

在 HotSpot 虚拟机中,本地方法栈和虚拟机栈合二为一。其他虚拟机(如 Sun HotSpot 之外的部分 JVM)可能仍然分开。

八、堆和栈的区别

维度
管理方式需要 GC 回收自动释放(方法出栈即销毁)
空间大小大(GB 级别)小(MB 级别)
碎片相关碎片较多碎片远小于堆
分配方式只支持动态分配支持静态 + 动态分配
效率差别高(仅次于寄存器)
线程归属线程共享线程私有

「栈上分配」是 JVM 优化手段——通过逃逸分析判定对象不会逃逸出方法时,把对象直接拆成标量在栈帧上分配,从而不进入堆。这个话题下一篇细讲。

九、小结:JVM 内存区域速记

区域线程归属抛错调优关键参数
程序计数器私有不会
虚拟机栈私有StackOverflowError / OOM-Xss
本地方法栈私有StackOverflowError / OOM
共享OOM-Xms / -Xmx / -Xmn
方法区共享OOM-XX:MetaspaceSize / -XX:MaxMetaspaceSize

下一篇「一文拿下 JVM(中):参数配置与常用命令」将围绕如何给这块内存配参数展开——-Xms 该设多大、-Xmn 该留多少、G1 该不该开。


分享这篇文章:
通过邮件分享这篇文章✓ 链接已复制
所属专题
JVM
第 1 / 6 篇
查看系列全部文章
  1. 01.一文拿下 JVM(上):内存区域全景
  2. 02.一文拿下 JVM(中):参数配置与常用命令
  3. 03.一文拿下 JVM(下):逃逸分析与代码优化
  4. 04.一文拿下 JVM(番外):OOM 排查与 MAT / JVisualVM 实战
  5. 05.一文拿下 JVM(终章):对象创建的 7 个步骤
  6. 06.一文拿下 JVM(锁篇):synchronized 锁的升级与三种锁对比

上一篇
一文拿下 JVM(中):参数配置与常用命令
下一篇
mac 系统的 IntelliJ IDEA 快捷键大全