JVM运行时数据区详解:变量、对象和类信息到底存在哪里

JVM运行时数据区详解:变量、对象和类信息到底存在哪里

写 Java 代码时,我们通常只关注变量的值和对象的行为,很少去思考它们在 JVM 中到底如何存放。

java 复制代码
int a = 10;

程序运行之后,a 并不是一直存在于 .class 文件中,而是会随着方法执行进入 JVM 的运行时数据区。

再比如:

java 复制代码
Object obj = new Object();

这里既有变量引用 obj,也有通过 new 创建的对象实例,它们的生命周期和存储位置并不相同。

理解 JVM 运行时数据区,可以帮助我们搞清楚几件事:局部变量为什么方法结束后自动消失,对象为什么需要 GC 回收,静态变量、类信息、对象实例分别存放在哪里。这也是理解 Java 内存模型和垃圾回收机制的基础。

目录

全景图:五个区域各司其职

JVM 规范把运行时内存划分为五个区域。从线程的角度看,可以分成两类:

线程私有(每个线程独占一份,线程创建时分配,线程销毁时回收):

  • 程序计数器
  • 虚拟机栈
  • 本地方法栈

线程共享(所有线程共用,JVM 启动时创建):

  • 方法区

注意:静态变量(类字段)在 HotSpot JDK 8+ 中随 Class 对象存放在堆中,而非方法区。

程序计数器

这是五个区域里最小的一块,也是唯一不会抛 OOM 的区域。

程序计数器保存当前线程下一条需要执行的字节码地址。JVM 的执行引擎执行完一条字节码后,会从程序计数器取出下一条的行号,继续执行。

每个线程都有自己的程序计数器,互不干扰。这也是"线程私有"的含义------线程 A 执行到第 10 行,线程 B 可能执行到第 50 行,各自记录,互不影响。

可以把它理解成书签。你在一本书里读到第 30 页,夹了个书签;别人读到第 80 页,也夹了个书签。书签各自独立,标记的是各自读到哪了。

唯一需要注意的:如果当前执行的是 native 方法,程序计数器的值为空(Undefined)。因为 native 方法的执行交给操作系统了,JVM 管不着。

虚拟机栈

虚拟机栈是我们最常接触的内存区域,也是理解 Java 方法调用的关键。

每当一个方法被调用,JVM 会为这个方法创建一个栈帧(Stack Frame),压入虚拟机栈。方法执行完毕,栈帧弹出。一个线程在同一时刻只有栈顶的栈帧是活跃的------正在执行的那个方法。

栈帧里装什么

每个栈帧包含四个部分:

组成 说明
局部变量表 存方法的局部变量和方法参数。基本类型直接存值,引用类型存引用(指针)
操作数栈 方法执行过程中的临时计算空间,比如做加法时先 push 两个数,再执行 iadd
动态链接 指向运行时常量池中该方法的引用,支持多态
方法返回地址 方法执行完后回到调用者的哪一行继续执行

其中最重要的是局部变量表 。你写的每一个局部变量,不管是 int a = 10 还是 String s = "hello",都存在这里。基本类型存的是值本身,引用类型存的是指向堆中对象的引用。

方法调用时创建栈帧,方法结束时栈帧销毁。局部变量的生命周期和方法调用一样长------方法返回,栈帧弹出,局部变量自然就没了。

两种异常

虚拟机栈有两种出错场景:

StackOverflowError: 方法调用太深,栈帧不断压入,栈空间撑爆了。最常见的原因是递归没有终止条件。

java 复制代码
public class StackOverflowDemo {
    public static void main(String[] args) {
        recurse(0);
    }

    static void recurse(int depth) {
        // 每调一次,压入一个栈帧,直到栈空间不够
        recurse(depth + 1);
    }
}
// 运行结果:java.lang.StackOverflowError

OutOfMemoryError: 如果虚拟机栈支持动态扩展,扩展时内存不够了就会抛这个错。不过现在的 JVM 实现通常在创建线程时就固定了栈大小,所以更多见的是 StackOverflowError。

本地方法栈

本地方法栈和虚拟机栈几乎一样,区别在于它服务的是 native 方法------用 C/C++ 写的方法,通过 JNI 调用。

比如 Thread.currentThread()Object.hashCode() 这些方法底层都是 native 实现。调用这些方法时,压入的是本地方法栈,不是虚拟机栈。

在 HotSpot JVM 的实现里,本地方法栈和虚拟机栈合二为一了,不单独区分。所以面试的时候提一句"本地方法栈为 native 方法服务"就够了。

堆是 JVM 中用于存放对象实例和数组的主要区域,也是 GC 主要工作的地方。

所有对象实例和数组都在堆上分配。 你每次写 new Object()new int[100],对象的内存都来自堆。前面说的虚拟机栈里存的是引用,真正的对象本体在堆里。

来看一个简单的例子:

java 复制代码
public void create() {
    Object obj = new Object();
}

调用 create() 时,发生了两件事:

  1. 在虚拟机栈的栈帧中,局部变量 obj 存了一个引用(指针)
  2. 在堆中,分配了一块内存,存放 Object 实例

obj 是引用,存在栈里;对象本体,存在堆里。方法结束后栈帧销毁,obj 这个引用就没了。但对象还在堆里,直到 GC 发现它没有引用指向了,才会回收。

为什么需要 GC

C++ 里,你 new 了一个对象,用完之后必须自己 delete,不然内存就泄漏了。Java 把这个责任交给了 GC(垃圾回收):你只管 new,GC 帮你判断哪些对象没人用了,然后回收它们的内存。

GC 判断对象"没人用"的依据是可达性分析。从一组叫"GC Roots"的根节点出发(比如虚拟机栈里的局部变量、类的静态字段),沿着引用链往下找,能到达的对象就是"活的",到达不了的就是"死的",可以回收。

复制代码
GC Roots
    │
    ├── 虚拟机栈中的局部变量
    │       └── obj ──→ Object实例(可达,不回收)
    │
    └── 类的静态字段
            └── list ──→ ArrayList实例(可达,不回收)
                         └── 其中引用的对象(可达,不回收)

    堆中没有任何引用指向的对象 → 不可达,GC 回收

这里先关注对象为什么需要 GC,具体的垃圾回收算法和分代设计会在后面的 GC 章节展开。

方法区

方法区存储的是类的元信息:类名、字段信息、方法信息、方法的字节码、运行时常量池。

当你写了一个 class User,JVM 第一次用到这个类时,会通过类加载器把 .class 文件读进来,在方法区中创建这个类的元数据。之后你每次 new User(),堆里分配的是对象实例,但"User 这个类长什么样"的信息始终在方法区里。

类加载完成后,JVM 会保存对应的类元信息。之后无论创建多少个对象实例,这些实例共享同一份类定义。

永久代和元空间

方法区是一个逻辑概念,JVM 规范定义了它,但不同版本的 HotSpot JVM 实现方式不同:

  • JDK 7 及之前:用永久代(PermGen)实现,放在 JVM 堆内存里,大小固定,容易 OOM
  • JDK 8 起:改为元空间(Metaspace),使用本地内存(操作系统内存),默认不限大小

永久代的问题在于它属于 JVM 堆内存中的一块固定区域,类信息增长后容易出现空间不足。JDK 8 引入元空间后,将类元数据放到了本地内存中,减少了固定大小带来的限制。

字符串常量池也从永久代移到了堆里,这是 JDK 7 就完成的变更。

实战:变量去哪了

来看一段代码,分析每个变量在运行时存在哪个区域:

java 复制代码
public class MemoryDemo {
    // 静态字段属于类变量
    // HotSpot JDK8+ 中存放在堆中的 Class 对象相关区域
    private static int count = 0;

    public static void main(String[] args) {
        // args 引用 → 虚拟机栈(main 方法栈帧)
        // args 数组对象 → 堆
        count++;
        createObject();
    }

    public static Object createObject() {
        // 局部变量 a → 虚拟机栈(createObject 方法栈帧)
        int a = 10;

        // obj 引用 → 虚拟机栈
        // Object 实例 → 堆
        Object obj = new Object();

        return obj;
    }
}

逐个拆解:

变量 存储位置 说明
count 堆(Class 对象相关区域) 静态字段属于类变量,HotSpot JDK8+ 中随 Class 对象存放在堆中
args 引用 虚拟机栈 main 方法的局部变量
args 数组对象 通过 new String[] 创建
a 虚拟机栈 createObject 方法的局部变量,基本类型直接存值
obj 引用 虚拟机栈 createObject 方法的局部变量,存的是指针
Object 实例 通过 new 创建的对象

一个规律:基本类型的局部变量直接存在栈帧的局部变量表里,方法结束就没了。对象的引用存在栈帧里,但对象本体在堆里,方法结束后引用没了,对象等着 GC 来回收。

五个区域的差异

区域 线程归属 存储内容 是否 GC 可能的异常
程序计数器 私有 字节码行号
虚拟机栈 私有 栈帧(局部变量、操作数栈) StackOverflowError / OOM
本地方法栈 私有 native 方法栈帧 StackOverflowError / OOM
共享 对象实例、数组 OOM: Java heap space
方法区 共享 类信息、常量池 是(类卸载) OOM: Metaspace

记住这张表,面试时问"堆和栈的区别"、"哪些区域会 GC"、"哪些会抛 OOM",都能从这里找到答案。

小结

JVM 将不同生命周期的数据放入不同区域:方法调用产生的数据随着栈帧销毁,对象实例交给 GC 管理,类元信息则伴随类加载存在。

但分区只是静态的内存模型。堆里的对象创建了,什么时候算"没人用了",GC 用什么算法判断可达性,分代模型是怎么回事------这些问题,是下一篇的事了。

相关推荐
孫治AllenSun2 天前
【DataX】生产环境搭建DataX集群案例
java·开发语言·jvm
丈剑走天涯2 天前
JVM(十) JDK8 CMS (Concurrent Mark Sweep)
jvm
丈剑走天涯2 天前
JVM(九) JDK8 G1 (Garbage-First)
jvm
笨蛋不要掉眼泪2 天前
Java虚拟机:堆的参数配置
java·开发语言·jvm
zy happy3 天前
VMware虚拟机添加新的硬盘
java·linux·jvm·docker
丈剑走天涯3 天前
JVM(八) JDK8 Parallel GC
jvm
LING-Z4 天前
JVM GC 机制详解:算法、分代、回收器、GC 类型一篇讲透
java·jvm·垃圾回收
大不点wow4 天前
Java序列化与反序列化:让对象走出JVM
java·开发语言·jvm
程序员天天困4 天前
Arthas trace 命令怎么用?一行定位最慢那行代码
jvm·后端