前言
上一篇我们从整体上建立了 JVM 的知识地图:
text
JVM
│
├── 类加载
├── 运行时数据区
├── 执行引擎
├── GC
└── 生产诊断
这一篇开始深入其中最核心的一部分:
JVM 运行时内存。
很多 JVM 问题最终都会回到内存。
例如:
text
为什么会 Full GC?
对象到底存在哪里?
为什么递归会 StackOverflow?
-Xmx 明明只配置了 1.5G,
为什么 Docker 2G 还是 OOMKilled?
Heap 没满,
为什么还能发生 OOM?
Metaspace 到底是不是 Heap?
Direct Memory 又是什么?
如果 JVM 内存区域没有真正理解清楚,后面学习:
text
GC
G1
OOM
Heap Dump
JVM 调优
都会比较吃力。
所以这一篇的目标很明确:
把 JVM 内存的整体结构、每块区域存什么、什么时候创建、是否线程共享、可能出现什么故障,一次串起来。
一、先纠正一个常见叫法:JVM 内存结构 ≠ JMM
很多文章或者面试中会问:
JVM 内存模型是什么?
这个说法其实容易产生歧义。
我们需要区分两个概念。
1.1 JVM 运行时数据区
也就是本文主要讨论的:
text
Heap
Java 虚拟机栈
本地方法栈
程序计数器
方法区
它解决的问题是:
Java 程序运行过程中,数据分别存在哪里。
1.2 Java Memory Model
也就是:
text
JMM
Java Memory Model
Java 内存模型
它主要讨论:
text
多线程之间如何共享数据
线程什么时候能看到其他线程修改的数据
指令是否允许重排序
volatile 为什么能保证可见性
synchronized 为什么既能互斥又能保证可见性
happens-before 是什么
所以:
text
JVM 运行时数据区
≠
JMM
可以简单记成:
text
JVM 内存区域
→ 数据放在哪里
JMM
→ 多线程之间怎么正确访问共享数据
我的 JMM 内容会放在并发编程专题中。
本文只讨论:
JVM 运行时内存结构。
二、JVM 运行时数据区整体结构
按照 JVM 规范,可以先把运行时数据区分成:
text
JVM Runtime Data Area
│
├── 线程共享
│ │
│ ├── Heap
│ │
│ └── Method Area
│
└── 线程私有
│
├── Program Counter Register
│
├── Java Virtual Machine Stack
│
└── Native Method Stack
翻译成中文:
text
JVM 运行时数据区
│
├── 线程共享
│ ├── Java 堆
│ └── 方法区
│
└── 线程私有
├── 程序计数器
├── Java 虚拟机栈
└── 本地方法栈
除此之外,在实际 HotSpot JVM 和生产排查中,我们还经常会关注:
text
Metaspace
Direct Memory
Code Cache
Native Memory
Thread Stack
因此从生产视角来看,一个 Java 进程占用的内存远远不只是:
text
Heap
而更接近:
text
Java Process
│
├── Heap
│
├── Metaspace
│
├── Thread Stack
│
├── Direct Memory
│
├── Code Cache
│
└── JVM Native Memory
这张图非常重要。
后面很多 OOM 问题都要从这里出发。
三、为什么要区分"线程共享"和"线程私有"?
假设一个 Java 服务同时有:
text
Thread A
Thread B
Thread C
它们都会在同一个 JVM 进程中运行。
但是不同内存区域的生命周期是不一样的。
3.1 线程共享区域
例如:
text
Heap
Method Area
所有线程都可以共同访问。
例如:
java
User user = new User();
这个 User 对象通常位于 Heap。
不同线程只要拿到它的引用,就都可能访问这个对象。
所以:
text
Heap
→ 多线程共享
这也是为什么共享对象会产生线程安全问题。
3.2 线程私有区域
例如:
text
Java 虚拟机栈
程序计数器
本地方法栈
每创建一个线程,就会拥有自己独立的一套。
可以理解为:
text
Thread A
├── PC Register A
├── Java Stack A
└── Native Stack A
Thread B
├── PC Register B
├── Java Stack B
└── Native Stack B
Thread A 不会直接使用 Thread B 的 Java 栈。
所以:
线程创建得越多,除了线程调度成本增加,也会消耗更多线程栈内存。
这一点后面排查:
text
unable to create new native thread
时非常重要。
四、程序计数器
程序计数器英文:
text
Program Counter Register
简称:
text
PC Register
它是 JVM 中非常小的一块内存。
但是它非常重要。
4.1 程序计数器是干什么的?
Java 是多线程执行的。
假设线程 A 正在执行:
text
第 100 条字节码
这时操作系统进行线程切换:
text
Thread A
↓
暂停
Thread B
↓
运行
Thread B
↓
暂停
Thread A
↓
继续
线程 A 再次获得 CPU 后,必须知道:
我刚才执行到哪里了?
程序计数器就负责记录类似这样的执行位置信息。
可以简单理解:
text
Thread A
PC → bytecode 100
Thread B
PC → bytecode 260
所以:
程序计数器是线程私有的。
每个线程都有自己的程序计数器。
4.2 为什么程序计数器通常不会 OOM?
程序计数器需要保存的信息很少。
它不像 Heap 一样不断创建大量对象。
所以在 JVM 规范定义的运行时区域中:
程序计数器通常也是唯一没有规定 OutOfMemoryError 场景的区域。
生产排查中,我们基本不会把:
text
OOM
首先定位到程序计数器。
五、Java 虚拟机栈
Java 虚拟机栈:
text
Java Virtual Machine Stack
是 JVM 面试和生产排查中非常重要的一部分。
它和:
text
Java 方法调用
密切相关。
六、每个线程都有自己的 Java 栈
假设程序有三个线程:
text
Thread A
Thread B
Thread C
那么可以简单理解为:
text
Thread A
└── Java Stack A
Thread B
└── Java Stack B
Thread C
└── Java Stack C
所以 Java 栈:
线程私有。
它的生命周期基本与线程一致。
线程创建:
text
创建 Stack
线程结束:
text
Stack 随之释放
七、什么是栈帧?
线程每调用一个 Java 方法,就会创建一个:
text
Stack Frame
栈帧
例如:
java
public void methodA() {
methodB();
}
public void methodB() {
methodC();
}
public void methodC() {
}
执行:
text
methodA()
以后:
text
methodA()
↓
methodB()
↓
methodC()
对应 Java Stack:
text
Java Stack
│
├── methodC Stack Frame
├── methodB Stack Frame
└── methodA Stack Frame
当:
text
methodC()
执行完成:
text
methodC 栈帧出栈
然后继续执行:
text
methodB
所以方法调用本质上存在:
text
入栈
↓
执行
↓
出栈
的过程。
八、栈帧里面放什么?
一个栈帧主要包含:
text
局部变量表
操作数栈
动态链接
方法返回信息
其中最容易理解的是:
text
局部变量表
例如:
java
public void test() {
int age = 18;
User user = new User();
}
可以粗略理解:
text
Java Stack
│
└── test Stack Frame
│
├── age = 18
│
└── user = reference
而:
text
User 对象
通常存在 Heap。
所以我们经常看到:
text
Stack
└── Reference
│
↓
Heap
└── Object
九、为什么无限递归会 StackOverflow?
例如:
java
public void test() {
test();
}
执行后:
text
test()
↓
test()
↓
test()
↓
test()
↓
test()
↓
...
每调用一次:
text
test()
都会创建新的栈帧。
Java Stack 会不断增长:
text
Java Stack
[test]
[test]
[test]
[test]
[test]
[test]
...
最终栈空间不足:
text
java.lang.StackOverflowError
这就是经典的:
text
StackOverflowError
十、StackOverflow 常见原因
生产中比较常见的原因包括:
text
无限递归
递归深度过大
循环调用
A → B → C → A
例如:
java
public void methodA() {
methodB();
}
public void methodB() {
methodA();
}
最终:
text
A
↓
B
↓
A
↓
B
↓
A
↓
...
同样会不断创建栈帧。
十一、怎么排查 StackOverflow?
StackOverflow 一般比较容易排查。
异常通常直接显示:
text
java.lang.StackOverflowError
然后看异常堆栈。
如果看到:
text
UserService.test()
UserService.test()
UserService.test()
UserService.test()
UserService.test()
UserService.test()
同一个方法重复成百上千次:
基本就可以判断存在递归问题。
所以 StackOverflow 的排查重点不是:
text
Heap Dump
而是:
text
Exception Stack Trace
十二、线程栈大小和 -Xss
Java 线程栈大小可以通过:
text
-Xss
控制。
例如:
text
-Xss1m
可以简单理解为:
每个 Java 线程允许使用的栈空间上限大约是 1MB。
因此如果创建:
text
1000 个线程
理论上光线程栈就可能需要非常可观的内存。
这也是为什么:
线程不是免费的。
十三、线程太多为什么也可能导致内存问题?
假设容器限制:
text
2GB
Java Heap:
text
-Xmx1200m
看起来似乎还有:
text
800MB
但如果:
text
-Xss1m
同时创建:
text
500 个线程
那么线程栈本身就可能消耗数百 MB 级别的地址空间和实际内存。
还没有计算:
text
Metaspace
Direct Memory
Code Cache
JVM Native Memory
所以:
text
Heap 没满
并不意味着:
text
Java 进程整体内存安全
十四、Unable to create new native thread
如果系统无法继续创建线程,还可能看到:
text
java.lang.OutOfMemoryError:
unable to create new native thread
这和:
text
Java heap space
不是一个问题。
常见原因可能包括:
text
线程创建过多
进程线程数达到系统限制
Native Memory 不足
容器内存不足
所以遇到这种错误,重点应该看:
text
线程数量
-Xss
容器内存
系统线程限制
而不是一看到 OOM 就去分析 Heap Dump。
十五、本地方法栈
本地方法栈:
text
Native Method Stack
和 Java 虚拟机栈类似。
区别主要在于:
text
Java Virtual Machine Stack
→ Java 方法
Native Method Stack
→ Native 方法
Java 可以通过:
text
JNI
Java Native Interface
调用底层本地代码。
例如:
text
C
C++
实现的方法。
这些 Native Method 的执行就可能涉及本地方法栈。
在 HotSpot 的具体实现中:
Java 栈和本地方法栈的实现细节可能不像规范概念划分得那么完全独立。
但是面试中首先理解逻辑区别即可。
十六、Java Heap
Java Heap:
text
Java 堆
是 JVM 内存中最重要的一部分。
也是 GC 最主要管理的区域。
它最大的特点:
text
线程共享
所有线程都可能访问 Heap 中的对象。
十七、Heap 主要放什么?
最常见的是:
text
对象实例
数组
例如:
java
User user = new User();
通常可以理解:
text
Stack
│
└── user Reference
│
↓
Heap
└── User Object
再例如:
java
int[] array = new int[100];
数组对象也通常位于:
text
Heap
十八、对象一定在 Heap 吗?
初学 JVM 时通常会说:
Java 对象都创建在 Heap。
这个说法作为入门理解没有问题。
但如果严格来说:
不应该机械理解成"所有对象在任何情况下都必须真的在 Heap 中分配完整对象"。
现代 JVM 会进行很多优化,例如:
text
逃逸分析
Escape Analysis
如果 JVM 能证明某个对象:
text
不会逃逸当前方法
理论上可以进一步进行:
text
标量替换
锁消除
等优化。
所以比较严谨的说法是:
从 Java 内存语义和常规理解上,对象主要位于 Heap;但 JIT 优化后,某些对象可能被优化掉,不能简单理解成所有
new都必然对应 Heap 上一个完整对象。
面试中通常说到这里已经足够。
十九、Heap 为什么需要 GC?
假设程序不断:
java
new User();
new User();
new User();
new User();
如果对象永远不释放:
text
Heap
↓
越来越满
↓
没有空间
↓
OOM
所以 JVM 需要:
text
Garbage Collector
找出:
text
已经没有业务需要的对象
然后释放它们占用的空间。
所以:
text
Heap
也是 GC 的核心管理区域。
二十、年轻代和老年代
很多 GC 使用:
text
Generational Collection
分代收集
的思想。
最经典的理解:
text
Heap
│
├── Young Generation
│ │
│ ├── Eden
│ ├── Survivor 0
│ └── Survivor 1
│
└── Old Generation
为什么需要分代?
因为大量 Java 对象都有这样的特点:
生命周期非常短。
例如:
java
List<UserDTO> result = new ArrayList<>();
一个接口执行过程中可能产生大量:
text
DTO
String
List
Map
JSON 对象
临时对象
请求结束以后,很快就没有引用了。
所以:
text
大量对象
→ 朝生夕死
这是分代 GC 的重要依据。
二十一、对象通常怎么流转?
可以先建立这样的理解:
text
new Object()
↓
Eden
↓
Young GC
↓
对象还活着
↓
Survivor
↓
继续经历 GC
↓
长期存活
↓
Old Generation
但是要注意:
这是一张帮助理解的简化图。
不同垃圾收集器的具体实现存在差异。
尤其是:
text
G1
并不是把年轻代和老年代永久划成两块固定连续空间。
G1 使用:
text
Region
动态承担:
text
Eden
Survivor
Old
Humongous
等角色。
所以:
text
年轻代 / 老年代
是重要的逻辑概念,但不要把它机械理解成所有 GC 都有完全相同的物理布局。
二十二、Heap OOM
Heap 无法继续满足对象分配时,最典型的异常就是:
text
java.lang.OutOfMemoryError:
Java heap space
常见原因包括:
text
内存泄漏
Heap 配置太小
一次查询太多数据
缓存无限增长
大集合
大量大对象
并发量突然增大
对象分配速度远高于回收速度
二十三、Heap OOM 不等于内存泄漏
这是生产排查非常重要的一点。
假设出现:
text
Java heap space
不能直接说:
内存泄漏了。
因为 OOM 可能只是:
text
-Xmx512m
但是业务本身正常运行就需要:
text
800MB
这种属于:
text
Heap 容量不足
而不是:
text
对象错误地无法释放
只有当:
text
本来应该释放的对象
↓
因为错误引用关系
↓
长期无法被 GC 回收
时,才属于真正意义上的:
text
Memory Leak
二十四、方法区
方法区:
text
Method Area
是 JVM 规范中的逻辑概念。
它用于存储和:
text
Class
相关的数据。
例如:
text
类型信息
字段信息
方法信息
运行时常量池
方法字节码等
它和 Heap 一样:
text
线程共享
二十五、方法区 ≠ 永久代
这是 JVM 学习中一个非常常见的混淆。
正确关系应该是:
text
方法区
→ JVM 规范定义的逻辑概念
而:
text
PermGen
Metaspace
属于:
HotSpot 对方法区相关数据的具体实现方式。
所以不能简单说:
text
方法区就是永久代
或者:
text
方法区就是 Metaspace
更准确的说法:
方法区是 JVM 规范定义的区域,而永久代和 Metaspace 是 HotSpot 不同时期对相关功能的具体实现。
二十六、PermGen 永久代
早期 HotSpot JVM 使用:
text
PermGen
Permanent Generation
永久代
保存大量和 Class 相关的数据。
但是永久代存在一些问题。
例如:
text
容量需要额外配置
动态类加载容易撑满
ClassLoader 泄漏容易造成 OOM
最终 Java 8 中:
text
PermGen 被移除
二十七、Metaspace 元空间
Java 8 以后,HotSpot 使用:
text
Metaspace
存储类元数据。
最大的变化之一:
Metaspace 使用 Native Memory,而不是 Java Heap。
所以:
text
-Xmx
并不能直接限制 Metaspace。
二十八、Metaspace 存什么?
可以简单理解为主要保存:
text
Class Metadata
也就是:
text
类的元数据
例如:
text
类结构
方法元数据
字段信息
继承关系
等 Class 相关信息。
二十九、Metaspace OOM
如果加载的 Class 数量持续增长:
text
Class
Class
Class
Class
Class
...
Metaspace 也会越来越大。
最终可能出现:
text
java.lang.OutOfMemoryError:
Metaspace
常见原因:
text
大量动态生成 Class
ClassLoader 泄漏
大量动态代理
字节码增强
频繁热部署
脚本引擎动态生成类
例如一些框架可能使用:
text
CGLIB
ByteBuddy
动态代理
生成 Class。
正常情况下问题不大。
但是如果:
text
不断创建新的 ClassLoader
+
旧 ClassLoader 一直不能释放
就可能导致:
text
Class 无法卸载
↓
Metaspace 不断增长
↓
Metaspace OOM
三十、怎么观察 Metaspace?
后面 JVM 排障专题会详细讲工具。
这里先知道两个方向。
例如:
bash
jstat -class PID 1000
可以观察:
text
Loaded
Unloaded
等类加载情况。
还可以:
bash
jcmd PID VM.native_memory summary
查看 JVM Native Memory。
不过:
text
VM.native_memory
通常要求 JVM 启动时开启:
text
Native Memory Tracking
所以生产环境需要提前考虑。
三十一、运行时常量池
运行时常量池:
text
Runtime Constant Pool
属于方法区的逻辑组成部分之一。
Class 文件编译以后会存在:
text
Constant Pool
常量池
类被加载进 JVM 后,会形成:
text
Runtime Constant Pool
它里面可能包含:
text
字面量
符号引用
方法引用
字段引用
等信息。
三十二、字符串常量池在哪里?
这个问题面试经常问:
String Constant Pool 在哪里?
在现代 HotSpot 中,例如 Java 8 / Java 17:
字符串常量池位于 Heap 中。
例如:
java
String s1 = "hello";
String s2 = "hello";
s1 和 s2 通常会指向字符串常量池中的同一个:
text
"hello"
对象。
需要注意:
字符串常量池和运行时常量池不是完全相同的概念。
不要简单把:
text
String Pool
Runtime Constant Pool
当成一个东西。
三十三、Direct Memory
直接内存:
text
Direct Memory
又经常被称为:
text
堆外内存
Off-Heap Memory
它不是 JVM 规范定义的运行时数据区之一。
但是在生产环境非常重要。
三十四、为什么要使用 Direct Memory?
Java NIO 提供:
java
ByteBuffer.allocateDirect(...)
申请直接内存。
例如:
java
ByteBuffer buffer =
ByteBuffer.allocateDirect(1024);
和普通 Heap Buffer 相比,Direct Buffer 在某些 IO 场景中可以减少数据在:
text
Java Heap
↔
Native Memory
之间来回复制的开销。
所以:
text
Netty
NIO
高性能网络框架
经常大量使用 Direct Memory。
三十五、Direct Memory OOM
如果直接内存使用过多,可能出现:
text
java.lang.OutOfMemoryError:
Direct buffer memory
这种情况下:
text
Heap
甚至可能还很正常。
例如:
text
Heap 60%
但是:
Direct Memory 已耗尽
最终仍然发生 OOM。
所以看到:
text
OutOfMemoryError
不能只盯 Heap。
三十六、Code Cache
JVM 中还有一块很容易被忽略的区域:
text
Code Cache
前面提到过:
text
JIT
会把热点字节码编译成:
text
Native Machine Code
这些编译后的机器码就需要存放在:
text
Code Cache
中。
可以简单理解:
text
Hot Method
↓
JIT Compile
↓
Native Code
↓
Code Cache
普通 Java 后端开发不需要每天关注 Code Cache。
但如果进行完整 Java 进程内存分析,它同样属于:
text
Native Memory
的一部分。
三十七、一个 Java 进程到底占多少内存?
这是整篇文章非常重要的一部分。
假设:
text
-Xmx = 2GB
是否说明:
text
Java 进程最多占 2GB?
答案:
不是。
-Xmx 主要控制的是:
text
Maximum Java Heap Size
也就是:
text
Java Heap 最大值
Java 进程还需要:
text
Metaspace
Thread Stack
Direct Memory
Code Cache
GC Internal Structure
JNI
JVM Native Memory
所以更准确的模型:
text
Java Process Memory
│
├── Heap
│
├── Metaspace
│
├── Thread Stack
│
├── Direct Memory
│
├── Code Cache
│
├── GC Native Structures
│
└── JVM Native Memory
因此:
text
Process Memory
>
-Xmx
是完全正常的。
三十八、Docker 为什么经常把 JVM 内存问题放大?
假设:
text
Docker Memory Limit = 2GB
然后 JVM 设置:
text
-Xmx1800m
看起来:
text
2GB - 1.8GB
≈ 200MB
很多人会觉得:
还有 200MB,应该够了。
但是还没有计算:
text
Metaspace
Thread Stack
Direct Memory
Code Cache
Native Memory
最终可能:
text
Heap = 1.6GB
Metaspace = 150MB
Thread Stack = 200MB
Direct Memory = 200MB
Other Native = 100MB
整个进程已经超过:
text
2GB
最终:
text
cgroup memory limit
↓
OOM
↓
Linux Kill Process
↓
Java Process disappears
这就是:
text
Container OOMKilled
三十九、Container OOM 和 Heap OOM 的区别
这两种一定要区分。
Heap OOM
JVM 自己发现:
text
Java Heap 没有足够空间
通常可以看到:
text
java.lang.OutOfMemoryError:
Java heap space
Container OOM
操作系统 / cgroup 发现:
text
整个 Java 进程使用内存
>
容器限制
然后直接:
text
kill Java process
这种情况下:
JVM 甚至可能没有机会抛出
Java heap space。
所以:
text
服务突然没了
不一定是:
text
Java Heap OOM
还要检查:
text
Docker OOMKilled
Linux OOM Killer
cgroup Memory Limit
四十、几种常见内存问题怎么区分?
可以先建立这张表。
| 问题 | 典型区域 | 典型现象 |
|---|---|---|
| Heap OOM | Heap | Java heap space |
| Metaspace OOM | Metaspace | Metaspace |
| StackOverflow | Java Stack | StackOverflowError |
| Thread 创建失败 | Native / Thread Stack | unable to create new native thread |
| Direct Memory OOM | Direct Memory | Direct buffer memory |
| Container OOM | 整个 Process | Java 可能直接被系统杀掉 |
四十一、用一张图记住
最简单的记忆方式:
text
对象太多
↓
Heap
↓
Java heap space
text
Class 太多
↓
Metaspace
↓
Metaspace OOM
text
方法调用太深
↓
Java Stack
↓
StackOverflowError
text
线程太多
↓
Thread Stack / Native Resource
↓
unable to create new native thread
text
DirectBuffer 太多
↓
Direct Memory
↓
Direct buffer memory
text
整个 Java Process 太大
↓
超过 Container Limit
↓
OOMKilled
四十二、堆、栈、元空间到底有什么区别?
把最重要的几块放在一起:
| 内存区域 | 线程关系 | 主要存什么 | 典型故障 |
|---|---|---|---|
| Heap | 共享 | 对象、数组 | Java heap space |
| Java Stack | 私有 | 栈帧、局部变量等 | StackOverflowError |
| PC Register | 私有 | 当前执行位置 | 一般不关注 OOM |
| Native Method Stack | 私有 | Native 方法调用 | 栈相关问题 |
| Method Area | 共享 | 类相关逻辑数据 | 取决于具体实现 |
| Metaspace | 共享 | Class Metadata | Metaspace OOM |
| Direct Memory | JVM 堆外 | DirectBuffer 等 | Direct buffer memory |
| Code Cache | JVM Native | JIT 编译机器码 | Code Cache 压力 |
四十三、创建一个对象时到底发生了什么?
现在把本文知识串起来。
例如:
java
User user = new User();
大致可以理解:
text
执行 new 指令
↓
检查 User Class 是否加载
↓
如果没有
↓
ClassLoader 加载
↓
Class Metadata
进入 Metaspace
↓
Heap 分配 User 对象
↓
Stack Frame 中
保存 user Reference
↓
Reference 指向 Heap Object
于是:
text
Metaspace
└── User Class Metadata
Java Stack
└── user reference
│
↓
Heap
└── User Object
这就把:
text
类加载
Metaspace
Stack
Heap
第一次真正串起来了。
四十四、调用方法时又发生什么?
例如:
java
user.login();
大致:
text
当前线程
↓
Java Stack
↓
创建 login() Stack Frame
↓
执行字节码
↓
可能访问 Heap 中的 User Object
↓
方法执行结束
↓
Stack Frame 出栈
所以:
text
Stack
和:
text
Method Invocation
紧密相关。
而:
text
Heap
和:
text
Object Lifecycle
紧密相关。
四十五、对象没有引用以后会怎样?
例如:
java
User user = new User();
user = null;
此时原来的:
text
User Object
如果已经无法从任何:
text
GC Root
到达,那么它就可能成为:
text
Garbage
之后等待:
text
GC
回收。
所以整个过程其实是:
text
ClassLoader
↓
Class Metadata
↓
Metaspace
new
↓
Object
↓
Heap
Method Call
↓
Stack Frame
↓
Java Stack
Object unreachable
↓
GC
↓
Heap Space Reclaimed
到这里,JVM 内存、类加载和 GC 已经连接起来了。
四十六、为什么 Full GC 和 Heap 关系这么大?
当 Heap 尤其老年代出现较高内存压力时:
text
Old Generation
接近满
JVM 需要更积极地尝试回收对象。
如果出现:
text
Full GC
Full GC
Full GC
就需要进一步分析:
text
Heap 太小?
对象分配太快?
大量对象进入 Old?
内存泄漏?
大对象太多?
所以:
text
Full GC
并不是一个独立问题。
它背后本质还是:
JVM 内存供给和对象生命周期之间失去了平衡。
后面会单独写一篇:
【JVM生产实战】为什么会频繁 Full GC?内存泄漏、堆太小还是分配太快?
四十七、生产排查时为什么不能只看 Heap?
假设:
text
Java Process Memory = 1.95GB
Docker Limit = 2GB
这时候 Heap:
text
Heap Used = 900MB
你如果只看 Heap:
才用了不到 1GB,没问题。
但实际上:
text
Metaspace = 200MB
Direct Memory = 400MB
Thread Stack = 300MB
Other Native = 150MB
整个进程已经非常危险。
所以生产排查必须建立:
Memory Accounting
也就是:
text
内存账本
不能只问:
text
Heap 用了多少?
还要问:
text
整个 Java Process 用了多少?
四十八、JVM 内存排查的正确思路
以后遇到 OOM,不要马上说:
text
调大 -Xmx
应该先回答:
text
到底是哪块内存出了问题?
排查可以先分:
text
OOM
│
├── Heap
│ └── Java heap space
│
├── Metaspace
│ └── Metaspace
│
├── Stack
│ └── StackOverflowError
│
├── Thread / Native Resource
│ └── unable to create native thread
│
├── Direct Memory
│ └── Direct buffer memory
│
└── Container / Linux
└── OOMKilled
先分类:
再使用对应工具。
而不是:
所有内存问题都拿 Heap Dump 分析。
四十九、几个常见 JVM 参数先认识一下
这一篇不深入参数调优。
先认识几个和内存相关的参数:
text
-Xms
Java Heap 初始大小。
例如:
text
-Xms1g
text
-Xmx
Java Heap 最大大小。
例如:
text
-Xmx2g
text
-Xss
线程栈大小。
例如:
text
-Xss1m
text
-XX:MaxMetaspaceSize
限制 Metaspace 最大值。
例如:
text
-XX:MaxMetaspaceSize=256m
text
-XX:MaxDirectMemorySize
控制直接内存相关上限。
例如:
text
-XX:MaxDirectMemorySize=512m
需要注意:
JVM 参数不能脱离 JDK 版本、GC、容器资源和业务场景机械配置。
后续调优篇会继续展开。
五十、如果线上 OOM,第一反应应该是什么?
不要先问:
-Xmx多大?
第一句话应该是:
先确认到底是什么 OOM。
例如:
text
Java heap space
方向:
text
Heap
text
Metaspace
方向:
text
Class / ClassLoader
text
StackOverflowError
方向:
text
Call Stack / Recursive Call
text
Direct buffer memory
方向:
text
Direct Memory / NIO / Netty
Java 直接消失:
text
No Java OOM Log
方向:
text
Linux OOM Killer
Docker OOMKilled
cgroup Memory Limit
这个思维非常重要。
五十一、面试回答:JVM 内存区域有哪些?
如果面试官问:
JVM 内存区域有哪些?
可以这样回答:
JVM 运行时数据区可以分为线程共享和线程私有两部分。
线程共享的主要是 Heap 和 Method Area,Heap 主要存对象和数组,也是 GC 最主要管理的区域;Method Area 主要存类相关信息。
线程私有的主要有 Java 虚拟机栈、本地方法栈和程序计数器。Java 虚拟机栈和方法调用有关,每次方法调用都会创建栈帧;程序计数器记录当前线程执行到哪条字节码。
在 HotSpot 和生产排查中还需要重点关注 Metaspace 和 Direct Memory。Java 8 以后 HotSpot 使用 Metaspace 保存类元数据,它使用本地内存;NIO、Netty 等还可能大量使用 Direct Memory。
所以生产上不能认为
-Xmx就是整个 Java 进程的最大内存。
这一段已经能够覆盖大多数 JVM 内存面试问题的第一层。
五十二、面试继续追问:Heap 和 Stack 有什么区别?
可以回答:
Heap 是线程共享的,主要存对象和数组,由 GC 统一管理;Java Stack 是线程私有的,主要保存方法调用产生的栈帧。
Heap 不足常见的是
Java heap space;栈调用层级过深常见的是StackOverflowError。另外每创建一个线程都会有自己的线程栈,所以线程数量过多还可能消耗大量 Native Memory,甚至导致无法创建新的线程。
五十三、面试继续追问:Metaspace 和 Heap 有什么区别?
可以回答:
Heap 主要保存业务对象和数组,受
-Xmx等参数影响;Metaspace 主要保存类元数据,并且 Java 8 之后 HotSpot 的 Metaspace 使用本地内存,不属于 Java Heap。所以 Heap 还有空间,并不代表 Metaspace 不会发生 OOM。大量动态生成类或者 ClassLoader 泄漏都可能导致 Metaspace 持续增长。
五十四、面试继续追问:为什么 Docker 2GB,Xmx 1.8GB 仍然可能 OOM?
可以回答:
因为
-Xmx只限制 Heap,并不等于 Java 进程的总内存。Java 进程除了 Heap,还包括 Metaspace、线程栈、Direct Memory、Code Cache 和 JVM Native Memory。如果容器限制 2GB,却把 Heap 最大值直接给到 1.8GB,剩下的空间可能无法容纳这些非 Heap 内存,最终整个进程超过 cgroup 限制,被 Linux 直接杀掉。这种情况下甚至不一定能看到
Java heap space。
这也是线上非常典型的一种问题。
五十五、整篇文章最终应该记住什么?
最重要的不是死记定义。
而是记住下面这棵树:
text
Java Process
│
├── JVM Runtime Data Area
│ │
│ ├── Thread Shared
│ │ ├── Heap
│ │ └── Method Area
│ │
│ └── Thread Private
│ ├── Program Counter
│ ├── Java Stack
│ └── Native Method Stack
│
└── HotSpot / Native Memory
│
├── Metaspace
├── Direct Memory
├── Thread Stack
├── Code Cache
└── Other Native Memory
然后把异常挂上去:
text
Heap
↓
Java heap space
text
Metaspace
↓
Metaspace OOM
text
Java Stack
↓
StackOverflowError
text
Thread / Native
↓
unable to create new native thread
text
Direct Memory
↓
Direct buffer memory
text
Whole Java Process
↓
Container Memory Limit
↓
OOMKilled
只要这两张图真正记住,JVM 内存问题就已经有了整体框架。
五十六、面试常见问题
学完这一篇以后,可以尝试回答:
- JVM 运行时数据区有哪些?
- 哪些内存区域线程共享?
- 哪些区域线程私有?
- Heap 主要存什么?
- Java Stack 主要存什么?
- 什么是栈帧?
- StackOverflow 为什么发生?
-Xss是什么?- 为什么线程太多可能导致 OOM?
unable to create new native thread是什么意思?- 方法区是什么?
- 方法区和永久代是什么关系?
- Java 8 为什么使用 Metaspace?
- Metaspace 在 Heap 里吗?
- Metaspace OOM 常见原因是什么?
- String 常量池在哪里?
- Direct Memory 是什么?
- Netty 为什么会使用 Direct Memory?
-Xmx是否等于 Java 进程最大内存?- 为什么 Docker 2GB,
-Xmx1.8g仍然可能 OOM? - Heap OOM 和 Container OOM 有什么区别?
- Heap OOM 一定是内存泄漏吗?
- Java 对象一定存在 Heap 吗?
- Code Cache 是什么?
- 一个
new User()到底涉及哪些 JVM 内存区域?
五十七、下一篇
现在我们已经回答了:
数据到底放在哪里?
下一步就可以继续回答:
一个 Java 对象到底是怎么创建出来的?
下一篇:
【JVM核心体系03】一个 Java 对象是怎么创建出来的?从 new 指令到对象内存布局
会继续深入:
text
new 指令
↓
类加载检查
↓
内存分配
↓
指针碰撞 / 空闲列表
↓
TLAB
↓
零值初始化
↓
对象头
↓
构造方法
↓
对象可用
并进一步讲:
text
Object Header
Mark Word
Klass Pointer
Instance Data
Padding
把一个:
java
new User();
背后的 JVM 行为真正拆开。
总结
JVM 内存不能只理解成:
text
Heap
+
Stack
真实 Java 进程要复杂得多。
从 JVM 规范角度:
text
线程共享
├── Heap
└── Method Area
线程私有
├── Program Counter
├── Java Stack
└── Native Method Stack
而从 HotSpot 和生产排查角度,还必须关注:
text
Metaspace
Direct Memory
Thread Stack
Code Cache
Native Memory
最终需要建立一个最重要的意识:
Java Heap 只是 Java 进程内存的一部分。
所以:
text
-Xmx
≠
Java Process Maximum Memory
同样:
text
OOM
≠
Heap OOM
生产环境看到内存问题以后,首先应该做的不是:
text
调大 Heap
而是:
先判断到底是哪一块内存出现了问题,再针对性获取证据。
这也是后面学习 Full GC、OOM、Heap Dump 和 JVM 调优的基础。