JVM内存区域中堆和方法区非线程私有,虚拟机栈、本地方法栈、程序计数器为线程私有;OOMError按区域分四类:堆空间、元空间、本地线程创建失败、直接内存超限;StringTable随JDK版本迁移影响intern行为与GC;ThreadLocal泄漏主因是Entry的key弱引用而value强引用,需显式remove。

Java堆和方法区不是线程私有的,而虚拟机栈、本地方法栈、程序计数器是线程私有的——这是JVM内存区域划分最核心的区分依据,直接决定GC行为、线程安全边界和OOM定位方向。
哪些区域会抛出 OutOfMemoryError?
不同内存区域触发 OutOfMemoryError 的条件和参数开关完全不同:
-
java.lang.OutOfMemoryError: Java heap space:堆内存不足,由-Xmx和-Xms控制,GC后仍无法分配对象时抛出 -
java.lang.OutOfMemoryError: Metaspace:元空间(JDK 8+)耗尽,由-XX:MaxMetaspaceSize限制,动态加载大量类(如热部署、反射生成类)时易触发 -
java.lang.OutOfMemoryError: unable to create new native thread:本地方法栈或操作系统线程资源耗尽,与-Xss(单线程栈大小)和系统级线程数上限共同相关 -
java.lang.OutOfMemoryError: Direct buffer memory:堆外内存(ByteBuffer.allocateDirect())超限,受-XX:MaxDirectMemorySize约束,默认值等于-Xmx
StringTable 在 JDK 7/8/9 中的位置变化
字符串常量池(StringTable)不是独立内存区域,而是方法区(JDK 7前在永久代)、堆(JDK 7)、元空间(JDK 8+)中的一个哈希表结构。这个迁移直接影响 intern() 行为和GC策略:
- JDK 6:常量池在永久代,
intern()可能引发永久代OOM,且不会回收已入池的字符串 - JDK 7:常量池移到堆中,
intern()对象可被GC,但大量重复字符串仍会快速占满老年代 - JDK 8+:常量池随方法区一起进入元空间,不再受堆GC影响,但元空间本身需监控
java.lang.ClassLoader泄漏
String s = new String("hello").intern();
// JDK 7+ 中,若"hello"未在堆中存在,intern() 会将堆中字符串引用放入StringTable
// JDK 6 中,intern() 会复制字符串到永久代,造成双倍内存占用
为什么 ThreadLocal 内存泄漏总和「弱引用」扯上关系?
根本原因在于 ThreadLocalMap 的 Entry 继承自 WeakReference,但 value 是强引用:
立即学习“Java免费学习笔记(深入)”;
- 当
ThreadLocal变量被置为null,key 因弱引用可被GC,但value仍留在ThreadLocalMap中,直到线程结束或主动调用remove() - 线程池场景下线程长期存活,
value泄漏会持续累积,最终导致堆OOM - 正确写法必须显式
threadLocal.set(null)或threadLocal.remove(),不能只靠置空引用
static ThreadLocallocal = new ThreadLocal<>(); local.set(new byte[1024 * 1024]); // 1MB local.remove(); // 必须调用,否则value无法释放
真正容易被忽略的是:元空间的类卸载依赖完整的类加载器链不可达,而不仅仅是类对象无引用;还有 ThreadLocal 的泄漏往往藏在 finally 块缺失或异常吞没的角落,排查时得盯住 ThreadLocalMap 的实际 size 而非表象引用。










