🎬 HoRain 云小助手个人主页

⛺️生活的理想,就是为了理想的生活!


⛳️ 推荐

前些天发现了一个超棒的服务器购买网站,性价比超高,大内存超划算!忍不住分享一下给大家。点击跳转到网站。

目录

⛳️ 推荐

🔧 核心内存区域详解

1. 程序计数器:执行线索的记录者

2. Java 虚拟机栈:方法执行的现场

3. 本地方法栈:Native 方法的支持

4. 堆:对象生存的乐园

5. 方法区:类型信息的仓库

💡 内存管理的演进:从永久代到元空间

🗑️ 垃圾回收与内存模型

💎 总结与实践建议


理解 Java 虚拟机(JVM)内存模型对于编写稳健、高性能的 Java 应用至关重要。它就像是 Java 程序运行的“城市布局”,不同区域承担不同职能,同时又需要高效的“市政管理”(垃圾回收)来保持秩序。下面这张表格汇总了各核心区域的关键信息,帮你快速建立整体认知。

内存区域

线程关系

核心功能

生命周期

异常错误

程序计数器

线程私有

存储当前线程所执行的字节码行号指示器,实现流程控制(分支、循环、跳转等)。

与线程共存亡

Java 虚拟机栈

线程私有

存储方法执行的栈帧,包括局部变量表、操作数栈、动态链接、方法出口信息等 。

与线程共存亡

StackOverflowError(栈深度超限)
OutOfMemoryError(扩展时内存不足)

本地方法栈

线程私有

为虚拟机使用到的 Native 方法服务 。

与线程共存亡

StackOverflowError
OutOfMemoryError

线程共享

存放对象实例和数组,是垃圾回收的主要区域 。

虚拟机启动时创建

OutOfMemoryError(内存不足且无法扩展)

方法区

线程共享

存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码缓存等数据 。

虚拟机启动时创建

OutOfMemoryError(内存不足且无法扩展)


🔧 核心内存区域详解

1. 程序计数器:执行线索的记录者

程序计数器是一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器​ 。在多线程环境下,Java 虚拟机通过时间片轮转的方式实现并发执行。当线程切换发生时,程序计数器能确保线程恢复执行时,能知道接下来该执行哪条指令 。执行 Java 方法时,它记录的是虚拟机字节码指令的地址;执行 Native 方法时,其值为空 (Undefined) 。此区域是唯一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError情况的区域 。

2. Java 虚拟机栈:方法执行的现场

每个方法在执行时,都会在虚拟机栈中创建一个栈帧,用于存储方法运行所需的各种信息 。

  • 局部变量表:存放编译期可知的基本数据类型(如 int, boolean等)、对象引用(reference类型)和 returnAddress类型。其中 64 位的 longdouble会占用 2 个局部变量空间(Slot),其余占用 1 个 。局部变量表的大小在编译期确定。

  • 操作数栈:用于方法执行过程中的计算。

  • 动态链接:将符号引用转换为直接引用。

  • 方法出口:记录方法返回的地址。

当线程请求的栈深度大于虚拟机所允许的深度(例如无限递归),会抛出 StackOverflowError。如果虚拟机栈可以动态扩展(大部分虚拟机都可动态扩展),但在扩展时无法申请到足够内存,则会抛出 OutOfMemoryError

3. 本地方法栈:Native 方法的支持

本地方法栈与虚拟机栈作用类似,区别在于虚拟机栈为执行 Java 方法服务,而本地方法栈则为虚拟机使用到的 Native 方法服务​ 。HotSpot 虚拟机直接把本地方法栈和虚拟机栈合二为一 。

4. 堆:对象生存的乐园

堆是 Java 虚拟机管理中最大的一块内存区域,被所有线程共享,在虚拟机启动时创建。此区域的唯一目的就是存放对象实例和数组​ 。堆是垃圾收集器管理的主要区域,因此也被称为 “GC 堆” 。从内存回收角度,Java 堆可细分为新生代(Young Generation)和老年代(Old Generation/Tenured Generation),新生代又可细分为 Eden 空间和 Survivor 空间(From Survivor, To Survivor),目的是为了更好地进行内存回收和分配 。

5. 方法区:类型信息的仓库

方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据​ 。方法区是 JVM 规范定义的逻辑部分,具体实现因版本和虚拟机而异。在 JDK 8 之前,HotSpot 虚拟机使用永久代来实现方法区。从 JDK 8 开始,永久代被移除,取而代之的是元空间,它使用本地内存(Native Memory)而非 JVM 内存,从而避免了永久代的内存溢出问题(如 java.lang.OutOfMemoryError: PermGen space),其大小仅受本地内存限制 。

运行时常量池是方法区的一部分,用于存放 Class 文件常量池中的符号引用和翻译出来的直接引用,具备动态性(如 String.intern()方法可在运行期间将新的常量放入池中)。在 JDK 7 时,字符串常量池已被移至堆中 。


💡 内存管理的演进:从永久代到元空间

特性

永久代(JDK 7 及以前)

元空间(JDK 8 及以后)

实现位置

JVM 堆内存中

本地内存(Native Memory)

内存限制

受 JVM 堆大小参数(如 -XX:MaxPermSize)限制

默认仅受本地内存大小限制,可通过 -XX:MaxMetaspaceSize设定上限

主要优势

与 JVM 堆统一管理

避免永久代内存溢出提升元数据管理灵活性;利于简化 GC 复杂度

主要挑战

易出现 java.lang.OutOfMemoryError: PermGen space;类元信息大小难确定,设置困难

需要关注本地内存使用情况


🗑️ 垃圾回收与内存模型

堆是垃圾收集器工作的主要区域。为了更高效地回收内存,堆被划分为新生代和老年代。

  • 新生代与 Minor GC:大多数新创建的对象首先分配在 Eden 区。当 Eden 区满时,会触发一次 Minor GC。存活的对象会被移动到 Survivor 区(S0, S1),并在 Survivor 区中经历多次 Minor GC 后,年龄(Age)达到阈值(默认 15)的对象会晋升到老年代 。新生代 GC 采用复制算法,因为新生代的对象存活率低,复制算法的效率高 。

  • 老年代与 Full GC:老年代存放长期存活的对象。当老年代空间不足或方法区(元空间)空间不足时,会触发 Full GC,其速度通常比 Minor GC 慢 10 倍以上。老年代 GC 一般采用标记-清除标记-整理算法 。


💎 总结与实践建议

  1. 理解核心分区:牢记程序计数器、虚拟机栈、本地方法栈、堆和方法区这五大核心区域及其线程属性、功能和异常。

  2. 关注版本演进:理解 JDK 8 中方法区从永久代到元空间的转变及其意义,这有助于避免配置错误和内存溢出。

  3. 善用 JVM 参数:根据应用特点合理设置堆大小(-Xms, -Xmx)、元空间大小(-XX:MaxMetaspaceSize)、线程栈大小(-Xss)等参数对性能调优至关重要。

  4. 掌握异常诊断:遇到 StackOverflowError通常意味着代码中存在过深递归或大量循环调用;而 OutOfMemoryError则需分析是堆内存不足、元空间不足还是其他原因。

希望这份详细的解析能帮助你深入理解 JVM 内存模型!如果你对垃圾回收算法的具体细节或性能调优实战有兴趣,我们可以继续深入探讨。

❤️❤️❤️本人水平有限,如有纰漏,欢迎各位大佬评论批评指正!😄😄😄

💘💘💘如果觉得这篇文对你有帮助的话,也请给个点赞、收藏下吧,非常感谢!👍 👍 👍

🔥🔥🔥Stay Hungry Stay Foolish 道阻且长,行则将至,让我们一起加油吧!🌙🌙🌙

Logo

有“AI”的1024 = 2048,欢迎大家加入2048 AI社区

更多推荐