第三章:Java 内存模型 (JMM) 与运行时数据区

第三章:Java 内存模型(JMM)与运行时数据区

本章目标:

  • 搞懂 JVM 内存结构
  • 理解堆、栈、方法区到底存什么
  • 理解线程共享与线程私有内存
  • 理解 JMM(Java Memory Model)解决什么问题
  • 为后续 GC、并发编程、JVM调优打下基础

一、为什么必须学习 JVM 内存?

很多 Java 程序员工作几年后都会遇到这些问题:

sql 复制代码
OutOfMemoryError
StackOverflowError
频繁Full GC
内存泄漏
线程安全问题
CPU 100%

这些问题最终都指向:

复制代码
JVM内存

例如:

csharp 复制代码
List<User> list = new ArrayList<>();

while (true) {
    list.add(new User());
}

运行后:

makefile 复制代码
java.lang.OutOfMemoryError: Java heap space

为什么会报错?

因为:

复制代码
堆内存被占满了

所以学习 JVM,首先必须搞懂内存结构。


二、JVM 运行时数据区

当 JVM 启动时,会创建运行时数据区(Runtime Data Area)。

官方结构图:

markdown 复制代码
                 JVM Runtime Data Area

      线程共享
 ┌─────────────────────┐
 │       Heap          │
 ├─────────────────────┤
 │    Method Area      │
 └─────────────────────┘


      线程私有
 ┌─────────────────────┐
 │ Program Counter     │
 ├─────────────────────┤
 │ Java Stack          │
 ├─────────────────────┤
 │ Native Method Stack │
 └─────────────────────┘

分为两大类:

线程共享

所有线程共同拥有:

sql 复制代码
Heap(堆)
Method Area(方法区)

线程私有

每个线程独立拥有:

复制代码
程序计数器
虚拟机栈
本地方法栈

三、程序计数器(Program Counter Register)

这是 JVM 最小的一块内存。

作用:

复制代码
记录当前线程执行到哪条字节码指令

例如:

ini 复制代码
public void test() {
    int a = 1;
    int b = 2;
    int c = a + b;
}

JVM执行过程:

makefile 复制代码
0: iconst_1
1: istore_1

2: iconst_2
3: istore_2

4: iload_1
5: iload_2
6: iadd
7: istore_3

程序计数器记录:

复制代码
当前执行位置

例如:

复制代码
当前执行到第5条指令

为什么每个线程都需要程序计数器?

因为 JVM 支持多线程。

例如:

css 复制代码
线程A
线程B
线程C

CPU不断切换:

css 复制代码
A → B → C → A → B

切换回来时必须知道:

复制代码
上次执行到哪里

因此每个线程都必须拥有独立程序计数器。


四、Java 虚拟机栈(Java Virtual Machine Stack)

这是面试最高频知识点之一。


什么是栈?

每个线程启动时:

复制代码
创建一个虚拟机栈

每调用一个方法:

复制代码
创建一个栈帧(Stack Frame)

例如:

typescript 复制代码
public static void main(String[] args) {
    test();
}

public static void test() {
    int age = 18;
}

执行过程:

scss 复制代码
main()
 │
 ▼
test()

栈结构:

scss 复制代码
┌──────────┐
│ test()   │
├──────────┤
│ main()   │
└──────────┘

五、栈帧结构

每个方法对应一个栈帧。

sql 复制代码
┌────────────────────┐
│ Local Variables    │
├────────────────────┤
│ Operand Stack      │
├────────────────────┤
│ Dynamic Linking    │
├────────────────────┤
│ Return Address     │
└────────────────────┘

局部变量表

例如:

ini 复制代码
public void test() {
    int age = 18;
    String name = "Tom";
}

存放:

复制代码
age
name引用

操作数栈

JVM计算使用。

例如:

ini 复制代码
int c = a + b;

执行过程:

perl 复制代码
push a
push b
add
pop

类似:

diff 复制代码
1
2
+
=
3

六、StackOverflowError 原理

经典面试题。

代码:

csharp 复制代码
public void test() {
    test();
}

执行:

scss 复制代码
test()
 └─ test()
     └─ test()
         └─ test()
             ...

栈不断增长:

bash 复制代码
┌──────┐
│test  │
├──────┤
│test  │
├──────┤
│test  │
├──────┤
│test  │
└──────┘

最终:

复制代码
java.lang.StackOverflowError

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

服务于:

java 复制代码
native

关键字修饰的方法。

例如:

csharp 复制代码
public native void start0();

Thread源码:

scss 复制代码
start()
    ↓
start0()

最终进入:

复制代码
C++
Linux
Windows API

执行。


八、堆(Heap)

JVM中最大的一块内存。

也是 GC 最主要工作区域。


堆存储什么?

所有对象:

scss 复制代码
new User()

new ArrayList()

new HashMap()

都在堆中。

例如:

sql 复制代码
User user = new User();

内存:

sql 复制代码
Stack
 └─ user引用

Heap
 └─ User对象

九、对象创建过程

代码:

sql 复制代码
User user = new User();

发生了什么?


第一步

检查类是否加载:

arduino 复制代码
User.class

未加载:

复制代码
ClassLoader加载

第二步

堆中分配内存:

sql 复制代码
Heap
 ┌─────────┐
 │ User对象 │
 └─────────┘

第三步

初始化对象:

ini 复制代码
name = null;
age = 0;

第四步

执行构造函数:

csharp 复制代码
public User() {
    this.age = 18;
}

第五步

返回引用:

sql 复制代码
user

保存到栈。


十、方法区(Method Area)

JDK8以前:

scss 复制代码
Permanent Generation(PermGen)

JDK8以后:

复制代码
Metaspace(元空间)

存储内容

类元数据:

kotlin 复制代码
public class User {
}

存放:

复制代码
类名
字段信息
方法信息
字节码
运行时常量池

示例

typescript 复制代码
public class User {

    private String name;

    public void hello() {

    }
}

方法区保存:

sql 复制代码
User类结构
hello方法信息
name字段信息

十一、运行时常量池

属于方法区的一部分。

例如:

ini 复制代码
String s = "hello";

字符串常量:

arduino 复制代码
"hello"

会进入:

arduino 复制代码
String Constant Pool

例如:

ini 复制代码
String a = "abc";
String b = "abc";

实际上:

css 复制代码
a
 \
  ---> "abc"
 /
b

共享同一个对象。


十二、JMM(Java Memory Model)是什么?

很多人把 JVM 内存结构和 JMM 混淆。

实际上:


JVM Runtime Data Area

解决:

复制代码
内存如何划分

问题。


JMM

解决:

复制代码
线程之间如何访问内存

问题。


十三、为什么需要 JMM?

假设:

ini 复制代码
private boolean flag = false;

线程A:

ini 复制代码
flag = true;

线程B:

arduino 复制代码
while (!flag) {

}

理论上:

css 复制代码
线程B应该结束循环

但实际上:

复制代码
可能永远循环

为什么?

因为 CPU 缓存。


十四、JMM 核心结构

css 复制代码
           Main Memory
                 │
      ┌──────────┴──────────┐
      │                     │
 Thread A             Thread B
 Working Memory    Working Memory

每个线程:

复制代码
有自己的工作内存

不能直接访问其他线程内存。


执行流程:

复制代码
主内存
 ↓
工作内存
 ↓
修改
 ↓
刷新主内存

十五、volatile 如何解决可见性?

代码:

arduino 复制代码
private volatile boolean flag = false;

线程A:

ini 复制代码
flag = true;

JMM保证:

复制代码
立即刷新主内存

线程B:

复制代码
读取主内存最新值

因此:

arduino 复制代码
while循环结束

十六、JMM 三大特性


1 原子性(Atomicity)

例如:

ini 复制代码
count++;

实际上:

diff 复制代码
读取
+
1
写回

不是原子操作。


2 可见性(Visibility)

线程A修改变量:

ini 复制代码
flag = true;

线程B能够立即看到。


3 有序性(Ordering)

JVM和CPU会优化:

复制代码
指令重排

JMM保证:

复制代码
在规则范围内有序

十七、面试高频问题

JVM内存结构有哪些?

复制代码
程序计数器
虚拟机栈
本地方法栈
堆
方法区

堆和栈有什么区别?

栈:

复制代码
线程私有
方法调用
局部变量

堆:

复制代码
线程共享
对象实例
GC管理

为什么会 StackOverflowError?

复制代码
递归过深
栈帧过多

导致栈空间耗尽。


为什么会 OOM?

复制代码
堆内存不足
元空间不足
直接内存不足

JMM 和 JVM 内存结构有什么区别?

JVM内存结构:

复制代码
关注内存区域划分

JMM:

复制代码
关注线程间共享变量访问规则

本章总结

牢记下面这张图:

markdown 复制代码
                JVM Runtime Data Area

         线程共享
    ┌─────────────────┐
    │      Heap       │
    ├─────────────────┤
    │   Method Area   │
    └─────────────────┘

         线程私有
    ┌─────────────────┐
    │ Program Counter │
    ├─────────────────┤
    │ VM Stack        │
    ├─────────────────┤
    │ Native Stack    │
    └─────────────────┘


                JMM

          Main Memory
                │
      ┌─────────┴─────────┐
      │                   │
 Working Memory      Working Memory
  Thread A            Thread B

理解了这两张图,你就掌握了 JVM 最核心的内存基础。

下一章《垃圾收集(GC)原理》,我们将深入讲解:

  • 对象什么时候变成垃圾?
  • GC Root 是什么?
  • 为什么 Java 不使用引用计数?
  • Minor GC、Major GC、Full GC 的区别
  • CMS、G1、ZGC 到底怎么选?

这一章是 JVM 面试和线上问题排查的核心内容。

相关推荐
程序员晓琪17 小时前
约定大于配置:基于 Java 包名自动生成 API 版本路由的最佳实践
java·spring boot·后端
Flittly17 小时前
【AgentScope Java新手村系列】(11)中断与恢复
java·spring boot·spring
众少成多积小致巨18 小时前
JNI (Java Native Interface) 技术手册中文参考指南
android·java·c++
东坡白菜18 小时前
破局全栈:前端开发的Java入门实战记录—JPA(2)
java·后端
SimonKing1 天前
艹,维护AI写的代码,我心态崩了......
java·后端·程序员
用户298698530141 天前
Java Word 文档样式进阶:段落与文本背景色设置完全指南
java·后端
小bo波2 天前
从"任意文件复制"深挖Java I/O:字符流与字节流的本质抉择
java·nio·io流·后端开发·文件复制
nanxun8863 天前
记一次诡异的 Docker 容器"串包"故障排查
java
用户1563068103513 天前
Day01 | Java 基础(Java SE)
java
行者全栈架构师3 天前
Maven dependency:tree 的 8 个高级用法
java·后端