Skip to content

JVM

🧠 一、JVM 内存模型概览

JVM 的内存区域可以分为以下几个主要部分:

区域名称是否线程私有用途说明
程序计数器✅ 是记录当前线程执行的字节码行号
虚拟机栈✅ 是存放方法调用时的局部变量、操作数栈等
本地方法栈✅ 是为 Native 方法服务(如 C/C++ 调用)
堆(Heap)❌ 否所有线程共享,存放对象实例
方法区(MetaSpace)❌ 否存储类信息、常量池、静态变量、编译器编译后的代码等
直接内存(Direct Memory)❌ 否NIO 使用,不属于 JVM 堆内

🔥 二、重点详解:堆、栈、方法区

1. 堆(Heap)

✅ 特点:

  • 所有线程共享的一块内存区域。
  • 用于存储所有的 对象实例(Object)数组
  • 是垃圾回收器(GC)管理的主要区域。
  • 可以动态扩展大小(通过 JVM 参数设置)。

示例:

java
Person p = new Person(); // new 出来的对象就存在堆中

GC 分代模型(现代 JVM 的堆结构):

  • 新生代(Young Generation)
    • Eden 区
    • Survivor 0、Survivor 1
  • 老年代(Old Generation)
  • 元空间(Metaspace):替代了永久代(PermGen)

💡 注意:从 JDK 8 开始,永久代(PermGen)被元空间(Metaspace)取代,元空间使用的是本地内存(Native Memory),不在 JVM 堆中。


2. 虚拟机栈(Java Stack)

✅ 特点:

  • 每个线程都有一个自己的虚拟机栈。
  • 栈中保存的是一个个的 栈帧(Stack Frame),每个栈帧对应一个方法的调用。
  • 每次方法调用都会在栈中压入一个新的栈帧,方法执行完毕弹出。

栈帧中包含:

  • 局部变量表(Local Variables)
  • 操作数栈(Operand Stack)
  • 动态链接(Dynamic Linking)
  • 方法返回地址(Return Address)
  • 其他附加信息

示例:

java
public void sayHello() {
    int a = 10;
    String name = "Tom";
}
  • sayHello() 方法被调用时,会创建一个栈帧。
  • aname 是局部变量,会被放入栈帧的局部变量表中。
  • 实际的字符串 "Tom" 是对象,放在堆中。

常见异常:

  • StackOverflowError:递归太深或方法调用层次太多导致栈溢出。
  • OutOfMemoryError:如果线程请求的栈深度超过最大允许值。

3. 本地方法栈(Native Method Stack)

  • 类似于虚拟机栈,但它是为 JVM 使用的 Native 方法 提供服务的。
  • 例如:Java 中调用 Thread.start() 底层可能调用了 C/C++ 的函数,这些函数就在本地方法栈中运行。

4. 方法区(Method Area / Metaspace)

✅ 特点:

  • 所有线程共享。
  • 存储已加载的类信息(Class)、常量池、静态变量、JIT 编译后的代码等。

与堆的区别:

  • 堆:存放对象实例。
  • 方法区:存放类的元数据。

JDK 8 之前 vs JDK 8 之后:

项目JDK 8 之前JDK 8 及以后
方法区实现永久代(PermGen)元空间(Metaspace)
内存来源JVM 堆内存本地内存(Native)
默认大小限制小,默认几十 MB无上限(受限于物理内存)

5. 程序计数器(Program Counter Register)

  • 每个线程都有一个程序计数器。
  • 它是唯一一个在 JVM 规范中没有规定 OutOfMemoryError 的区域。
  • 作用:记录当前线程执行的字节码指令地址(如果是 Native 方法,则为空)。

6. 直接内存(Direct Memory)

  • 不属于 JVM 堆内存的一部分。
  • 使用 ByteBuffer.allocateDirect() 创建。
  • 由操作系统直接管理,速度更快,适合 IO 操作(如 NIO)。
  • 但不受 JVM 垃圾回收机制管理,需要手动释放。

🧪 三、举个例子理解整个流程

java
public class Demo {
    public static void main(String[] args) {
        int age = 20;                // 局部变量,存在栈中
        String name = "John";        // 引用变量在栈中,实际对象在堆中
        Person p = new Person();     // 对象在堆中,p 是引用变量,在栈中
        p.setName("Alice");          // 调用方法,方法调用信息也在栈中
    }
}

内存分布如下:

内容存储位置
age
name, p
"John" 字符串对象
new Person()
setName() 方法信息方法区
当前执行位置程序计数器

🛠 四、常见问题 & 面试高频点

Q1: Java 中“栈”和“堆”的区别?

项目栈(Stack)堆(Heap)
存储内容局部变量、基本类型、对象引用对象实例
线程可见性线程私有所有线程共享
生命周期方法调用开始 → 方法结束对象创建 → GC 回收
内存管理自动分配、自动释放手动分配(new),GC 自动回收
速度相对慢

Q2: String 存在哪?"abc" 和 new String("abc") 有什么不同?

  • "abc":字符串常量池(在堆中,JDK7+ 之后统一在堆中)
  • new String("abc"):会在堆中新建一个字符串对象,同时常量池中也可能存在 "abc"

Q3: 为什么会有元空间(Metaspace)代替永久代(PermGen)?

  • PermGen 容易出现 OutOfMemoryError: PermGen space
  • 元空间使用本地内存,避免内存不足的问题
  • 更容易进行垃圾回收和性能优化

📚 五、总结图解(文字版)

┌───────────────────────────────┐
│         方法区 (Metaspace)   │ ← 类信息、静态变量、常量池
├───────────────────────────────┤
│             堆               │ ← 所有对象实例
├───────────────────────────────┤
│           线程 1             │
│   ┌───────────────────────┐  │
│   │  程序计数器(PC Reg) │  │
│   ├───────────────────────┤  │
│   │    虚拟机栈(Stack)  │  │ ← 局部变量、方法调用
│   └───────────────────────┘  │
├───────────────────────────────┤
│           线程 2             │
│   ┌───────────────────────┐  │
│   │  程序计数器(PC Reg) │  │
│   ├───────────────────────┤  │
│   │    虚拟机栈(Stack)  │  │
│   └───────────────────────┘  │
└───────────────────────────────┘

如果你还想了解:

  • 如何查看 JVM 内存使用情况?
  • 如何设置 JVM 内存参数(如 -Xms, -Xmx, -XX:MaxMetaspaceSize)?
  • JVM 内存调优技巧?
  • G1、CMS、ZGC 等垃圾回收器的区别?

欢迎继续提问,我可以一步步带你深入 JVM 世界 😎