第三章: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 面试和线上问题排查的核心内容。

相关推荐
m0_744724931 小时前
Tomcat相关
java·tomcat
AugustRed1 小时前
A2UI 完整学习指南(含 Java 后端 + 前端实战示例)
java·开发语言·前端
程序猿乐锅2 小时前
【MySQL | 第五篇】 MySQL 性能分析:如何查询慢 SQL
java·sql·mysql
lee_curry2 小时前
tomcat+springmvc+spring源码流通过程
java·spring·tomcat·springmvc
w1wi2 小时前
【兼职】边学边练的AI网站
java·人工智能·ai·ai编程·ai写作
basketball6162 小时前
C++进阶:1. 引用折叠规则
java·开发语言·c++
404号扳手2 小时前
Java 进阶知识(七)
java·后端
小马爱打代码2 小时前
Spring框架:介绍和快速入门
java·后端·spring
糖果店的幽灵2 小时前
LangChain 1.3 完全教程:从入门到精通-Part 7: Documents(文档处理)
java·python·langchain