Java中对象存堆、变量和调用帧存栈;堆线程共享需防并发问题,栈线程私有天然安全;堆溢出因内存泄漏,栈溢出多因深度递归;字符串字面量入常量池(堆内),new创建在堆;栈快于堆但性能瓶颈常在GC与引用关系。

堆存对象,栈存变量和调用帧
Java里所有 new 出来的对象(包括数组、String 实例、自定义类实例)都落在堆中;而方法内的局部变量(如 int i = 10、String s)、方法参数、返回地址这些,全在栈里。注意:s 是引用变量,它自己在栈上,但它指向的字符串对象可能在堆(new String("abc"))或字符串常量池("abc",JDK7+ 后常量池也挪到堆里了)。
- 堆是线程共享的——多个线程能同时读写同一个对象,所以要小心并发修改引发的
ConcurrentModificationException或数据不一致 - 栈是线程私有的——每个线程有自己的一套栈帧,互不干扰,天然线程安全
- 栈帧随方法调用自动压入、方法结束自动弹出,不用 GC;堆里的对象得靠垃圾回收器判断是否“不可达”后才清理
堆溢出 vs 栈溢出:错误现象和排查方向完全不同
OutOfMemoryError: Java heap space 和 StackOverflowError 看似都是“内存不够”,但成因和解法毫无交集。
- 堆溢出常见于:缓存没设上限(比如
Map一直put不清理)、大文件流未关闭导致对象堆积、监听器/回调未反注册造成内存泄漏 - 栈溢出几乎只发生在:深度递归(比如没写好终止条件的树遍历)、超长方法链(A→B→C→…→Z,上百层)、或单个方法定义了巨量局部变量(如声明了几十个大数组)
- 调参区别:
-Xmx和-Xms控堆大小,-Xss控单个线程栈大小;增大-Xss可能缓解栈溢出,但会减少可创建的线程数
字符串创建方式决定它在堆还是常量池
字符串是唯一一个“写法不同、内存位置可能不同”的典型:
String a = "hello"; // 字符串字面量 → 先查字符串常量池(堆内),命中则复用,否则新建并入池
String b = new String("hello"); // 强制在堆中新建对象,即使池里已有"hello"
System.out.println(a == b); // false(引用不同)
System.out.println(a.equals(b)); // true(内容相同)
- JDK7 起,字符串常量池从方法区移到堆中,所以现在“池”也是堆的一部分,只是有特殊管理逻辑
-
intern()方法能把堆中字符串手动加入常量池,返回池中引用,可用于节省内存或做快速判等 - 不要误以为
"abc"就一定比new String("abc")更省内存——如果只是临时用一次,后者反而避免污染常量池
别把“栈快堆慢”当教条,关键看访问模式
栈确实比堆快,因为它是连续内存 + LIFO + 无GC开销;但实际性能瓶颈往往不在这里。
立即学习“Java免费学习笔记(深入)”;
- 频繁创建小对象(如循环里
new ArrayList())看似“堆操作”,但现代 JVM(尤其 G1/ZGC)对新生代分配极快,真正拖慢的是后续 GC 压力或缓存行失效 - 把本该在堆的对象硬塞进栈(比如用
ThreadLocal存大对象)会导致栈空间暴涨,容易触发StackOverflowError - 真正影响响应时间的,往往是对象间的引用关系深度(GC Roots 扫描成本)、是否跨代引用(记忆集开销)、以及是否产生内存碎片(老年代分配失败)
堆和栈的边界在代码里是清晰的,但它们的交互(比如栈上引用指向堆对象)才是多数内存问题真正的藏身之处——漏掉一个引用,对象就永远无法被回收。










