JVM之Java虚拟机内存结构


概述

Java虚拟机内存结构,也叫运行时数据区,根据《Java 虚拟机规范》,Java 虚拟机的内存结构可以分为公有和私有两部分。公有指的是所有线程都共享的部分,指的是 Java 堆、方法区、常量池。私有指的是每个线程的私有数据,包括:PC寄存器、Java 虚拟机栈、本地方法栈。

1.公有部分:Java堆、方法区、常量池

在 Java 虚拟机中,线程共享部分包括 Java 堆、方法区及常量池。

Java堆

Java 堆专门用于 Java 实例对象的内存分配,几乎所有实例对象都在会这里进行内存的分配。

Java 堆根据对象存活时间的不同,Java 堆还被分为年轻代、老年代两个区域,年轻代还被进一步划分为 Eden 区、From Survivor 0、To Survivor 1 区。如下图所示。

当有对象需要分配时,一个对象永远优先被分配在年轻代的 Eden 区,等到 Eden 区域内存不够时,Java 虚拟机会启动垃圾回收。此时 Eden 区中没有被引用的对象的内存就会被回收,而一些存活时间较长的对象则会进入到老年代。在 JVM 中有一个名为 -XX:MaxTenuringThreshold 的参数专门用来设置晋升到老年代所需要经历的 GC 次数,即在年轻代的对象经过了指定次数的 GC 后,将在下次 GC 时进入老年代。

这里让我们思考一个问题:为什么 Java 堆要进行这样一个区域划分呢?

根据我们的经验,虚拟机中的对象必然有存活时间长的对象,也有存活时间短的对象,这是一个普遍存在的正态分布规律。如果我们将其混在一起,那么因为存活时间短的对象有很多,那么势必导致较为频繁的垃圾回收。而垃圾回收时不得不对所有内存都进行扫描,但其实有一部分对象,它们存活时间很长,对他们进行扫描完全是浪费时间。因此为了提高垃圾回收效率,分区就理所当然了。

另外一个值得我们思考的问题是:为什么默认的虚拟机配置,Eden:from :to = 8:1:1 呢?

其实这是 IBM 公司根据大量统计得出的结果。根据 IBM 公司对对象存活时间的统计,他们发现 80% 的对象存活时间都很短。于是他们将 Eden 区设置为年轻代的 80%,这样可以减少内存空间的浪费,提高内存空间利用率。

方法区

在jvm启动时会加载class文件,把每个类的结构信息放到方法区,方法区指的是存储 Java 类字节码数据的一块区域,它存储了每一个类的结构信息,例如运行时常量池、字段和方法数据、构造方法等。运行时常量池属于方法区的一部分。方法区在不同版本的虚拟机有不同的表现形式,例如在 1.7 版本的 HotSpot 虚拟机中,方法区被称为永久代(Permanent Space),而在 JDK 1.8 中则被称之为 元空间(MetaSpace),并且为了减少OOM发生的可能,元空间使用了直接内存。

2.私有部分

程序计数器

CPU会在多个线程之间进行切换,为了记录每个线程执行的位置。

  • 若执行的是java方法,那么计数器记录的就是 虚拟机字节码指令地址。
  • 若执行的是 native 方法,那么计数器保存的值是 空(undefined)。

程序计数器是唯一一个不会发生OOM的区域

Java虚拟机栈

作用:存储Java方法调用的栈帧(Stack Frame),每个方法调用对应一个栈帧。

  • 栈帧包含:
    • 局部变量表(方法参数、局部变量)。
    • 操作数栈(用于计算中间结果)。
    • 动态链接(指向方法所属类的符号引用)。
    • 方法返回地址(方法结束后返回的位置)。

特点:

  • 线程私有,生命周期与线程相同。
  • StackOverflowError:当栈深度超过限制(如无限递归)。
  • OutOfMemoryError:若栈可动态扩展但内存不足。

本地方法栈

作用:为JVM调用本地(Native)方法(如C/C++代码)服务。

特点:

  • 线程私有。
  • 可能抛出与虚拟机栈相同的错误(StackOverflowError/OOM)。
  • HotSpot JVM中,本地方法栈与虚拟机栈合并。

3.内存泄露与内存溢出

内存泄漏(Memory Leak)和内存溢出(OOM)是内存管理中的两个核心问题,虽然都与内存相关,但它们的本质原因和表现场景不同。以下是详细对比与解析:

内存泄露

定义

  • 内存泄漏:程序在运行过程中,由于代码设计问题或资源管理不当,导致不再使用的对象无法被垃圾回收(GC)释放,从而占用内存空间的现象。
  • 本质:对象该回收的没回收。

常见场景

使用了ThreadLocal,在线程执行结束的没有remove,导致对象无法被回收。

影响

  • 短期:可能无明显症状,但内存占用逐步增加。
  • 长期:累积到一定程度后,可能触发内存溢出(OOM)。

内存溢出

定义

  • 内存溢出:程序在申请内存时,没有足够的空间供其使用,导致JVM抛出OutOfMemoryError。
  • 本质:内存资源不足(“想要的内存不够用了”)。

常见场景

  1. 堆内存溢出(Java Heap Space)
    • 原因:对象数量超过堆容量(如一次性加载超大文件到内存)。
    • 错误信息:java.lang.OutOfMemoryError: Java heap space。
  2. 方法区/元空间溢出(Metaspace)
    • 原因:加载过多类(如动态生成类、反射滥用)。
    • 错误信息:java.lang.OutOfMemoryError: Metaspace(Java 8+)。
  3. 栈溢出(StackOverflowError)
    • 原因:线程请求的栈深度超过JVM限制(如无限递归)。
    • 错误信息:java.lang.StackOverflowError。
  4. 直接内存溢出(Direct Buffer Memory)
    • 原因:频繁使用NIO的ByteBuffer.allocateDirect()分配堆外内存,未及时释放。
    • 错误信息:java.lang.OutOfMemoryError: Direct buffer memory。
  5. GC效率低下导致的OOM
    • 原因:垃圾回收器频繁工作,但每次回收的内存极少(如老年代内存不足)。
    • 错误信息:java.lang.OutOfMemoryError: GC Overhead Limit Exceeded。

4.总结

Java 虚拟机的内存结构是学习虚拟机所必须掌握的地方,其中以 Java 堆的内存模型最为重要,因为线上问题很多时候都是 Java 堆出现问题。因此掌握 Java 堆的划分以及常用参数的调整最为关键。


文章作者: Fuchanglai
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Fuchanglai !
赏
  目录