一文搞懂JVM-内存区域

发布于:2025-09-15 ⋅ 阅读:(15) ⋅ 点赞:(0)


Java内存区域

JVM内存结构分为五大区域:程序计数器、虚拟机栈、本地方法栈、方法区、堆

一、线程私有区域

这些区域是每个线程独有的,线程之间互不影响

1. 程序计数器(PC寄存器)

程序计数器是每个线程私有的,用于记录当前线程正在执行的字节码指令的地址(行号指示器)。

  • 每个线程在执行 Java字节码时,JVM 会为其分配一个 程序计数器

  • 这个计数器的值指向当前线程所执行的字节码指令的地址

  • JVM 是多线程的,采用线程轮流切换执行的方式(线程间上下文切换),而程序计数器就起到“记住上次执行到哪”的作用。

特点:
  • 每个线程一个,线程私有
  • 唯一一个不会发生内存溢出(OutOfMemoryError)的内存区域:程序计数器占用空间小,只记录当前线程执行的字节码指令地址,不参与复杂内存操作,每个线程一个,也不需要频繁分配/释放内存,所以它是 JVM 中唯一一个不会出现 OOM 的内存区域
作用:
  • 控制程序执行流程
  • 支持多线程切换恢复执行位置

2. 虚拟机栈

每个方法执行时会创建一个栈帧压入虚拟机栈,执行结束后弹出,栈帧用于存储:

  • 局部变量表(基本数据类型、引用类型)
  • 操作数栈:用于执行指令时临时存储操作数和计算结果
  • 动态链接:用于将符号引用转为实际调用的方法地址
    • 符号引用:符号引用是 Java 字节码中对类、字段、方法等实体的一种“名称级”描述,在类加载阶段将其解析为直接引用 —— 也就是在内存中的真实地址
    • 直接引用:在程序运行期间JVM 根据符号引用解析出来的具体内存地址 或者对内存中某个对象、类、方法、字段等的直接访问方式
    • 也就是说符号引用是通过类名方法名等间接指向内存地址,直接引用是直接指向内存地址
  • 方法返回信息:用于方法执行完毕后返回调用者位置,恢复现场
作用:
  • 方法调用与执行的内存模型
特点
  • 线程私有
  • 生命周期随线程而定
  • 可能抛出异常
    • StackOverflowError:如果栈的内存大小不允许动态扩展,那么当线程请求栈的深度超过当前 Java 虚拟机栈的最大深度的时候,就抛出 StackOverFlowError 错误。
    • OutOfMemoryError:如果栈的内存大小可以动态扩展, 那么当虚拟机在动态扩展栈时无法申请到足够的内存空间,则抛出OutOfMemoryError异常

3. 本地方法栈

类似于 Java 虚拟机栈,只不过执行的是 Native 方法

可能抛出 OOM 或 StackOverflowError

作用
  • 为 JVM 使用的 Native 本地方法服务(一般由其他语言C、C++编写)
特点
  • 类似于 Java 虚拟机栈,只不过执行的是 Native 方法

  • 可能抛出 OutOfMemoryError 或 StackOverflowError


二、线程共享区域

这些区域是所有线程共享的,在 JVM 启动时创建。

4. 堆

Java 堆是 JVM 中内存最大的一块区域,用于存储所有对象实例和数组,是垃圾回收器(GC)管理的主要区域

结构划分

Java 堆通常被划分为 新生代(Young Generation)老年代(Old Generation),有些实现还有 元空间(Metaspace)

1. 新生代
  • 存放新创建的对象
  • Eden 区:新对象最初分配在这里
  • Survivor 区:在一次新生代垃圾回收后,如果对象还存活,幸存对象会进入Survivor区
2. 老年代
  • 存放从新生代“晋升”来的长生命周期对象
  • GC 不频繁,但回收代价高
作用
  • 存放所有对象实例、数组,几乎所有 new 出来的对象都在这里
特点
  • 所有线程共享
  • 垃圾收集器(GC)主要工作区域
  • 按功能划分:
    • 新生代(Young Generation)
    • 老年代(Old Generation)
    • 元空间(在 JDK8 前叫永久代)
  • 可通过 -Xms -Xmx 调整堆大小

可能抛出: OutOfMemoryError


字符串常量池

字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

JDK1.7 之前,字符串常量池存放在永久代。JDK1.7 字符串常量池和静态变量从永久代移动到了 Java 堆中。

String s1 = "hello";
String s2 = "hello";
System.out.println(s1 == s2); // 输出 true,指向同一个池中的对象

String s3 = new String("hello");
System.out.println(s1 == s3); // 输出 false,new创建了新的对象,引用不同


5. 方法区

方法区是 JVM 内存结构的一部分,用于存储类的元信息,包括类结构、方法、字段、常量、静态变量等

  • 在 JDK 1.7 及之前,它的实现依赖于 永久代(PermGen)
  • 在 JDK 1.8 及之后,方法区实现被替换为 元空间(Metaspace),并从 JVM 内存中移出,改用本地内存分配

运行时常量池

运行时常量池是方法区的一部分,它存储的是编译期生成的各种字面量(如字符串字面量)和符号引用,运行时解析后变成直接引用。

1. 存储编译期生成的常量
  • 包括类、方法、字段的符号引用(如类名、方法名、描述符等)
  • 编译时生成的各种字面量(如字符串字面量、整数字面量等)

这些内容最初在 .class 文件中存在于常量池表,JVM 类加载时会将它们加载进运行时常量池


2. 支持符号引用 → 直接引用的动态解析

运行时常量池负责将符号引用(如“java/lang/String”)解析为直接引用(内存地址)

这正是 动态链接 的过程。

例如:

String s = new String("abc");
  • "abc" 是字面量,存在常量池。
  • String 的类符号引用也来自常量池,运行时解析为 java.lang.String 的真实内存地址。

3. 支持字符串常量池机制
  • 所有字符串字面量都会存储在运行时常量池中,便于复用,节省内存。
  • 通过 intern() 方法也可以手动将字符串加入常量池。

三、本地内存和直接内存

本地内存

指JVM管理之外的系统内存,比如:

  • 操作系统分配的直接内存(Direct Memory)
  • JNI调用中使用的内存
  • JVM自身运行时的代码缓存、线程栈等操作系统内存

这部分内存不受JVM垃圾回收控制,由操作系统管理。


直接内存

  • 直接内存是不属于Java堆(Heap)的内存区域,位于操作系统的本地内存中。
  • 直接内存的优势是避免了Java堆和操作系统内核之间的内存拷贝,提高了I/O操作的效率

在jdk1.8中

Java程序内存 = JVM内存 + 本地内存

本地内存 = 元空间 + 直接内存