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

JVM之Java内存区域与内存溢出异常

Java虚拟机在执行Java程序的过程中,会把它所管理的内存划分为若干个不同的数据区域。这些区域各有各的用途,以及创建和销毁的时间。理解这些区域的划分、作用、生命周期以及可能发生的内存溢出异常,是进行JVM调优和线上问题排查的基础。


第一部分:JVM内存区域划分(运行时数据区)

根据《Java虚拟机规范(Java SE 8版)》的规定,JVM在执行Java程序的过程中,会管理以下几种运行时数据区域。我们可以将其分为两大类:线程共享区线程私有区

A. 线程共享区

这类区域是所有线程都可以访问的,它们随着JVM进程的启动而创建,随着JVM进程的结束而销毁。

1. 堆

作用:Java世界中最大的一块内存区域,几乎所有的对象实例以及数组都在这里分配内存。这也是垃圾收集器管理的主要区域,因此有时也被称为“GC堆”。

特点:
物理上不连续,逻辑上连续:堆的物理内存空间可以是不连续的,但在逻辑上它被视为一个连续的地址空间。
动态扩展:堆的大小可以是固定的,也可以是动态扩展的(通过 -Xms 和 -Xmx 参数设置初始和最大值)。
线程共享:所有线程共享堆内存,因此多线程在堆上创建对象时需要考虑线程安全问题。
分代设计:为了更高效地进行垃圾回收,现代垃圾收集器通常将堆划分为新生代和老年代。
新生代:又细分为一个 Eden区 和两个 Survivor区(From/To)。绝大多数新创建的对象首先被分配在Eden区。
老年代:在新生代中经过多次Minor GC仍然存活的对象,会被“晋升”(Promote)到老年代。

2. 方法区

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

特点:
线程共享:所有线程共享方法区。
内存回收目标:虽然方法区的垃圾回收(如对废弃常量和无用的类的卸载)效率不高,但并非完全不进行回收。
实现差异:这是JVM规范中的一个概念,不同虚拟机的实现方式不同。
在JDK 7及之前:HotSpot虚拟机使用“永久代”(Permanent Generation, PermGen)来实现方法区。永久代是堆的一部分,有自己的内存上限(可通过 -XX:MaxPermSize 设置)。
在JDK 8及之后:HotSpot虚拟机移除了永久代,改用元空间来实现方法区。元空间使用的是本地内存,而不是JVM堆内存。这使得方法区的大小不再受JVM设定的最大堆大小的限制,而是受限于本地可用内存。

3. 运行时常量池

作用:是方法区的一部分。用于存放编译期生成的各种字面量符号引用。这部分内容将在类加载后存放到方法区的运行时常量池中。

特点:
动态性:运行时常量池具备动态性,并非只有预置入Class文件中常量池的内容才能进入,运行期间也可以将新的常量放入池中,例如 String 类的 intern() 方法。
位置变迁:在JDK 7之前,字符串常量池也存放在方法区(永久代)中。从JDK 7开始,字符串常量池被移到了堆中。JDK 8之后,方法区整体由元空间实现,运行时常量池也随之移至元空间。

B. 线程私有区

这类区域是每个线程独立拥有的,它们的生命周期与线程的生命周期相同。

1. 程序计数器
  • 作用:一块较小的内存空间,可以看作是当前线程所执行的字节码的行号指示器
  • 特点
    • 线程私有:为了线程切换后能恢复到正确的执行位置,每条线程都需要一个独立的程序计数器。
    • 唯一无OOM的区域:如果线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;如果正在执行的是本地方法,这个计数器值则为空。此内存区域是唯一一个在《Java虚拟机规范》中没有规定任何 OutOfMemoryError 情况的区域。
2. Java虚拟机栈
  • 作用:描述的是Java方法执行的线程内存模型。每个方法在执行时,JVM都会同步创建一个栈帧,用于存储局部变量表操作数栈动态链接方法出口等信息。
  • 特点
    • 线程私有:生命周期与线程相同。
    • 栈帧:每个方法从调用到执行完毕,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。
    • 局部变量表:存放了编译期可知的各种基本数据类型、对象引用和 returnAddress 类型(指向了一条字节码指令的地址)。局部变量表所需的内存空间在编译期间完成分配。
3. 本地方法栈
  • 作用:与虚拟机栈非常相似,其区别在于:虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则为虚拟机使用到的本地方法服务。
  • 特点
    • 线程私有
    • 实现灵活:《Java虚拟机规范》对本地方法栈的实现方式没有强制规定,具体的虚拟机可以自由实现它。例如,HotSpot虚拟机直接就把本地方法栈和虚拟机栈合二为一。

第二部分:内存溢出异常

内存溢出,即 OutOfMemoryError,是指程序在申请内存时,没有足够的内存空间供其使用。当JVM在各个内存区域中无法分配到足够的内存,并且也无法再扩展时,就会抛出该异常。

1. Java堆溢出

现象java.lang.OutOfMemoryError: Java heap space

原因:
内存泄漏:程序中存在无用但未被回收的对象,导致它们持续占用堆内存。随着时间的推移,可用内存越来越少,最终耗尽。这是最常见的原因。
内存溢出:程序确实需要分配一个非常大的对象(如巨大的数组),超出了堆的最大容量限制。
排查与解决:
排查:
通过 -Xms 和 -Xmx 参数设置堆的初始和最大大小为相同值,避免堆自动扩展。
启动应用,使用内存分析工具(如 Eclipse MAT, JProfiler, VisualVM)分析堆转储快照。
在快照中,首先确认是内存泄漏还是内存溢出。如果是内存泄漏,查看导致泄漏的对象是通过什么引用链与GC Roots关联并导致无法被回收的。
解决:
如果是内存泄漏,修复代码中的泄漏点(如未关闭的资源、未取消的监听器、静态集合持有对象引用等)。
如果是内存溢出,检查业务逻辑是否合理,或者考虑增加堆内存大小(-Xmx),优化数据结构(如使用更节省内存的结构),或者分批处理大数据。

2. 虚拟机栈和本地方法栈溢出

由于HotSpot虚拟机不区分虚拟机栈和本地方法栈,因此 -Xoss 参数(设置本地方法栈大小)对于HotSpot是无效的,栈容量只由 -Xss 参数设定。
现象:
如果线程请求的栈深度大于虚拟机所允许的最大深度,将抛出 StackOverflowError。
如果虚拟机的栈内存允许动态扩展,当扩展栈容量无法申请到足够的内存时,将抛出 OutOfMemoryError。
原因:
StackOverflowError:通常是由于无限递归调用导致的。每次方法调用都会在栈中压入一个栈帧,如果递归没有出口,栈帧会一直压入,直到超过栈的最大深度。
OutOfMemoryError:当不断建立新线程时,可能导致栈内存溢出。每个线程都需要分配独立的栈空间,如果系统内存不足以创建新的线程,就会抛出此异常。
排查与解决:
StackOverflowError:检查代码中是否存在无限递归或循环调用层级过深的情况。如果是业务逻辑需要很深的调用栈,可以适当增大栈大小(-Xss)。
OutOfMemoryError:检查是否创建了过多的线程。如果是,考虑使用线程池来复用线程,而不是无限制地创建新线程。或者减少单个线程的栈大小(-Xss),但这可能导致 StackOverflowError,需要权衡。

3. 方法区(或元空间)溢出

现象:
JDK 7及之前 (永久代):java.lang.OutOfMemoryError: PermGen space
JDK 8及之后 (元空间):java.lang.OutOfMemoryError: Metaspace
原因:
加载了过多的类:在应用中动态生成大量的类(如使用CGLIB、反射、JSP等),或者部署了大量的应用,导致类信息耗尽了方法区/元空间。
运行时常量池溢出:例如,在JDK 6或更早版本中,String.intern() 方法会将其内容复制到永久代的字符串常量池中,如果调用 intern() 的字符串过多,也会导致永久代溢出。
排查与解决:
排查:检查应用是否使用了动态生成类的技术,或者是否存在类加载器泄漏(例如,在Web容器中重复部署应用时,旧的类加载器及其加载的类没有被卸载)。
解决:
对于永久代,可以尝试增大 -XX:MaxPermSize。
对于元空间,可以增大 -XX:MaxMetaspaceSize(默认没有上限,受限于本地内存)。
更根本的解决方案是优化代码,避免不必要的类加载,或修复类加载器泄漏问题。

4. 本地直接内存溢出

现象:java.lang.OutOfMemoryError: Direct buffer memory
原因:
直接内存并不是JVM运行时数据区的一部分,但它也会导致 OutOfMemoryError。它通过 java.nio.ByteBuffer.allocateDirect() 方法分配,使用的是 Unsafe 类的 allocateMemory() 方法,直接在堆外分配内存。虽然直接内存的分配不受JVM堆大小的限制,但它会受到本机总内存(包括RAM和SWAP区)的限制。如果 -XX:MaxDirectMemorySize 参数被设置,则受此参数限制。
当应用程序频繁申请直接内存,而回收不及时(依赖于 System.gc() 或 Cleaner 机制),就可能耗尽直接内存。
排查与解决:
排查:检查代码中是否大量使用了 NIO 的 DirectBuffer。
解决:
合理使用 DirectBuffer,及时释放不再需要的 DirectBuffer 对象(通过设置其引用为null,使其可被GC回收,从而触发 Cleaner 释放本地内存)。
可以通过 -XX:MaxDirectMemorySize 参数来限制直接内存的大小,防止其无限制地消耗系统内存。


总结

内存区域线程共享/私有可能的异常异常原因解决思路
共享OutOfMemoryError: Java heap space对象创建过多且无法回收(内存泄漏),或单个对象过大分析堆快照,修复泄漏,优化代码,或增大堆大小
方法区/元空间共享OutOfMemoryError: PermGen space (JDK 7)
OutOfMemoryError: Metaspace (JDK 8+)
加载的类信息过多,或运行时常量池过大检查动态类生成,修复类加载器泄漏,或增大方法区/元空间大小
虚拟机栈/本地方法栈私有StackOverflowError线程请求的栈深度过大(如无限递归)检查递归代码,优化调用深度,或增大栈大小
虚拟机栈/本地方法栈私有OutOfMemoryError创建线程过多,导致栈内存耗尽减少线程数量,使用线程池,或减小单个线程栈大小
程序计数器私有--
直接内存-OutOfMemoryError: Direct buffer memoryNIO中直接内存分配过多,回收不及时合理使用DirectBuffer,及时释放,或设置最大直接内存大小
http://www.dtcms.com/a/338589.html

相关文章:

  • 微服务-06.微服务拆分-拆分原则
  • 117. 软件构建,拓扑排序,47. 参加科学大会,dijkstra算法
  • webpack》》Plugin 原理
  • VSCode 从安装到精通:下载安装与快捷键全指南
  • 视觉采集模块的用法
  • 企业知识管理革命:RAG系统在大型组织中的落地实践
  • 大数据数据库 —— 初见loTDB
  • 最新研究进展:2023-2025年神经机器翻译突破性成果
  • 【无标题】基于大数据+Python的共享单车骑行数据分析关系可视化 基于Spark+Hadoop的共享单车使用情况监测与数据可视化
  • AI 药物发现:化学分子到机器学习数值特征的转化——打通“化学空间”与“模型空间”关键路径
  • 大语言模型基本架构
  • 全网首发CentOS 7.6安装openGauss 6.0.2 LTS企业版(单机)
  • Linux------《零基础到联网:CentOS 7 在 VMware Workstation 中的全流程安装与 NAT 网络配置实战》
  • vue3实现实现手机/PC端录音:recorder-core
  • Apache IoTDB(4):深度解析时序数据库 IoTDB 在Kubernetes 集群中的部署与实践指南
  • Chrome原生工具网页长截图方法
  • 实现Johnson SU分布的参数计算和优化过程
  • STM32 vscode 环境, 官方插件
  • 进程通信:进程池的实现
  • JUC之CompletableFuture【上】
  • PythonDay31
  • 力扣(电话号码的字母组合)
  • 如何安全删除GitHub中的敏感文件?git-filter-repo操作全解析
  • STM32 定时器(主从模式实现 3路PWM相位差)
  • c#联合halcon的基础教程(案例:亮度计算、角度计算和缺陷检测)(含halcon代码)
  • 运维监控prometheus+grafana
  • 深入理解Java中的四类引用:强、软、弱、虚引用
  • 【科研绘图系列】R语言绘制多组火山图
  • 第六天~提取Arxml中CAN Node节点信息Creat_ECU
  • STL库——string(类模拟实现)