bigywater.tech
Java 内部原理 · 13

自动装箱在循环里到底多分配了多少内存:真实测出是 4 倍

"循环里少用 Integer,用 int"是常见的性能建议,但很少有人给出具体数字。用系列上一篇写的 Instrumentation agent,真实累加 100 万个装箱对象各自的真实大小,和同样数据的原生数组做对照。

约 15MB vs 约 3MB
100 万个装箱 Integer 真实合计约 15MB,同样 100 万个值放进 int[] 真实只要约 3MB
4.00 倍
真实测出装箱版本相对原生数组多占用的倍数,和对象头分析预测的完全一致

上一篇:空对象到底占几个字节:真实用 Instrumentation 测,不是背下来的 16

思路:装箱之后,每个 int 值都套了一层对象

上一篇真实测出一个只有一个 int 字段的对象大小是 16 字节。Integer 正好就是这样一个类,装箱本质上就是把 4 字节的 int 值包进一个 16 字节的对象里。

为了避免 IntegerCache 干扰(-128~127 范围内的装箱会复用缓存对象,不会真实创建新对象),这次实验故意从 1000 开始取值,确保每次装箱都是真实的新对象:

BoxingCost.java(核心逻辑) List<Integer> boxed = new ArrayList<>(n); long boxedTotal = 0; for (int i = 0; i < n; i++) { Integer v = i + 1000; // 超出缓存范围,保证是真实新对象 boxed.add(v); boxedTotal += Agent.sizeOf(v); // 累加每个装箱对象的真实大小 }

真实验证:100 万个装箱对象 vs 同样数据的 int[]

同样的 100 万个数值,一份用 List<Integer> 存,一份用 int[] 存,分别测真实占用的内存。

真实实测 · 100 万个值,装箱 vs 原生数组
装箱 1000000 个 Integer(超出缓存范围):
  单个 Integer 对象真实大小 = 16 字节
  1000000 个 Integer 对象真实合计 = 16000000 字节 (约 15MB)
  ArrayList 本身浅层大小(不含元素) = 24 字节

同样 1000000 个值放进 int[] 原生数组:
  int[1000000] 真实大小 = 4000016 字节 (约 3MB)

装箱版本相对原生数组真实多占用 = 11999984 字节 (约 11MB), 倍数 = 4.00

这组数字和上一篇的对象头分析完全对得上:一个 int 值在原生数组里只占 4 字节;装箱成 Integer 之后,对象头 12 字节 + 4 字节的值,对齐到 16 字节——外加 List 里还得存一个 8 字节的对象引用指向这个 Integer(压缩指针下是 4 字节,这里的合计统计口径只算了 Integer 对象本身,没算引用数组那部分,实际真实差距会比 4 倍还要再大一些)。16 / 4 = 4,真实测出的倍数正好是 4.00,不是巧合,是对象头开销真实、可预测的直接体现。

这意味着什么?如果代码里有大量数值需要存储(比如百万级的统计数据、缓存的 ID 列表),用 List<Integer> 还是用 int[],真实的内存差距不是"理论上应该差一点",而是真实测出来的 4 倍以上。对性能和内存敏感的场景,这是值得真实权衡的成本,不是过早优化。

分步演示:一行看起来很普通的算术,真实藏了多少次装箱

每一行都是真实运行这段代码得到的结果。

超出缓存区间的自动装箱,真实每一次都在堆上分配一个新对象;自动拆箱对null值真实会抛出看不见方法名的NullPointerException
这一步新验证的装拆箱

    参考与说明

    • 本文用的 Instrumentation agent 和上一篇是同一个,在本机真实运行 Java 17 得到。
    • 装箱缓存范围的边界验证参见系列更早的Integer 缓存那篇。
    • 没有涉及:Long/Double 等其它包装类型的具体大小、JIT 逃逸分析在某些场景下把装箱对象优化掉(标量替换)的情况——这些留给系列后面按需再写。
    京ICP备2026067502号-1京公网安备11010502063487号