当前位置: 首页 > news >正文

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

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

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

inta=10;

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

再比如:

Objectobj=newObject();

这里既有变量引用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:方法调用太深,栈帧不断压入,栈空间撑爆了。最常见的原因是递归没有终止条件。

publicclassStackOverflowDemo{publicstaticvoidmain(String[]args){recurse(0);}staticvoidrecurse(intdepth){// 每调一次,压入一个栈帧,直到栈空间不够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],对象的内存都来自堆。前面说的虚拟机栈里存的是引用,真正的对象本体在堆里。

来看一个简单的例子:

publicvoidcreate(){Objectobj=newObject();}

调用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 就完成的变更。

实战:变量去哪了

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

publicclassMemoryDemo{// 静态字段属于类变量// HotSpot JDK8+ 中存放在堆中的 Class 对象相关区域privatestaticintcount=0;publicstaticvoidmain(String[]args){// args 引用 → 虚拟机栈(main 方法栈帧)// args 数组对象 → 堆count++;createObject();}publicstaticObjectcreateObject(){// 局部变量 a → 虚拟机栈(createObject 方法栈帧)inta=10;// obj 引用 → 虚拟机栈// Object 实例 → 堆Objectobj=newObject();returnobj;}}

逐个拆解:

变量存储位置说明
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 用什么算法判断可达性,分代模型是怎么回事——这些问题,是下一篇的事了。

http://www.jsqmd.com/news/1269848/

相关文章:

  • Docker容器化部署.NET API应用实战指南
  • 零预算实现企业级HTTPS:Cloudflare+Docker+Nginx实战
  • [转] 2011年中国各省政区图及港澳台地区地图
  • REFramework终极指南:用简单脚本和VR支持彻底改变你的RE引擎游戏体验
  • Dragonfly实战指南:企业级P2P文件分发与分布式下载加速高效方案
  • 联想拯救者工具箱终极指南:开源性能管理工具深度解析
  • 淀水藏岁月,打捞守心安——白洋淀迅捷水下打捞队,深耕水乡四十余载的水下守护者 - 国麟测评
  • SimpleRemote高级功能揭秘:如何批量管理多台远程服务器?
  • 如何快速解决Minecraft MASA模组全家桶汉化难题:终极中文界面完整指南
  • 终极暗黑破坏神II角色编辑器:3步打造完美游戏体验
  • LeetCode公司题库数据仓库:537家企业面试题目分类整理终极指南
  • 深入解析CC27xx射频寄存器LRFDRFE32:原理、API配置与调试实践
  • 想给《我的世界》换背景音乐却总报错?2026 免费音频转 OGG 工具,游戏音效直接替换,开源格式全兼容。 - 今日咨询
  • 2026金九银十|Java后端面试题大全(附答案详解),一篇通关后端面试
  • XActivatePowerMode未来展望:8bit BGM与更多功能即将上线
  • 单招语文积累不够拿低分?成都融创单招:真题阅读定位法短期快速提分 - 成都单招培训
  • 2026 潮汕本地导游怎么选?费用、预约、避坑完整攻略 - 纯玩旅游推荐官
  • 3分钟快速上手:用MoocDownloader轻松实现中国大学MOOC课程离线学习
  • 【路径规划】基于改进的智能水滴算法求解送取货且带时间窗的车辆路径与调度优化问题matlab代码
  • 上海闲置大牌包怎么高价变现?静安线下、黄浦门店、浦东上门回收全对比 - 讯息早知道
  • Python毕业设计-基于 Python 机器学习的智能邮件分类系统设计与实现 面向垃圾邮件识别的文本分类系统开发(源码+LW+部署文档+全bao+远程调试+代码讲解等)
  • 浅谈上海二手腕表市场:热门运动款与复古老款回收逻辑区别 - 讯息早知道
  • 重新定义扑克策略分析:如何用TexasSolver实现GTO求解的工程突破
  • HarmonyOS应用《玄象》开发实战:掷钱动画:animateTo + 缓动曲线的物理感模拟
  • OpenClaw:AI驱动的智能自动化工具解析与应用
  • VMD-BiLSTM在电力负荷预测中的工程实践
  • Unity PSD自动化导入:从设计稿到UI预制体的高效工作流
  • AI Agent工程实践:架构设计与性能优化
  • 浏览器资源嗅探神器:猫抓如何解决你的在线内容保存难题?
  • Codex Sites Analytics网站数据分析工具:从集成到实战应用指南