深入理解JVM《JVM内存区域详解 - 世界的基石》

简介: Java代码从编译到执行需经javac编译为.class字节码,再由JVM加载运行。JVM内存分为线程私有(程序计数器、虚拟机栈、本地方法栈)和线程共享(堆、方法区)区域,其中堆是GC主战场,方法区在JDK 8+演变为使用本地内存的元空间,直接内存则用于提升NIO性能,但可能引发OOM。

1.1 概述:Java代码的执行之旅

一段简单的Java代码System.out.println("Hello JVM");,从源代码到屏幕输出,需要经历编译和运行两个阶段。

  1. 编译期:JDK中的javac编译器将.java源文件编译成JVM能够理解的.class字节码文件。字节码是一种平台无关的中间代码,它是JVM的“机器语言”。
  2. 运行期:JVM负责加载.class文件,通过类加载器(Class Loader) 将其载入内存,然后由执行引擎(Execution Engine) 解释或编译(JIT编译器)成特定操作系统的本地机器码并执行。

而这个加载和执行过程所发生的地方,就是运行时数据区(Runtime Data Areas),它是JVM内存模型的核心,是Java世界的物理基石。理解它,是理解一切JVM原理和调优的基础。

下图清晰地展示了JVM运行时数据区的核心组成部分及其关系:

1.2 线程私有区域:程序的执行现场

线程私有区域的生命周期与线程相同,随线程的启动而创建,随线程的结束而销毁。

程序计数器(Program Counter Register)

  • 定义与作用:它可以看作是当前线程所执行的字节码的行号指示器。执行引擎通过改变这个计数器的值来选取下一条需要执行的字节码指令。分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
  • 特性
  • 线程私有:每个线程都有自己独立的程序计数器,互不干扰。这是JVM内存区域中唯一一个没有规定任何OutOfMemoryError情况的区域
  • 如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址。
  • 如果正在执行的是Native方法(本地方法,如C代码),这个计数器值则为空(Undefined)。

Java虚拟机栈(Java Virtual Machine Stacks)

  • 定义与作用:描述的是Java方法执行的内存模型。每个方法在执行的同时都会创建一个栈帧(Stack Frame),用于存储局部变量表操作数栈动态链接方法出口等信息。每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
  • 特性:线程私有。
  • 可能产生的错误
  • StackOverflowError:如果线程请求的栈深度大于虚拟机所允许的深度(例如,一个方法无限递归调用自己),将抛出此错误。
  • OutOfMemoryError:如果虚拟机栈可以动态扩展(当前大部分Java虚拟机都可动态扩展,但也允许固定长度的虚拟机栈),而在扩展时无法申请到足够的内存,就会抛出此异常。

栈帧(Stack Frame)结构详解

每个栈帧包含以下核心部分:

  1. 局部变量表(Local Variables)
  2. 一个数字数组,主要用于存储方法参数和方法内部定义的局部变量
  3. 变量槽(Slot) 为最小单位。对于64位的longdouble类型变量,会占用两个连续的Slot。
  4. 在方法执行时,虚拟机使用局部变量表来完成参数值到参数变量列表的传递过程
  5. 操作数栈(Operand Stack)
  6. 也常称为操作栈,它是一个后入先出(LIFO)栈
  7. 方法的执行过程,就是执行引擎不断地将常量或变量从局部变量表压入操作数栈,再进行各种计算,最后将结果出栈并存储的过程。可以理解为方法计算的工作区
  8. 动态链接(Dynamic Linking)
  9. 每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用。持有这个引用是为了支持方法调用过程中的动态连接
  10. 在Class文件的常量池中存有大量的符号引用(如方法名、描述符)。字节码中的方法调用指令就以常量池中指向方法的符号引用作为参数。动态链接的作用就是将这些符号引用转换为调用方法的直接引用
  11. 方法返回地址(Return Address)
  12. 存放调用该方法的程序计数器的值
  13. 方法退出后,需要返回到方法被调用的位置,程序才能继续执行。无论是正常返回(return)还是异常退出(抛出异常),都必须返回到这个位置。

本地方法栈(Native Method Stack)

  • 定义与作用:与虚拟机栈所发挥的作用非常相似。其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的Native方法服务
  • 特性与错误:与虚拟机栈一样,本地方法栈区域也会抛出StackOverflowErrorOutOfMemoryError异常。
  • 在HotSpot虚拟机中,本地方法栈和Java虚拟机栈是同一个,并不做区分。

1.3 线程共享区域:数据的宇宙中心

Java堆(Java Heap)

  • 定义与作用:这是JVM管理的最大的一块内存,此内存区域的唯一目的就是存放对象实例。几乎所有的对象实例以及数组都在这里分配内存。“几乎”是因为现代JVM的优化技术(如逃逸分析)可能导致对象在栈上分配。
  • 特性线程共享,是垃圾收集器管理的主要区域,因此也被称作“GC堆”。
  • 可能产生的错误OutOfMemoryError: Java heap space。如果在堆中没有内存完成实例分配,并且堆也无法再扩展时,抛出此异常。

堆的内存划分

垃圾回收(GC) 的角度来看,现代收集器基本都采用分代收集算法,所以Java堆可以细分为:

  • 新生代(Young Generation):新创建的对象首先在这里分配。
  • Eden区:对象诞生的地方。
  • Survivor区(S0和S1,也称为From和To):用于存放Minor GC后存活的对象。两个Survivor区大小永远一致,互为备份。
  • 老年代(Old/Tenured Generation):在新生代中经历了多次GC后仍然存活的对象(默认15次),会被晋升到老年代。一些大对象(如长数组)也可能直接在老年代分配。

这种划分是为了更好地管理对象的生命周期,从而采用更高效(Stop-The-World时间更短)的垃圾收集算法。

方法区(Method Area)与运行时常量池

  • 定义与作用:用于存储已被虚拟机加载的类型信息常量静态变量即时编译器编译后的代码缓存等数据。
  • 特性线程共享
  • 可能产生的错误OutOfMemoryError: Metaspace(JDK 8+)。

演进:从永久代(PermGen)到元空间(Metaspace)

这是一个非常重要的演变,也是面试常考点。

  • JDK 7及之前:方法区在HotSpot虚拟机中被称为永久代(Permanent Generation)。它使用JVM的堆内存来实现方法区。
  • JDK 8及之后:永久代被完全移除,取而代之的是元空间(Metaspace)。元空间不在虚拟机内存中,而是使用本地内存(Native Memory)

演变的主要原因

  1. 解决内存溢出问题:永久代有固定的上限(-XX:MaxPermSize),很容易在动态生成大量类(如CGLib、JSP)时出现OutOfMemoryError: PermGen space。而元空间使用本地内存,默认只受本地内存大小的限制,可通过-XX:MaxMetaspaceSize设定上限。
  2. 促进融合:将HotSpot与JRockit等JVM融合,JRockit本就没有永久代。
  3. 提高性能:永久代的垃圾回收效率低,且与老年代共用收集器,调优困难。元空间由元空间虚拟机自行管理,效率更高。
  • 运行时常量池(Runtime Constant Pool):是方法区的一部分。Class文件中除了有类的版本、字段、方法、接口等描述信息外,还有一项是常量池表(Constant Pool Table),用于存放编译期生成的各种字面量符号引用,这部分内容将在类加载后存放到方法区的运行时常量池中。String.intern()方法的行为与运行时常量池密切相关。

1.4 直接内存(Direct Memory)

  • 定义与作用:并不是虚拟机运行时数据区的一部分,也不是《Java虚拟机规范》中定义的内存区域。但它被频繁使用(如NIO的DirectByteBuffer),可能导致内存溢出。
  • 原理:通过在Java堆中分配一个对象,这个对象作为这块内存的引用进行操作,避免了在Java堆和Native堆中来回复制数据,能显著提高性能。
  • 特性:受本机总内存大小限制。
  • 可能产生的错误:虽然不受JVM内存参数限制,但若各个内存区域总和大于物理内存限制,动态扩展时会导致OutOfMemoryError。可通过-XX:MaxDirectMemorySize参数指定大小。

1.5 内存区域核心特性对比表

内存区域

是否线程私有

是否GC主要区域

可能产生的错误

作用

程序计数器

当前线程执行的字节码行号指示器

Java虚拟机栈

StackOverflowError
OutOfMemoryError

存储Java方法执行的栈帧

本地方法栈

StackOverflowError
OutOfMemoryError

为Native方法服务

Java堆

OutOfMemoryError: Java heap space

存放几乎所有对象实例和数组

方法区/元空间

(卸载类)

OutOfMemoryError: Metaspace

存储类信息、常量、静态变量等

直接内存

-

OutOfMemoryError

NIO使用的堆外内存,提高IO性能

1.6 本章小结与面试精要

核心知识点

  1. JVM内存分为线程私有(PC、JVM栈、本地方法栈)和线程共享(堆、方法区)两大部分。
  2. 是GC的主战场,分为新生代和老年代。
  3. 方法区在JDK 8+称为元空间,使用本地内存,其大小可动态调整,解决了永久代的OOM问题。
  4. 直接内存不属于JVM运行时数据区,但频繁使用且可能导致OOM。

常见面试问题

  • 说一下JVM的内存区域划分?
  • 堆和栈的区别是什么?
  • 永久代为什么被元空间取代?
  • String.intern()方法有什么作用?它与运行时常量池有什么关系?
  • 堆外内存(直接内存)如何分配和回收?
  • 如何理解“栈帧”?

在下一部分,我们将深入垃圾收集机制,探讨JVM如何自动清理这些内存区域中的“垃圾”,让Java世界保持整洁。

相关文章
|
6月前
|
Arthas 存储 算法
深入理解JVM,包含字节码文件,内存结构,垃圾回收,类的声明周期,类加载器
JVM全称是Java Virtual Machine-Java虚拟机JVM作用:本质上是一个运行在计算机上的程序,职责是运行Java字节码文件,编译为机器码交由计算机运行类的生命周期概述:类的生命周期描述了一个类加载,使用,卸载的整个过类的生命周期阶段:类的声明周期主要分为五个阶段:加载->连接->初始化->使用->卸载,其中连接中分为三个小阶段验证->准备->解析类加载器的定义:JVM提供类加载器给Java程序去获取类和接口字节码数据类加载器的作用:类加载器接受字节码文件。
554 55
|
18天前
|
存储 缓存 Java
我们来说一说 JVM 的内存模型
我是小假 期待与你的下一次相遇 ~
143 4
|
7月前
|
Arthas 监控 Java
Arthas memory(查看 JVM 内存信息)
Arthas memory(查看 JVM 内存信息)
533 6
|
10月前
|
存储 设计模式 监控
快速定位并优化CPU 与 JVM 内存性能瓶颈
本文介绍了 Java 应用常见的 CPU & JVM 内存热点原因及优化思路。
993 166
|
12月前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
1949 1
|
8月前
|
存储 缓存 算法
JVM简介—1.Java内存区域
本文详细介绍了Java虚拟机运行时数据区的各个方面,包括其定义、类型(如程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和直接内存)及其作用。文中还探讨了各版本内存区域的变化、直接内存的使用、从线程角度分析Java内存区域、堆与栈的区别、对象创建步骤、对象内存布局及访问定位,并通过实例说明了常见内存溢出问题的原因和表现形式。这些内容帮助开发者深入理解Java内存管理机制,优化应用程序性能并解决潜在的内存问题。
349 29
JVM简介—1.Java内存区域
|
8月前
|
缓存 监控 算法
JVM简介—2.垃圾回收器和内存分配策略
本文介绍了Java垃圾回收机制的多个方面,包括垃圾回收概述、对象存活判断、引用类型介绍、垃圾收集算法、垃圾收集器设计、具体垃圾回收器详情、Stop The World现象、内存分配与回收策略、新生代配置演示、内存泄漏和溢出问题以及JDK提供的相关工具。
JVM简介—2.垃圾回收器和内存分配策略
|
8月前
|
存储 设计模式 监控
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?
200 0
如何快速定位并优化CPU 与 JVM 内存性能瓶颈?
|
9月前
|
存储 算法 Java
JVM: 内存、类与垃圾
分代收集算法将内存分为新生代和老年代,分别使用不同的垃圾回收算法。新生代对象使用复制算法,老年代对象使用标记-清除或标记-整理算法。
114 6
|
11月前
|
存储 Java 程序员
【JVM】——JVM运行机制、类加载机制、内存划分
JVM运行机制,堆栈,程序计数器,元数据区,JVM加载机制,双亲委派模型
281 10