原始类型在栈上分配,包装类和引用类型在堆上分配;前者分配快、无gc开销,后者引发频繁minor gc、内存碎片及代际晋升负担。

原始类型(如 int、bool、double)在栈上分配,而其包装类(如 Integer、Boolean、Double)或引用类型实例则必须在堆上分配——这个根本区别直接决定了性能表现和 GC 压力大小。
栈分配:快、确定、零 GC 干预
原始类型变量(例如 int i = 42;)的值直接写入当前线程栈帧中。栈内存是连续的、编译期可预测的空间,分配仅需移动栈指针(1–2 条 CPU 指令),释放随作用域退出自动完成,无需任何运行时跟踪。
- 生命周期与方法调用严格绑定,无悬挂引用风险
- 访问局部原始变量几乎等同于寄存器读写,缓存友好
- 完全绕过垃圾回收器,不产生任何 GC 负载
堆分配:灵活但代价明确
一旦使用包装类(Integer i = 42;)或装箱(object o = 42;),JVM 或 CLR 就必须在堆上创建一个新对象。这个对象包含元数据、同步块、实际值,并由 GC 跟踪其存活状态。
- 每次分配都触发堆管理器查找空闲块、更新元数据,开销比栈高 10–100 倍
- 对象进入 GC 代龄体系(如 Java 的 Young/Old,.NET 的 Gen 0/1/2),即使短暂存活也要经历标记、搬运、指针更新
- 大量短命包装对象会快速填满年轻代,引发高频 Minor GC,拖慢吞吐量
数组场景放大差异
原始类型数组(int[] arr = {1, 2, 3};)只在堆上分配一块连续内存,存储所有值;而包装类数组(Integer[] arr = {1, 2, 3};)会在堆上创建一个引用数组 + 三个独立的 Integer 对象,共四次堆分配。
- 前者内存紧凑,遍历快,GC 只需扫描一个对象头
- 后者产生碎片、增加 GC 扫描范围,且每个
Integer都要单独判断是否可达 - 在高性能计算、游戏逻辑、实时数据处理中,这种差异会直接反映为毫秒级卡顿
对 GC 的真实影响不是“多一点”,而是“结构性负担”
GC 的工作本质是遍历所有根引用(栈、静态字段、寄存器等),顺藤摸瓜标记可达对象。每多一个堆对象,就多一次潜在的指针追踪、多一次可能的内存搬运、多一次代际晋升判断。
- 堆越大,GC 扫描时间越长(线性增长),大堆(GB 级)下一次 Full GC 可达数秒
- 频繁小对象分配加剧内存碎片,迫使 GC 更频繁压缩堆,进一步抬高延迟
- 装箱/拆箱还引入隐式转换开销,在循环中极易成为热点瓶颈











