Java中局部变量和方法调用帧(含基本类型和对象引用)存于栈,对象实例、数组及所有new创建的对象存于堆;String字面量在常量池(属堆),new String()在堆新建对象。

Java中哪些变量存在栈上,哪些在堆上
Java的栈内存只存局部变量和方法调用帧,包括基本类型(int、boolean等)和对象引用(String s中的s本身),但不存对象实体;对象实例(如new String("abc")产生的字符串对象)、数组、所有通过new创建的实例,都分配在堆内存中。
常见误解是“String在栈上”,其实String s = "hello"中,s这个引用在栈,而"hello"字面量在运行时常量池(属于堆的一部分,JDK 7+后已移入堆),new String("hello")则明确在堆上新建两个对象(一个在常量池,一个在堆)。
- 方法参数如果是基本类型,值拷贝进栈;如果是引用类型,引用(即地址)拷贝进栈,对象仍在堆
- 静态变量(
static字段)和类元数据(Class对象、方法区信息)不在栈或堆,而是放在元空间(Metaspace,JDK 8+)或永久代(JDK 7及以前) - 局部变量若被内部类或lambda捕获,可能被编译器提升为堆对象(逃逸分析后可能优化回栈,但不可依赖)
逃逸分析如何影响栈上分配
HotSpot JVM在JDK 6u23后默认开启逃逸分析(-XX:+DoEscapeAnalysis),它会判断一个新对象是否“逃逸”出当前方法或线程作用域。如果没逃逸(例如只在方法内使用、未被返回、未被赋给静态字段、未被传入未知方法),JVM可能将该对象直接分配在栈上(标量替换),避免堆分配和GC压力。
但这不是开发者能显式控制的:没有stackalloc关键字,也不能强制指定。你只能通过写法间接影响,比如:
立即学习“Java免费学习笔记(深入)”;
- 避免将局部对象返回(
return new ArrayList()→ 逃逸) - 避免赋值给
static或成员变量 - 避免作为参数传递给可能存储引用的方法(如
someMap.put(key, obj)) - 注意日志框架(如SLF4J)的
logger.debug("msg", obj)可能触发逃逸(取决于实现)
用-XX:+PrintEscapeAnalysis可查看分析结果,但实际是否栈分配还受JIT编译时机、对象大小、同步块等限制——小对象更可能被优化,带锁或复杂字段的对象基本不会。
堆内存分配失败时抛什么异常
堆空间不足时,JVM会先尝试GC;若GC后仍无法满足分配需求,抛出java.lang.OutOfMemoryError: Java heap space。这不是Exception,不能被常规catch捕获(除非用catch (Throwable),但极不推荐)。
要注意区分其他OOM类型:
-
OutOfMemoryError: Metaspace→ 类加载过多,需调-XX:MaxMetaspaceSize -
OutOfMemoryError: GC overhead limit exceeded→ GC花太多时间却回收太少,通常意味着堆太小或有内存泄漏 -
OutOfMemoryError: unable to create new native thread→ 栈空间耗尽(每个线程默认1MB栈),和堆无关
排查时优先看GC日志(加-Xlog:gc*)和堆转储(-XX:+HeapDumpOnOutOfMemoryError),而不是盲目加大-Xmx。
String、Integer等包装类的内存行为特殊在哪
它们的“缓存机制”直接影响堆/栈分配行为,容易误判内存位置:
-
Integer i = 127→ 使用Integer.valueOf(127),命中缓存(-128~127),返回常量池中已有对象,不新建堆对象 -
Integer i = 128→ 缓存未命中,每次调用valueOf都新建堆对象(除非手动缓存) -
String s = "abc"→ 字符串字面量,指向字符串常量池(堆内),重复字面量复用同一对象 -
String s = new String("abc")→ 强制在堆上新建对象,即使常量池已有
这种复用看似节省内存,但也会导致意外的引用相等(==成立),尤其在序列化、深拷贝、多线程共享时容易出错。别依赖缓存做逻辑判断,一律用.equals()。
真正难调试的,是逃逸分析失效叠加缓存复用——比如一个本该栈分配的小对象,因被ConcurrentHashMap缓存而长期驻留堆中,又没明显引用链,GC Roots追踪就容易漏掉。









