源本科技 | 码上会

Java 栈内存与堆内存

2026/01/23
43
0

学习目标

  • 清晰区分栈内存与堆内存的用途、生命周期和管理方式

  • 理解对象、引用、基本类型在内存中的实际存储位置

  • 掌握字符串常量池等特殊机制对内存布局的影响

  • 能够分析典型代码的内存表示并预测行为(如引用相等性)

  • 了解栈溢出(StackOverflowError)与堆内存不足(OutOfMemoryError)的成因


核心概念

Java 程序运行时,JVM 将内存主要划分为 栈(Stack)堆(Heap) 两大区域,二者分工明确、特性迥异。

特性

栈内存(Stack)

堆内存(Heap)

用途

存储方法调用帧、局部变量、对象引用

存储所有对象实例、数组

线程模型

每个线程私有(天然线程安全)

所有线程共享(需同步控制)

分配 / 释放

方法进入时自动分配,退出时自动释放

通过 new 分配,由垃圾回收器(GC)回收

访问速度

极快(连续内存 + LIFO 结构)

相对较慢(随机分配)

内存大小

较小(默认几百 KB 到几 MB)

较大(可配置,通常数百 MB 至数 GB)

异常风险

StackOverflowError(递归过深)

OutOfMemoryError: Java heap space

数据结构

线性(LIFO 栈)

非线性(分代、分区等复杂结构)


内存分配示例

class Employee {
    int id;        // 实例字段 → 存储在堆中(属于对象的一部分)
    String name;   // 引用字段 → 存储在堆中,指向另一个堆对象
    double salary; // 实例字段 → 存储在堆中
}

public class Main {
    public static void display(Employee emp) { // 参数 emp → 栈(引用)
        emp.display();
    }

    public static void main(String[] args) {
        Employee emp1 = new Employee(101, "Maddy", 50000.0); // emp1 → 栈;对象 → 堆
        Employee emp2 = new Employee(102, "Maddy", 60000.0); // emp2 → 栈;对象 → 堆
        
        display(emp1); // 调用时创建新栈帧,emp 参数指向 emp1 对象
        display(emp2);
    }
}

关键点:

  • emp1emp2栈中的引用变量

  • 两个 Employee 对象及其内部的 idsalary 字段存储在 堆中

  • 字符串字面量 "Maddy" 存储在 堆中的字符串常量池,两个 name 引用指向同一对象

  • 因此 (emp1.name == emp2.name) 返回 true


栈内存

存储内容

  • 局部基本类型变量:如 int a = 10;

  • 方法参数(包括对象引用)

  • 返回地址操作数栈

生命周期

  • 方法调用时压入栈帧

  • 方法返回时自动弹出并释放

public static int add(int a, int b) {
    int res = a + b; // a, b, res 均在当前方法的栈帧中
    return res;
}

所有变量随 add() 方法结束而销毁,无需手动管理。


堆内存

存储内容

  • 所有通过 new 创建的对象

  • 数组(如 int[] arr = new int[10];

  • 字符串常量池(Java 7+ 位于堆中)

内部结构

区域

说明

新生代

新对象分配区,包含 Eden 和两个 Survivor 区(S0/S1)

老年代

长期存活对象晋升至此

Metaspace

类元数据,使用本地内存(Java 8+)

Code Cache

JIT 编译后的本地代码

垃圾回收

  • JVM 自动识别不可达对象并回收

  • 不同 GC 算法(如 G1、ZGC)优化不同场景下的停顿时间与吞吐量


性能与安全

栈的优势

  • 高速访问:连续内存 + CPU 缓存友好

  • 自动清理:无内存泄漏风险

  • 线程隔离:天然避免竞态条件

堆的挑战

  • GC 开销:回收过程可能导致应用暂停

  • 内存碎片:动态分配可能产生不连续空闲块

  • 并发风险:多线程共享需显式同步(如 synchronized


异常场景

异常

成因

解决方案

StackOverflowError

递归过深或无限方法调用

优化递归为迭代,增大栈大小(-Xss

OutOfMemoryError: Java heap space

堆中对象过多或内存泄漏

分析堆转储(heap dump),
优化对象生命周期,增大堆(-Xmx

OutOfMemoryError: Metaspace

动态生成大量类
(如反射、脚本引擎)

增大 Metaspace(-XX:MaxMetaspaceSize),
减少动态类加载


思考题

  1. 如果一个方法返回一个局部 StringBuilder 对象的引用,该对象是否会在方法结束后被回收?为什么?

  2. 在高并发系统中,大量短生命周期对象频繁分配在堆上,可能引发什么性能问题?如何缓解?

  3. 为什么字符串常量池从永久代(PermGen)移到堆内存(Java 7+)?这对字符串去重和内存管理有何影响?