资讯动态

递归太深报 StackOverflowError,对象太多报 OutOfMemoryError,根源是两块内存

发布时间:2026/9/8 17:20:25 来源:尧图企业网站定制
「Java 进阶之路」系列 Day31写在前面模块四异常与IO收官了从这篇开始进入模块五JVM与GC——这是面试里问法最深、也最容易被问倒的一块。第一篇先把地基打好JVM运行时到底把内存划成了哪几块各自存什么、归谁管后面讲类加载、GC都要在这个基础上展开。一、是什么JVM 运行时数据区的五个分区线程私有 每个线程各自一份虚拟机栈 存栈帧 局部变量表本地方法栈 服务Native方法程序计数器 记录当前执行到哪条字节码线程共享 JVM进程只有一份堆 Heap 存对象实例方法区 元空间 存类信息 静态变量 常量池堆Heap几乎所有new出来的对象实例都存在这里是JVM里最大的一块内存区域也是垃圾回收GC主要工作的地方。线程共享——所有线程都能访问同一个堆。虚拟机栈VM Stack每个线程私有每次调用一个方法都会创建一个栈帧压入这个线程自己的栈栈帧里存局部变量表、操作数栈、方法返回地址等信息方法执行完毕栈帧就出栈。本地方法栈Native Method Stack和虚拟机栈类似只是服务对象换成了native方法用C/C实现、通过JNI调用的方法。方法区Method Area存放类的结构信息字段、方法、运行时常量池等和static静态变量线程共享。JDK8之前这块内存实现叫永久代PermGenJDK8之后改叫元空间Metaspace这个变化下面细讲。程序计数器PC Register记录当前线程执行到了哪一条字节码指令每个线程私有占用内存很小是JVM规范里唯一不会发生OutOfMemoryError的区域。二、为什么要这样划分线程私有 vs 线程共享这套划分的核心逻辑是按归谁管、活多久来分区方法调用天然是每个线程各自独立进行的一个方法里的局部变量、调用到哪一步了只跟当前这个线程有关跟别的线程毫无关系所以虚拟机栈、本地方法栈、程序计数器都设计成线程私有——方法调用返回对应的栈帧数据立刻可以回收不需要垃圾回收器操心而对象实例和类信息则天然需要被多个线程共享访问比如两个线程操作同一个对象生命周期也更长、更不确定所以放进堆和方法区这两块线程共享的区域交给专门的垃圾回收机制去管理。这也解释了两种最常见的内存溢出异常分别对应哪块区域异常对应哪块内存典型触发场景StackOverflowError虚拟机栈递归调用没有终止条件或者递归层数太深栈帧不断压入超过栈的深度限制OutOfMemoryError: Java heap space堆对象持续被创建、又没办法被GC回收比如内存泄漏堆装不下了三、怎么用两个容易被忽略的进阶点不是所有对象都必须在堆上分配很多人把对象都在堆里当成绝对真理但JIT编译器在做逃逸分析Escape Analysis优化时如果能确认一个对象的作用范围完全局限在某个方法内部、不会被外部引用也就是不会逃逸出这个方法就有可能通过标量替换这类手段把对象拆解成几个基本类型变量直接在栈上分配而不占用堆内存。这样对象随着方法调用结束、栈帧出栈就自动释放了完全不需要垃圾回收器介入是JVM为了减轻GC压力做的一项性能优化。这是JVM内部的自动优化不需要开发者手动干预但作为知识点理解它有助于纠正堆是对象唯一去处这种过于绝对的印象。方法区为什么从永久代改成了元空间JDK8之前方法区是用永久代实现的本质上是堆内存的一部分大小受-XX:MaxPermSize这个参数限制、有一个固定上限。这在某些场景下很容易出问题——比如大量使用反射、动态代理CGLIB这类字节码生成技术的应用会在运行时动态生成大量的类这些类的信息全部要塞进永久代很容易把这块本来就大小固定的空间撑爆抛出OutOfMemoryError: PermGen space而且这个上限通常还得靠开发者手动调大参数去猜很不灵活。JDK8把方法区的实现换成了元空间元空间不再从JVM堆里划一块出来而是直接使用本地内存操作系统内存理论上限只受物理机器内存总量约束不再有一个人为设定的固定上限虽然依然可以用-XX:MaxMetaspaceSize去限制但默认情况下要宽松得多。这大幅缓解了这类动态生成大量类导致方法区被撑爆的问题是JDK8对JVM内存管理做的一项重要改进。四、面试追问Q1JVM运行时数据区分为哪几块哪些是线程私有的哪些是线程共享的分为堆、虚拟机栈、本地方法栈、方法区、程序计数器五块。堆和方法区是线程共享的所有线程访问的是同一份虚拟机栈、本地方法栈、程序计数器是线程私有的每个线程都有自己独立的一份互不干扰。Q2为什么虚拟机栈、程序计数器要设计成线程私有因为方法调用、代码执行到哪一步本质上是每个线程各自独立进行的过程一个线程的局部变量和执行进度跟别的线程毫无关系。设计成线程私有方法调用结束后对应的栈帧数据可以立即随之释放不需要垃圾回收器介入管理也天然避免了多线程访问这部分数据时的同步问题。Q3StackOverflowError 和 OutOfMemoryError 分别对应哪块内存区域触发场景有什么区别StackOverflowError对应虚拟机栈典型场景是递归调用没有正确的终止条件、或者递归层数太深栈帧不断压入超过了栈的深度限制OutOfMemoryError: Java heap space对应堆典型场景是对象持续被创建但因为内存泄漏等原因无法被垃圾回收堆内存被逐渐耗尽。两者分别对应线程私有区域和线程共享区域的资源耗尽。Q4是不是所有的对象实例都只能在堆上分配不完全是。JIT编译器在做逃逸分析优化时如果能判断出某个对象的作用范围完全局限在方法内部、不会被外部引用不会逃逸可能会通过标量替换等手段把这个对象直接在栈上分配随着方法调用结束自动释放不占用堆空间也不需要垃圾回收器介入。这是JVM为减轻GC压力做的自动优化开发者不需要手动干预。Q5JDK8为什么把方法区的实现从永久代改成了元空间永久代本质上是堆内存的一部分大小有固定上限在大量使用反射、动态代理这类会运行时动态生成大量类的场景下容易把这块空间撑爆抛出OutOfMemoryError: PermGen space。元空间直接使用本地内存而不是从JVM堆里划分空间理论上限只受物理机器内存总量约束大幅缓解了这类因为动态生成大量类导致方法区溢出的问题是JDK8对JVM内存管理的一项重要改进。下一篇预告Day32 讲类加载机制——双亲委派模型到底是什么为什么JVM要设计成这样一层一层往上委托而不是直接自己去加载类。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价