内存溢出是当场崩溃,内存泄漏是慢性失血;溢出立即抛OutOfMemoryError并中断线程,泄漏则无报错但老年代内存持续缓慢上升直至最终溢出。
内存溢出是你申请 10MB,JVM 告诉你:“堆只剩 2MB,不给,java.lang.OutOfMemoryError: Java heap space”。它立刻抛异常、中断线程、可能整服务挂掉。而内存泄漏根本不会报错——对象明明没用了,却还被 static Map、未注销的监听器、未关闭的 InputStream 死死拽着,GC 回收不了,内存占用一天天涨,直到某次 Full GC 后仍无法腾出空间,才触发溢出。
OOM 错误日志里一定带明确的 OutOfMemoryError 和具体区域(如 Metaspace、Direct buffer memory),且往往发生在某个操作之后(比如导出大报表、批量上传)。而内存泄漏的典型信号是:jstat -gc 显示老年代(OU)使用率持续缓慢上升,每次 GC 后回收量越来越少;jmap -histo 发现某个类实例数暴涨(比如 com.example.UserCacheEntry 十万+);或者 VisualVM 中堆内存曲线呈阶梯式爬升,中间没有明显回落。
new byte[200 * 1024 * 1024] 直接申请 200MBstatic ConcurrentHashMap 缓存用户会话,但忘了加过期或清理机制别一上来就啃 MAT(Memory Analyzer Tool)堆转储文件。先做轻量级排查:
jconsole 或 VisualVM 连上运行中的进程,打开“Classes”页签,按实例数排序,重点关注你项目里的类(不是 java.* 或 sun.*)jm
ap -dump:format=b,file=heap.hprof ,用 MAT 打开后点 “Leak Suspects Report”,它会自动标出疑似泄漏链,重点看 “Path to GC Roots” 是否包含不该存在的静态引用或线程局部变量常见坑:MAT 默认只显示“强引用”路径,但有时泄漏由 ThreadLocal 引起——得手动切换到 “Weak References” 或 “Soft References” 查看;还有些框架(如某些 RPC 客户端)会把回调注册进全局监听器池,不显式 remove 就永远不释放。
调大 -Xmx 只是把 OOM 推迟,治标不治本。真正管用的是从编码和配置双管齐下:
Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(10, TimeUnit.MINUTES),而不是裸写 static Map
AutoCloseable 的资源(InputStream、Connection、Statement)必须用 try-with-resources,哪怕只是临时读个配置文件
onDestroy()、Spring Bean 的 @PreDestroy 方法里调用 eventBus.unregister(this)
remove()——尤其 Web 容器中线程复用,不 remove 会导致前一个请求的数据污染下一个最易被忽略的一点:第三方 SDK 的内存行为。比如旧版 FastJSON 在反序列化时若开启 Feature.SupportNonPublicField,可能因反射缓存导致元空间泄漏;又比如某些日志框架的 MDC(Mapped Diagnostic Context)在线程池中未清理,会随线程复用越积越多。上线前务必跑一轮压测 + 内存监控,别等凌晨报警才翻文档。