
栈内存的分配由编译器在编译期完成,遵循后进先出(LIFO)原则。每个线程创建时,JVM会为其分配一块独立的栈空间(虚拟机栈),栈的大小可通过-Xss参数设置(默认1MB左右)。
栈的基本单元是栈帧(Stack Frame),每个方法调用时会创建一个栈帧并压入栈中,方法执行完毕后栈帧出栈。栈帧内部包含:
栈分配的特点:
StackOverflowError(如递归过深)堆是JVM中最大的内存区域,用于存储对象实例和数组。堆的分配发生在运行时,由垃圾收集器(GC)负责管理,其大小可通过-Xms(初始堆大小)和-Xmx(最大堆大小)参数调整。
JDK 1.8后,堆的结构分为:
堆分配的特点:
OutOfMemoryError
栈的局部变量表通过索引访问,操作数栈通过压栈/出栈指令操作,所有数据访问都是直接寻址,速度极快。例如:
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.StringUtils;
/**
* 栈操作示例
* @author ken
*/
@Slf4j
publicclass StackOperationDemo {
/**
* 计算两数之和(栈帧演示)
* @param a 第一个整数
* @param b 第二个整数
* @return 两数之和
*/
public static int add(int a, int b) {
int result = a + b; // result存储在局部变量表中
return result; // 将result压入操作数栈返回
}
public static void main(String[] args) {
int x = 10; // 局部变量x入栈
int y = 20; // 局部变量y入栈
int sum = add(x, y); // 方法调用创建栈帧
log.info("sum: {}", sum);
String str = "hello"; // 字符串引用入栈,对象本身在堆中
if (StringUtils.hasText(str)) {
log.info("str: {}", str);
}
}
}
栈操作的关键特性:
堆中的对象通过栈中的引用访问(指针间接寻址),对象的生命周期不受方法限制,需通过GC判断是否回收。例如:
package com.jam.demo;
import com.alibaba.fastjson2.JSON;
import com.google.common.collect.Lists;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.CollectionUtils;
import java.util.List;
/**
* 堆操作示例
* @author ken
*/
@Slf4j
@Data
@AllArgsConstructor
class User {
private Long id;
private String name;
private Integer age;
}
@Slf4j
publicclass HeapOperationDemo {
/**
* 创建用户列表(堆内存分配)
* @return 用户列表
*/
public static List<User> createUserList() {
List<User> userList = Lists.newArrayList(); // 集合对象在堆中
userList.add(new User(1L, "Alice", 25)); // User对象在堆中
userList.add(new User(2L, "Bob", 30));
userList.add(new User(3L, "Charlie", 35));
return userList; // 返回堆中对象的引用
}
public static void main(String[] args) {
List<User> users = createUserList();
if (!CollectionUtils.isEmpty(users)) {
log.info("users: {}", JSON.toJSONString(users));
}
// 手动置null,帮助GC回收
users = null;
System.gc(); // 建议GC执行(不保证立即执行)
}
}
堆操作的关键特性:
特性 | 栈 | 堆 |
|---|---|---|
访问方式 | 直接寻址(索引) | 间接寻址(引用) |
访问速度 | 极快(纳秒级) | 较慢(微秒级) |
生命周期 | 方法级(自动销毁) | 对象级(GC回收) |
扩容能力 | 不支持(固定大小) | 支持(动态扩展) |
线程安全 | 线程私有(安全) | 线程共享(不安全) |
JVM规范要求虚拟机栈必须是线程私有的,且采用栈式结构。以HotSpot虚拟机为例,栈的实现基于C++的栈结构,每个Java线程对应一个JavaThread对象,其内部包含_osthread(操作系统线程)和_stack(虚拟机栈)。
栈帧的局部变量表大小在编译期确定(存储在class文件的Code属性中),例如:
// 反编译add方法的字节码
public static int add(int, int);
descriptor: (II)I
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=3, args_size=2
0: iload_0
1: iload_1
2: iadd
3: istore_2
4: iload_2
5: ireturn
其中locals=3表示局部变量表大小为3(参数a、b和局部变量result)。
栈溢出示例:
package com.jam.demo;
import lombok.extern.slf4j.Slf4j;
/**
* 栈溢出示例
* @author ken
*/
@Slf4j
publicclass StackOverflowDemo {
privatestaticint count = 0;
/**
* 无限递归导致栈溢出
*/
public static void recursiveCall() {
count++;
recursiveCall(); // 不断创建栈帧,超出栈容量
}
public static void main(String[] args) {
try {
recursiveCall();
} catch (StackOverflowError e) {
log.error("栈溢出,递归次数:{}", count, e);
}
}
}
运行结果:
ERROR com.jam.demo.StackOverflowDemo - 栈溢出,递归次数:11419
java.lang.StackOverflowError: null
at com.jam.demo.StackOverflowDemo.recursiveCall(StackOverflowDemo.java:15)
at com.jam.demo.StackOverflowDemo.recursiveCall(StackOverflowDemo.java:15)
...
HotSpot虚拟机的堆基于分代收集理论设计,使用空闲列表或位图管理内存分配。以Eden区为例,对象分配采用指针碰撞(Bump-the-Pointer)方式:
堆溢出示例:
package com.jam.demo;
import com.google.common.collect.Maps;
import lombok.extern.slf4j.Slf4j;
import java.util.Map;
/**
* 堆溢出示例(JVM参数:-Xms20m -Xmx20m)
* @author ken
*/
@Slf4j
publicclass HeapOverflowDemo {
public static void main(String[] args) {
Map<String, byte[]> map = Maps.newHashMap();
int i = 0;
try {
while (true) {
map.put("key" + i, newbyte[1024 * 1024]); // 每次分配1MB数组
i++;
}
} catch (OutOfMemoryError e) {
log.error("堆溢出,分配次数:{}", i, e);
}
}
}
运行结果:
ERROR com.jam.demo.HeapOverflowDemo - 堆溢出,分配次数:18
java.lang.OutOfMemoryError: Java heap space
at com.jam.demo.HeapOverflowDemo.main(HeapOverflowDemo.java:17)


以UserService调用UserMapper为例:
package com.jam.demo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.extension.service.IService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
/**
* 实战示例:堆与栈协同工作
* @author ken
*/
@Data
@TableName("user")
class UserPO {
@TableId(type = IdType.AUTO)
private Long id;
private String name;
private Integer age;
}
interface UserMapper extends BaseMapper<UserPO> {}
@Service
@Slf4j
@Tag(name = "用户服务")
class UserServiceImpl extends ServiceImpl<UserMapper, UserPO> implements IService<UserPO> {
@Operation(summary = "创建用户")
public boolean createUser(String name, Integer age) {
if (!StringUtils.hasText(name)) {
thrownew IllegalArgumentException("用户名不能为空");
}
if (ObjectUtils.isEmpty(age) || age < 0) {
thrownew IllegalArgumentException("年龄必须为正数");
}
UserPO user = new UserPO(); // 堆中创建UserPO对象
user.setName(name); // 栈中name引用指向堆中字符串对象
user.setAge(age); // 栈中age值传递给堆对象
return save(user); // 栈中user引用传递给save方法
}
}
@Slf4j
publicclass HeapStackCollaborationDemo {
public static void main(String[] args) {
UserServiceImpl userService = new UserServiceImpl(); // 堆中创建服务对象
try {
boolean result = userService.createUser("Alice", 25); // 方法调用创建栈帧
log.info("用户创建结果:{}", result);
} catch (IllegalArgumentException e) {
log.error("参数错误", e);
}
}
}
内存变化分析:
main方法栈帧创建,userService引用入栈,指向堆中的UserServiceImpl对象createUser方法,创建新栈帧,参数name和age入栈UserPO对象,堆中分配内存,栈中user引用指向该对象save方法,传递user引用,创建新栈帧JVM的逃逸分析技术可将未逃逸出方法的对象分配在栈上,避免GC开销。例如:
package com.jam.demo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;
/**
* 栈上分配示例(JVM参数:-XX:+DoEscapeAnalysis -XX:+EliminateAllocations)
* @author ken
*/
@Slf4j
@Data
@AllArgsConstructor
class Point {
privateint x;
privateint y;
public int getSum() {
return x + y;
}
}
publicclass StackAllocationDemo {
/**
* 计算点坐标和(对象未逃逸出方法)
* @return 坐标和
*/
public static int calculateSum() {
Point point = new Point(10, 20); // 对象未逃逸,分配在栈上
return point.getSum();
}
public static void main(String[] args) {
long start = System.currentTimeMillis();
for (int i = 0; i < 10000000; i++) {
calculateSum(); // 循环调用,栈上分配避免GC
}
long end = System.currentTimeMillis();
log.info("执行时间:{}ms", end - start);
}
}
逃逸分析效果:
正确理解:基本类型的局部变量存储在栈中,成员变量存储在堆中(随对象);常量池中的字符串常量存储在堆的元空间。
正确理解:栈的分配效率高,但栈上分配受大小限制;堆的分配效率低,但支持大数据量存储。
正确理解:栈大小固定,递归过深或方法嵌套过多会导致StackOverflowError。
A:局部变量存储在栈中,直接寻址;成员变量存储在堆中,需通过对象引用间接访问,且可能存在线程安全开销。
A:不一定。通过逃逸分析,未逃逸的对象可分配在栈上;标量替换技术可将对象拆解为基本类型存储在栈中。
A:栈是线程私有的,不存在线程安全问题;堆是线程共享的,多线程访问需同步(如synchronized、volatile)。
A:栈溢出可通过-Xss增大栈空间,或优化递归/方法嵌套;堆溢出可通过-Xmx增大堆空间,或排查内存泄漏(使用MAT工具)。
堆与栈是Java内存模型的两大核心组件,其本质差异源于内存分配机制的不同:
理解堆与栈的底层原理,不仅能帮助你写出更高效的代码,更能在排查内存问题、性能优化时直击要害。在实际开发中,合理利用栈的高效性和堆的灵活性,结合JVM的优化技术(如逃逸分析),才能真正发挥Java的性能潜力。