为什么 Java 的自动装箱和拆箱会影响性能?
Java自动装箱拆箱由编译器插入Integer.valueOf和intValue调用,超出缓存会创建对象,在循环和集合中增加分配、方法调用和GC开销,显著拖慢性能,应改用原始类型避免频繁装箱。
自动装箱拆箱影响性能的根本原因,是编译器在你写的每一处 Integer i = 1 和 i + 1 上,偷偷插入了 Integer.valueOf() 和 intValue() 调用:前者在 -128~127 之外每次都要在堆上 new 一个对象,后者是额外的方法调用。在热点循环或大集合里,这意味着成百万次的对象分配和 GC 压力,实测吞吐量常常掉到原始类型版本的 1/3 到 1/10。
自动装箱和拆箱在字节码层面做了什么?
结论:装箱就是调用 Integer.valueOf(int),拆箱就是调用 intValue(),这两个动作全部由 javac 在编译期插入,源码里看不见。
执行 javac Test.java 后跑 javap -c Test,你会看到 Integer x = 10; 编译成 invokestatic Integer.valueOf:(I)Ljava/lang/Integer;,而 int y = x; 编译成 invokevirtual Integer.intValue:()I。拆箱还有一个隐性风险:如果 x 是 null,拆箱会直接抛 NullPointerException,而且异常栈指向的是那行看似无害的赋值语句。最典型的是三元表达式 Integer a = null; int b = flag ? a : 0;,只要走到 a 那一支就 NPE。
为什么 Integer 比 int 慢这么多?
结论:一个 Integer 对象在 64 位 HotSpot 默认开启压缩指针(-XX:+UseCompressedOops)时占 16 字节(12 字节对象头 + 4 字节 int 字段),而 int 只占 4 字节,内存开销差 4 倍;更贵的是分配和回收本身。
缓存机制只能救一小段范围:Integer.valueOf() 对 -128 到 127 之间的值返回缓存里的同一个对象(Byte、Short、Long、Character 同样是 -128~127,Character 是 0~127,Boolean 缓存两个常量),超出范围就老老实实 new。缓存上界可以用启动参数放大:-XX:AutoBoxCacheMax=10000(等价于 -Djava.lang.Integer.IntegerCache.high=10000),但它只对 Integer 生效,Long 扩不了。
放到集合里看更直观:ArrayList<Integer> 存 100 万个数,需要 4MB 引用数组加 100 万个 16 字节的对象,合计 20MB 左右;换 int[] 只要 4MB。这多出来的 16MB 全是年轻代垃圾,会直接把 Young GC 频率推上去。
哪些写法最容易踩性能坑?
结论:循环累加、集合批量存取、Stream 中间操作,是自动装箱消耗最集中的三个场景。
第一个是累加器写成包装类型:Integer sum = 0; for (int i = 0; i < n; i++) sum += i; 每次迭代都会走「拆箱 → 相加 → 重新装箱」,产生 n 个短命 Integer 对象,改成 int sum = 0 后这条分配链完全消失。
第二个是 List<Integer>、Map<Integer, Integer> 做数值统计,每次 add/put/get 都涉及装箱或拆箱;能换 int[]、long[] 就换,需要 Map 语义时优先考虑用基本类型特化库(如 Eclipse Collections、fastutil)。
第三个是 Stream:list.stream().map(...).reduce(0, Integer::sum) 一路都在包装类型上走,换成 list.stream().mapToInt(...).sum() 走 IntStream 特化管道,中间不再产生装箱对象。
能不能指望 JIT 把装箱优化掉?
结论:不能。HotSpot 的逃逸分析确实能标量替换掉一部分不逃逸的装箱对象,但只要对象进了集合、被返回、被别的线程看到,逃逸分析就失效,分配照旧。
所以正确姿势是让装箱根本不出现,而不是赌 JIT。判断方法很直接:用 JMH 而不是 System.currentTimeMillis() 做基准,运行参数加上 -prof gc,看分配速率(GC 压力)那一栏,Integer 版本每秒分配几百 MB 是常态,int 版本是 0。
== 比较包装类型为什么结果不稳定?
结论:== 比的是引用,只有在 -128~127 缓存范围内才「看起来」等于值比较,超出范围就变 false。
Integer a = 1000, b = 1000; a == b 返回 false,因为两个都是 new 出来的不同对象;换成 100 就返回 true,因为指向缓存里的同一个对象。JLS 5.1.7 只保证 boolean、byte、\u0000~\u007f 的 char、-128~127 的 int/short 装箱后相等,long 不在规范保证范围内(HotSpot 实现里缓存了,但别依赖)。比较包装类型一律用 equals(),或者先拆成 int 再比。
收束一下:装箱拆箱不是语法糖无关痛痒的细节,它把「对象分配」塞进了本该是纯数值计算的路径;把热点路径上的包装类型换成原始类型、循环里避免 Integer 累加、集合和 Stream 优先选特化类型,这三步能消掉绝大部分开销。
转载请注明出处,版权归原作者所有。