公司动态
一个字母搞崩团队?Long换long,GC从地狱到天堂
引言几个月之前, 团队里头的一名年轻同事匆匆忙忙地找到我, 说道: “哥, 咱们的订单服务突然间 GC 变得频繁起来, CPU 快要被打满了, 你赶紧帮忙瞧瞧是怎么一回事。” 于是我把 JFR 打开查看了一番, 一大堆 Long 对象在新生代呈现出朝生暮死的状况, 每分钟就能产生上亿个。之后定位到了代码, 发觉是一段 Long 来进行循环累加的日志统计的逻辑性内容。尝试将Long替换为long, 十分钟过后, 服务回归正常状态, 他的脸上呈现出不可思议的神情, 疑惑道: “仅仅相差一个字母, 为何差别会如此之大呢? ”。竟然真的就这般大。我觉着好多中高级开发者都自认为对基本类型十分清楚, 但一旦真正谈到字节码、缓存边界、内存布局这方面, 能够讲明白的人并不是很多。今天, 我打算凭借我所踩过的坑, 跟你将“基本类型”这件事情彻彻底底地给聊透彻。基本类型究竟是什么于Java范围里, 数据能够被放置进两类“容器”内, 其一为基本类型, 其二是引用类型。我惯于运用这般一个类比, 基本类型仿若你兜里揣着的钱包, 价值也就是数据径直装于其中, 引用类型恰似你家大门外的快递柜, 你手上握持的仅仅是取件码即为引用, 实际的货物乃属对象置于堆上的柜子里。Java 的基本类型共有 8 种可归为四类它们被直接存储于栈帧的局部变量表当中, 或者是对象像数组这样的在堆上的内嵌部分, 不会产生额外的对象头开销。在这一点上, 这是它同包装类最为本质的区别之处。经验之谈, 好多书讲, 占1 bit, 然而在JVM字节码层面, 常常当作int来处理, 数组里的, 有可能借助byte数组去实现, 你没必要死记硬背, 只要晓得, 它的内存占用以及访问开销, 远比对象包装类要小得多。代码实战装箱、拆箱与那些令人意外的行为把理论讲完之后呢, 咱们来瞅一看一段能够马上直接拿来运行的代码。这段代码将自动装箱、拆箱, 缓存陷阱以及性能差异集中一块儿展示出来了。public class PrimitiveDeepDive { public static void main(String[] args) { // 1. 基本类型比较纯值的比较 int a 100; int b 100; System.out.println(int 100 int 100: (a b)); // true理所当然 // 2. 自动装箱 Integer 缓存-128 ~ 127 Integer c 100; // 背后调用 Integer.valueOf(100)命中缓存 Integer d 100; // 因为缓存c 和 d 指向同一个 Integer 对象 System.out.println(Integer 100 Integer 100: (c d)); // true // 3. 超出缓存范围new 出新对象 Integer e 200; // 200 127valueOf 内部 new Integer(200) Integer f 200; System.out.println(Integer 200 Integer 200: (e f)); // false 很多bug的源头 // 4. 拆箱比较安全回归值的比较 int g e; // 自动拆箱相当于 e.intValue() System.out.println(int 200 Integer 200: (g f)); // true拆箱后是比较值 // 5. 性能对比基本类型 vs 装箱类型 final int LOOP 100_000_000; // 5.1 使用装箱类型 Long long start System.currentTimeMillis(); Long boxedSum 0L; // 装箱的 Long for (int i 0; i LOOP; i) { boxedSum i; // 每循环一次拆箱 → 加法 → 装箱产生一个 Long 对象 } long boxedTime System.currentTimeMillis() - start; System.out.println(装箱 Long 累加耗时: boxedTime ms); // 5.2 使用基本类型 long start System.currentTimeMillis(); long primitiveSum 0L; // 基本类型 long for (int i 0; i LOOP; i) { primitiveSum i; // 直接在栈上或寄存器内操作无对象生成 } long primitiveTime System.currentTimeMillis() - start; System.out.println(基本 long 累加耗时: primitiveTime ms); System.out.println(性能差距倍数: 约 (boxedTime / Math.max(primitiveTime, 1)) 倍); } }要是你去运行这段代码, 极有可能会瞧见数十倍乃至上百倍的性能方面的差异, 与此同时还会伴随因 Long sum 循环区间而产生的 GC 压力, 而这情形, 便是在我于引子里所讲述的那次线上事故的一个微缩版本了。技术补充 缓存上界可通过不建议随意去修改, 把 -Djava.lang...high的值调成xxx调大, 缓存维持着性能和内存之间的平衡, 要是盲目将其调大, 那么可能会致使启动变慢, 还会占用更多堆内存。优势与局限性客观地看待基本类型优势局限性一种观念是, 基本类型的弱点有着设计取舍的缘由, 并非是缺陷, 在“一切皆对象”的Java生态里, 它发挥着性能基石这类独特的作用, 有项目尝试借助值类型使两者统一, 然而在此之前, 理解并且善于运用基本类型, 是工程师的一门必须课程。总结与思考返回实战状态, 我去讲几条自身始终遵循的原则, 说不定是你能够拿走的最具价值的事物:能裸奔就别披着包装的外衣对于方法内的局部变量, 以及涉及循环累加、简单运算的情况, 无脑地选用基本类型去处理。并以此来促使 JIT 和寄存器 地发挥其实力。然而在 DTO 当中, 要接纳包装类, 不过对于 null 的语义, 得慎重考虑清楚。比如说数据库映射的那个金额字段, 你想要去区分“0元”和“未填写”这种情况, 或者Long包装类算较为合理性的选择, 不然的话, 就老老实实地初始化为合理的默认值, 调用()而不是new。现代Java里, 自动装箱所使用的是.( ), 它能够提供缓存。要尽最大可能去避免显式new(10)这种情况, 因为几乎在任何时候它都是没有必要的。在进行比较的时候, 要拆箱或者使用。要永远进行比较, 比较引用时除了基本类型本身, 间接比较使用.() , 或者先拆箱成为int。要是你使用之类的代码检查工具, 这类陷阱会被优雅地拦下。必须警惕无意识的情况, 即装箱函数返回值类型写成Long, 但实际返回的是long, 也就是JDK自动装箱的情况在集合类进行大量Math运算时, 例如在List里反复get然后再累加在日志参数拼接时乱使用Long.() , 直接把Long变量传进可变参数, 会装箱吗? 会哟。性能暗坑存在于这些地方, 知晓JMH有助于探究, async -作用也不能小觑, 它对你来说能成为好朋友啊。最后, 我要讲, 基本类型乃是Java赠予开发者用于放置礼物, 其礼物为性能的“性能大礼包”。它们静静处于这语言的底层位置上了, 然而却是撑起了基本上所有达成高性能的Java应用的骨架结构。请花上一些时间着力去理解它, 并非是为了炫耀技艺, 而是在于当你的系统承受不住之时, 你能够知晓该从哪里拧下那颗起着关键作用的螺丝。