堆内存溢出更常见,因其多由业务逻辑复杂、内存泄漏、缓存未清理、全表查询等生产环境高频场景引发;栈溢出则多因递归无终止、调用过深等开发阶段问题导致,频率较低。

栈溢出是“调用太深”,堆溢出是“对象太多”。业务场景不同,防范重点也完全不同。
栈溢出的典型业务场景
栈内存属于线程私有,空间小(默认1MB左右),主要用于保存方法调用上下文和局部变量。溢出往往发生在以下真实业务环节:
- 深度递归处理树形结构:比如组织架构遍历、无限嵌套的JSON解析、工作流节点递归校验,未设最大深度或终止条件失效
- 高并发下线程栈配置过小:微服务中每个请求开一个线程,若使用-Xss128k又叠加多层AOP代理+框架拦截器,单次调用栈帧数轻松超限
-
局部变量占用过大:在方法内声明超大数组(如
new byte[1024 * 1024])、序列化临时缓存、日志拼接生成超长字符串等
堆溢出的典型业务场景
堆内存线程共享,容量大(常设为几GB),用于存放所有对象实例。溢出多源于长期运行中的资源累积,常见于:
-
缓存滥用:用
HashMap或静态集合做本地缓存,未加淘汰策略或过期机制,用户会话、报表数据持续堆积 -
数据库结果集全量加载:分页缺失时
SELECT *查百万级记录进List<entity></entity>,尤其配合MyBatis的resultType自动映射 -
资源未释放:文件流、数据库连接、HTTP客户端响应体未显式
close(),导致关联的缓冲区对象长期存活 - 监听器/回调注册未注销:GUI应用或事件驱动架构中,观察者未解绑,使被观察对象无法GC
代码层面的针对性防范
不能只靠调JVM参数,必须从编码习惯入手:
- 防栈溢出:递归逻辑一律加深度计数器与阈值判断;避免在方法内分配>64KB的栈上数组;复杂调用链拆分为异步任务或状态机
-
防堆溢出:集合类优先用
WeakHashMap或ConcurrentHashMap配合computeIfAbsent控制生命周期;大数据查询强制分页+流式处理(Stream.iterate或JDBCsetFetchSize);所有AutoCloseable资源用try-with-resources包裹 -
统一兜底:上线前必加
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/dump/;核心服务启动时设置合理栈大小(如-Xss512k兼顾线程数与深度)
如何快速区分当前问题是栈还是堆
看异常类型和堆栈特征:
- 抛
java.lang.StackOverflowError且堆栈里出现明显重复方法名(如parseNode → parseNode → parseNode...)→ 栈问题 - 抛
java.lang.OutOfMemoryError: Java heap space且日志紧随GC overhead limit exceeded或长时间Full GC → 堆问题 - 同一服务部分实例报栈溢出、部分报堆溢出 → 很可能线程池复用导致栈帧残留,需检查ThreadLocal清理









