java原生集合装箱开销极大:integer对象比int多占12字节以上并引发gc压力,实测插入1000万int时比fastutil的intarraylist慢2–3倍、内存多3–4倍;高频数值计算必须用primitive集合库,其中fastutil最成熟,支持完整接口继承且兼容null key,而trove和apache commons primitives存在迁移成本高或无真正集合实现等问题;其open系列采用连续数组布局提升缓存友好性,避免对象分散导致的cpu cache miss。

Java原生集合装箱开销到底多大
原生 ArrayList<integer></integer> 或 HashMap<integer string></integer> 每次存取都强制装箱/拆箱,底层实际是 Integer 对象而非 int。一个 int 占 4 字节,而 Integer 对象在 HotSpot 上至少占 16 字节(对象头 + 字段 + 对齐),还附带 GC 压力。
实测:向 ArrayList<integer></integer> 插入 1000 万个 int,比 IntArrayList(FastUtil)慢 2–3 倍,内存多占 3–4 倍。这不是理论值,是压测里能直接看到的 GC pause 和 heap dump 差异。
- 高频数值计算(如实时风控特征聚合、时间序列采样)必须避开装箱
- 小对象集合(
short、byte、boolean)装箱浪费更夸张,Boolean对象本身比值还大 - Java 14+ 的
sealed和未来值类型(Valhalla)还没落地,现在没得选
FastUtil 和其他 primitive 库怎么选
不是所有“支持基本类型”的库都靠谱。FastUtil 是目前最成熟的选择,但要注意它和 Trove、Apache Commons Primitives 的关键差异:
-
FastUtil提供完整接口继承体系(如IntList继承List),能无缝替换部分原生集合用法;Trove的TIntArrayList完全不实现List,迁移成本高 -
FastUtil的Object2IntOpenHashMap支持 primitive value + object key,而GNU Trove的TObjectIntHashMap不支持 null key —— 这点在缓存场景容易踩坑 -
Apache Commons Primitives只提供数组工具类(如ArrayUtils),没有真正意义上的集合实现,别误当集合库用
简单说:要替代 HashMap<integer string></integer>,用 Int2ObjectOpenHashMap;要替代 Set<double></double>,用 DoubleOpenHashSet;别去凑合改 Arrays.sort()。
立即学习“Java免费学习笔记(深入)”;
编译期类型擦除导致的泛型陷阱
Java 泛型擦除后,ArrayList<integer></integer> 和 ArrayList<string></string> 运行时都是 ArrayList,但 primitive 集合是真·不同类:IntArrayList 和 LongArrayList 是完全独立的类,不能靠泛型抽象统一处理。
- 写工具方法时别试图用
<t> void process(PrimitiveCollection<t> c)</t></t>—— 这种泛型根本不存在 - 反射调用
size()或get(int)没问题,但别指望getClass().getGenericSuperclass()能帮你推导元素类型 - 序列化需注意:
IntArrayList默认序列化协议和ArrayList不兼容,跨服务传输前务必确认序列化器支持(比如 Kryo 需注册,Jackson 需加@JsonDeserialize)
性能提升不是白来的:内存布局与缓存友好性
FastUtil 的 OpenXXX 系列(如 IntOpenHashSet)用开放寻址法,数据连续存储在单个 int[] 数组里;而原生 HashSet 是数组 + 链表/红黑树,节点分散在堆上。这对 CPU 缓存非常关键。
- 遍历 100 万个
int:连续数组一次 cache line(64 字节)可读 16 个int;对象散列则可能每取一个值就 miss 一次 cache - JVM 无法对 primitive 数组做逃逸分析优化(因为没对象头),但反而因此避免了锁膨胀和同步开销
- 注意扩容阈值:
IntOpenHashSet默认负载因子 0.75,但初始容量设太小会导致频繁 rehash —— 建议预估大小后用new IntOpenHashSet(expected)
真正卡顿的从来不是算法复杂度,而是你没意识到 JVM 正在为每个 int 分配对象、触发 Young GC、把数据打散在内存各处。










