【JVM】JVM内存结构解析

你这张图和总结整体是对的,它讲的是 Java 程序从源码到运行时,JVM 内部各个区域怎么配合工作。我帮你按"流程 + 内存区域 + 易混点"解释一下。


1. JVM 运行流程先看这条线

text 复制代码
Java 源代码 .java
        ↓ javac 编译
Java 字节码 .class
        ↓ 类加载器加载
运行时数据区
        ↓ 执行引擎执行
机器指令 / 操作系统指令

也就是说:

你写的是:

java 复制代码
public class User {
    public static void main(String[] args) {
        User u = new User();
    }
}

先被编译成 .class 字节码文件。

然后 JVM 启动后,类加载器 会把 .class 文件加载进内存,也就是加载到 JVM 的 运行时数据区

接着 执行引擎负责执行字节码。执行方式主要有两种:

text 复制代码
解释器:一行一行解释字节码,马上执行,启动快
JIT 编译器:把热点代码编译成本地机器码,后面执行更快

2. 运行时数据区是什么?

你说得对:

text 复制代码
JVM 内存结构 ≈ 运行时数据区

运行时数据区主要分为 5 块:

text 复制代码
线程共享:
1. 堆
2. 方法区

线程私有:
3. 虚拟机栈
4. 本地方法栈
5. 程序计数器

可以这样记:

text 复制代码
对象放堆里
类信息放方法区
方法调用靠虚拟机栈
native 方法靠本地方法栈
当前执行到哪靠程序计数器

3. 程序计数器 PC Register

程序计数器的作用是:

text 复制代码
记录当前线程下一条要执行的字节码指令地址

比如一个线程正在执行:

java 复制代码
int a = 1;
int b = 2;
int c = a + b;

这些 Java 代码会被编译成一条条字节码指令。

程序计数器就像一个"小书签",记录当前线程执行到哪一条指令了。

为什么它是线程私有的?

因为 CPU 会在线程之间切换。

比如:

text 复制代码
线程 A 执行到第 10 条指令,被切走
线程 B 开始执行
过一会儿线程 A 又回来继续执行

线程 A 必须知道自己之前执行到哪里了,所以每个线程都要有自己的程序计数器。

这里注意一个小点:

你写的:

物理上程序计数器是使用"寄存器"完成的。

这个说法可以作为理解,但严格来说,JVM 里的 PC Register 是一个 逻辑概念,不一定真的对应 CPU 的某一个物理寄存器。


4. 虚拟机栈 JVM Stack

虚拟机栈也叫线程栈。

每个线程创建时,都会有自己的虚拟机栈。

它的核心作用是:

text 复制代码
管理 Java 方法的调用过程

比如代码:

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

public static void a() {
    b();
}

public static void b() {
    System.out.println("hello");
}

执行过程大概是:

text 复制代码
main() 入栈
a() 入栈
b() 入栈
b() 执行完,出栈
a() 执行完,出栈
main() 执行完,出栈

栈里面的基本单位叫 栈帧

一个方法调用,对应一个栈帧。

栈帧里面主要存:

text 复制代码
局部变量表:方法参数、局部变量
操作数栈:字节码执行过程中的临时计算空间
动态链接:指向方法区中方法信息的引用
方法返回地址:方法执行完后回到哪里

你总结里写:

栈帧存储方法参数、方法内局部变量、方法返回地址

这是对的,但面试里可以再补充一句:

text 复制代码
栈帧还包括操作数栈和动态链接。

虚拟机栈为什么会 StackOverflowError?

比如递归没有出口:

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

每调用一次 test(),都会创建一个新的栈帧。

栈帧越来越多:

text 复制代码
test()
test()
test()
test()
...

虚拟机栈空间被撑爆,就会报:

text 复制代码
java.lang.StackOverflowError

所以你写的:

方法递归过多会导致 StackOverflowError

是正确的。


5. 本地方法栈 Native Method Stack

本地方法栈服务的是 native 方法

比如:

java 复制代码
public native int hashCode();

或者 Thread 类底层的一些方法:

java 复制代码
private native void start0();

native 方法不是用 Java 实现的,而是用 C/C++ 等语言实现的。

为什么需要 native?

因为 Java 有些操作没法直接做,比如:

text 复制代码
操作系统线程创建
底层文件操作
网络 I/O
内存管理
硬件相关操作

所以 Java 通过 JNI,也就是 Java Native Interface,本地方法接口,去调用 C/C++ 写好的本地库。

关系可以理解成:

text 复制代码
Java 代码
   ↓
native 方法声明
   ↓
JNI 本地方法接口
   ↓
C/C++ 本地库
   ↓
操作系统底层 API

本地方法栈就是给这些 native 方法运行时使用的栈空间。


6. 堆 Heap

堆是 JVM 中最大的一块内存区域,主要用来存放:

text 复制代码
对象实例
数组

比如:

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

这里要区分:

text 复制代码
user 这个引用变量:在栈中
new User() 这个对象:在堆中

可以理解成:

text 复制代码
栈中 user 变量保存的是地址
堆中保存的是真正的 User 对象

例如:

java 复制代码
public void test() {
    User user = new User();
}

大概是:

text 复制代码
虚拟机栈:
user = 0x001

堆:
0x001 -> User 对象

堆为什么需要 GC?

因为对象都在堆里,很多对象用完之后就没用了。

比如:

java 复制代码
public void test() {
    User user = new User();
}

方法执行完后,user 这个局部变量出栈了。

如果这个 User 对象没有其他引用指向它,那么它就变成垃圾对象,后续会被 GC 回收。

所以:

text 复制代码
GC 主要回收堆中的对象

年轻代和老年代

堆从逻辑上可以分为:

text 复制代码
年轻代:新创建的对象大多在这里
老年代:长期存活的对象会晋升到这里

年轻代又可以分为:

text 复制代码
Eden 区
Survivor From 区
Survivor To 区

大致过程:

text 复制代码
新对象先进入 Eden
Minor GC 后还活着,进入 Survivor
多次 GC 后还活着,进入老年代

这就是你写的:

年轻代存生命周期短的对象,老年代存生命周期长的对象

这个理解是对的。


7. 方法区 Method Area

方法区是线程共享的,主要存放 类相关信息

比如一个类:

java 复制代码
public class User {
    private String name;
    private static int count;

    public void sayHello() {
        System.out.println("hello");
    }
}

类被加载后,方法区里会保存:

text 复制代码
类名
父类信息
接口信息
字段信息
方法信息
运行时常量池
JIT 编译后的代码

你写的:

方法区存储类信息、静态变量、常量、编译后的代码、运行时常量池

大方向是对的。

但这里有一个容易被问的细节:

JDK 7 和 JDK 8 方法区变化

方法区是 JVM 规范里的概念。

它的具体实现,在不同 JDK 版本中不一样。

text 复制代码
JDK 7 及以前:永久代 PermGen
JDK 8 以后:元空间 Metaspace

区别是:

text 复制代码
永久代:主要使用 JVM 堆内存
元空间:使用本地内存,也就是操作系统内存

所以 JDK 8 后,如果类加载太多,可能报:

text 复制代码
java.lang.OutOfMemoryError: Metaspace

方法区、永久代、元空间的关系

这个地方最容易混:

text 复制代码
方法区:规范,是 JVM 规定应该有这么一块区域
永久代:JDK 7 及以前 HotSpot 对方法区的实现
元空间:JDK 8 以后 HotSpot 对方法区的实现

就像:

text 复制代码
接口:方法区
实现类1:永久代
实现类2:元空间

所以你可以这样记:

text 复制代码
方法区是抽象概念
永久代和元空间是具体实现

8. 直接内存 Direct Memory

直接内存不是 JVM 运行时数据区的一部分。

它属于:

text 复制代码
操作系统内存 / 本地内存

常见于 NIO。

比如:

java 复制代码
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);

它分配的就是直接内存。

直接内存的好处是:

text 复制代码
减少 Java 堆和操作系统内核之间的数据拷贝
I/O 性能更高

普通 I/O 大概是:

text 复制代码
磁盘/网络
  ↓
操作系统内核缓冲区
  ↓ 拷贝
Java 堆内存

直接内存大概是:

text 复制代码
磁盘/网络
  ↓
直接内存

Java 和本地代码都可以访问这块区域,所以读写性能更高。

但缺点是:

text 复制代码
分配和释放成本更高
不受 JVM 堆大小直接限制
使用不当也可能 OOM

可能报:

text 复制代码
java.lang.OutOfMemoryError: Direct buffer memory

9. 线程私有和线程共享怎么理解?

线程私有

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

为什么私有?

因为每个线程执行的方法、执行到的位置都不一样。

比如:

text 复制代码
线程 A 正在执行 login()
线程 B 正在执行 pay()

它们的方法调用链不同,局部变量不同,执行位置也不同,所以这些东西不能共享。


线程共享

text 复制代码
堆
方法区

为什么共享?

因为所有线程都可能访问同一个对象、同一个类信息。

比如:

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

线程 A 可以访问这个对象,线程 B 也可以访问这个对象。

所以堆是共享的。

又比如:

java 复制代码
User.class

类信息只需要加载一份,所有线程共用即可。

所以方法区也是共享的。


10. 一个完整例子串起来

看这段代码:

java 复制代码
public class Demo {
    private static int count = 0;

    public static void main(String[] args) {
        User user = new User("Tom");
        user.sayHello();
    }
}

执行时大概是:

第一步:类加载

Demo.classUser.class 被类加载器加载。

类信息进入方法区:

text 复制代码
方法区:
Demo 类信息
User 类信息
main 方法信息
sayHello 方法信息
static count
运行时常量池

第二步:main 线程启动

JVM 创建 main 线程。

main 线程拥有自己的:

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

第三步:调用 main 方法

main() 方法入栈。

text 复制代码
虚拟机栈:
main 栈帧

main 栈帧里有局部变量:

text 复制代码
args
user

第四步:创建对象

执行:

java 复制代码
User user = new User("Tom");

堆中创建对象:

text 复制代码
堆:
User 对象 { name = "Tom" }

栈中的 user 保存对象地址:

text 复制代码
虚拟机栈:
user -> 堆中的 User 对象

第五步:调用 sayHello

执行:

java 复制代码
user.sayHello();

sayHello() 方法入栈:

text 复制代码
虚拟机栈:
sayHello 栈帧
main 栈帧

执行完后,sayHello 出栈:

text 复制代码
虚拟机栈:
main 栈帧

main 执行完后,main 栈帧也出栈。


11. 你这份笔记里需要稍微修正的地方

主要有几个小点:

1. "运行时数据区把字节码加载到内存"

更准确说法是:

text 复制代码
类加载系统把 class 字节码加载到运行时数据区。

运行时数据区本身不是主动加载者,它是 JVM 的内存区域。


2. "程序计数器物理上使用寄存器完成"

可以改成:

text 复制代码
程序计数器是 JVM 中的逻辑概念,用来记录当前线程正在执行的字节码指令地址。

不要强调一定是物理寄存器。


3. "JDK1.7 中堆中存在方法区的实现:永久代"

这个说法容易引起误解。

建议改成:

text 复制代码
JDK 7 及以前,HotSpot 虚拟机使用永久代实现方法区,永久代属于 JVM 管理的内存区域。
JDK 8 以后,使用元空间实现方法区,元空间使用本地内存。

4. "本地方法栈是本地方法接口运行时所需要的内存空间"

可以改成:

text 复制代码
本地方法栈是 native 方法执行时所使用的栈空间。
JNI 是 Java 调用 native 方法的接口机制。

JNI 是接口,本地方法栈是运行时内存区域,两个不要混成一个东西。


12. 面试版总结

你可以这样背:

text 复制代码
JVM 运行时数据区分为五部分:堆、方法区、虚拟机栈、本地方法栈、程序计数器。

堆和方法区是线程共享的。堆主要存放对象实例,是 GC 管理的主要区域;方法区主要存放类信息、常量、静态变量、运行时常量池和 JIT 编译后的代码。

虚拟机栈、本地方法栈和程序计数器是线程私有的。虚拟机栈用于 Java 方法调用,每次方法调用都会创建一个栈帧,栈帧中保存局部变量表、操作数栈、动态链接和返回地址。本地方法栈服务于 native 方法。程序计数器记录当前线程正在执行的字节码指令地址,用于线程切换后恢复执行位置。

JDK 7 及以前方法区的实现是永久代,JDK 8 以后改为元空间,元空间使用本地内存。

一句话记忆:

text 复制代码
堆放对象,方法区放类,栈跑方法,PC 记位置,本地栈跑 native。
相关推荐
梁辰兴1 小时前
计算机网络基础:电子邮件的信息格式
网络·计算机网络·电子邮件·计算机网络基础·梁辰兴·信息格式
酉鬼女又兒2 小时前
零基础入门计算机网络:点对点协议PPP、媒体接入控制基本概念、静态划分信道技术、CSMA/CD与CSMA/CA协议全面详解
服务器·网络·网络协议·计算机网络·职场和发展·求职招聘·媒体
酉鬼女又兒2 小时前
零基础快速入门IP编址计算练习题详解:从基础到实战
网络·网络协议·tcp/ip·计算机网络·考研·职场和发展·分类
liulilittle11 小时前
论 Linux 内核态全局稳态带宽的卡尔曼估计与工程实现
linux·服务器·网络·c++·计算机网络·tcp·通信
酉鬼女又兒21 小时前
零基础入门计算机网络可靠传输:从基本概念到三大实现机制(停止 - 等待 / 回退 N 帧 / 选择重传)全解析
网络·网络协议·计算机网络·考研·职场和发展·计算机外设·求职招聘
internet Boy1 天前
(第一阶段)计算机 & 网络基础知识
计算机网络
酉鬼女又兒1 天前
零基础入门计算机网络数据链路层:从基本概念、封装成帧到差错检测核心原理全解析
服务器·网络·网络协议·tcp/ip·计算机网络·考研·职场和发展
梦奇不是胖猫1 天前
[ 计算机网络 | 第四章 ] 网络层 04 IP的局限与扩展
网络·网络协议·tcp/ip·计算机网络
艾莉丝努力练剑1 天前
【Qt】事件
服务器·开发语言·网络·数据库·qt·tcp/ip·计算机网络