ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

JIT黑科技:让Java代码飞起来的三大优化

JIT黑科技:让Java代码飞起来的三大优化 全文目录开篇语前言从“翻译官”到“架构师”的华丽转身一、 JIT 编译从“旁观者”到“改造者”1. 编译的触发什么是“热点代码”2. JIT 编译器的“两兄弟”C1 和 C2 ‍二、 JIT 的“黑魔法”热点代码优化策略 ‍♂️1. 方法内联Method Inlining“化零为整”的艺术2. 逃逸分析Escape Analysis对象的生死劫3. 循环优化Loop Optimizations加速器4. 消除死代码Dead Code Elimination“减肥瘦身”5. 窥孔优化Peephole Optimization局部微调三、 避坑指南给JIT点时间别“一梭子”打死 四、 写在最后 JVM 的“灵魂”所在文末开篇语哈喽各位小伙伴们你们好呀我是喵手。运营社区C站/掘金/腾讯云/阿里云/华为云/51CTO欢迎大家常来逛逛今天我要给大家分享一些自己日常学习到的一些知识点并以文字的形式跟大家一起交流互相学习一个人虽可以走的更快但一群人可以走的更远。我是一名后端开发爱好者工作日常接触到最多的就是Java语言啦所以我都尽量抽业余时间把自己所学到所会的通过文章的形式进行输出希望以这种方式帮助到更多的初学者或者想入门的小伙伴们同时也能对自己的技术进行沉淀加以复盘查缺补漏。小伙伴们在批阅的过程中如果觉得文章不错欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持前言从“翻译官”到“架构师”的华丽转身兄弟们你们知道吗我们写出来的 Java 代码经过javac编译之后会变成一堆字节码.class文件。这字节码JVM 可不是直接拿去运行的它需要一个“翻译官”来把它翻译成操作系统能听懂的机器码。刚开始这个“翻译官”是解释器Interpreter。它就像个同声传译来一句翻译一句执行一句。优点是启动快不需要预编译缺点是慢因为每次都要翻译。但是JVM 里面的“大聪明”们可不满足于此。它们想“有没有一种办法能让那些经常被调用的、非常重要的代码直接变成机器码以后就不用一遍遍翻译了呢” 于是JITJust-In-Time编译器就横空出世了JIT 编译器就像一个非常聪明的“项目架构师”它会默默地观察你的程序运行找出那些最忙碌、最核心的代码我们称之为“热点代码”然后把它们精心优化并编译成高效的机器码直接“缓存”起来。下次再遇到这段代码就不用解释器慢悠悠地翻译了直接飙车️今天咱们就来深扒一下这个 JIT 到底是怎么“变魔术”的以及它都偷偷摸摸地做了哪些优化让我们的 Java 程序跑得飞快一、 JIT 编译从“旁观者”到“改造者”JIT 编译过程可不是一蹴而就的它是一个动态的过程。JVM 会在程序运行期间根据代码的执行频率和热度决定是否以及如何进行编译。这就像一个餐厅老板他不会把所有菜谱都背下来提前编译所有代码而是会观察哪些菜点得最多就把那些菜的制作流程优化到极致热点代码编译。1. 编译的触发什么是“热点代码”JVM 怎么知道哪段代码是热点呢它有两个主要的“侦查手段”*基于采样的热点探测Sampling-based Hot Spot Detection简单粗暴JVM 隔一段时间检查一下哪些方法在被执行。*基于计数器的热点探测Counter-based Hot Spot Detection这个更精准JVM 会为每个方法或循环体维护两个计数器方法调用计数器Method Invocation Counter统计方法被调用的次数。回边计数器Back Edge Counter统计方法内部循环体被执行的次数回边可以理解为循环跳转到起点的指令。当这两个计数器的值达到一定的阈值时JVM 就认为这段代码是“热点代码”了会把它提交给 JIT 编译器进行编译。举个栗子你可以通过java -XX:PrintFlagsFinal -version来查看这些默认阈值。比如CompileThreshold方法调用计数器阈值和OnStackReplaceRatio回边计数器阈值。2. JIT 编译器的“两兄弟”C1 和 C2 ‍在 HotSpot JVM 里JIT 编译其实是由两个编译器协同完成的它们就像一对“兄弟”各有所长*Client Compiler (C1 编译器)速度快优化相对保守。主要进行一些简单的优化比如局部变量的优化、循环展开等。它的目标是快速启动在程序运行初期就能把一部分热点代码编译掉减少解释执行的开销。就像一个“快枪手”瞄准就打不管打得是不是要害。*Server Compiler (C2 编译器)速度慢但优化激进甚至会进行一些预测性优化。它会花更多的时间去分析代码进行更复杂的优化比如逃逸分析、内联、循环优化等。它的目标是追求极致的运行性能。就像一个“狙击手”慢慢瞄准一击毙命。在**分层编译Tiered Compilation**模式下JDK 7 默认开启它们会协同作战解释执行 - C1 编译 - C2 编译程序刚启动时用解释器快速启动。然后部分热点代码会被 C1 编译器快速编译成机器码提升初期性能。再然后那些更热点的代码会被 C2 编译器进行深度优化。这就像练武功先学个三脚猫功夫解释器然后练套拳法C1最后修炼内功心法C2二、 JIT 的“黑魔法”热点代码优化策略 ‍♂️JIT 编译器可不是简单地把字节码翻译成机器码就完事了它还会使出各种“黑魔法”来优化你的代码让它跑得飞快。这些优化手段有时候甚至能达到你手动优化都难以企及的高度。1. 方法内联Method Inlining“化零为整”的艺术这是 JIT 最重要、最基础的优化之一重要性怎么强调都不为过想象一下你有这么一段代码publicclassCalculator{publicintadd(inta,intb){returnab;}publicintcalculateSum(intx,inty){// 假设这里被频繁调用returnadd(x,y)*2;}}每次调用add方法都会涉及方法栈帧的创建、参数传递、返回值处理等等开销。JIT 看到add方法很小而且calculateSum方法频繁调用add它就会偷偷地把它“内联”进去// 经过JIT内联优化后逻辑上可能变成这样publicintcalculateSum(intx,inty){// return add(x, y) * 2;return(xy)*2;// 直接把 add 方法体搬过来了省去了方法调用的开销}内联的意义在于它为后续更多的优化打开了大门没有内联很多跨方法边界的优化根本无从谈起。2. 逃逸分析Escape Analysis对象的生死劫这个优化听起来就很玄乎但它能极大地减少堆内存的压力和 GC 的负担。核心思想JIT 分析一个对象是否“逃逸”出了它被创建的方法或线程。*不逃逸如果一个对象只在方法内部使用方法执行完就没用了那它就是“不逃逸”的。*逃逸如果一个对象被外部方法引用或者被其他线程访问那它就是“逃逸”的。优化手段*栈上分配Stack Allocation如果一个对象不逃逸JIT 可能会把它直接分配到栈上而不是堆上。栈上的对象随着方法执行结束就自动销毁了根本不用 GC 操心效率杠杠的*标量替换Scalar Replacement如果一个对象连引用都没逃逸甚至可以把它分解成基本类型标量来存储。比如Point p new Point(1, 2);如果p不逃逸JIT 可能直接用两个int变量x 1, y 2来代替这个对象。这连对象头都省了直接在寄存器或栈上操作基本类型极致性能代码示例感受逃逸与不逃逸publicclassEscapeAnalysisDemo{publicvoidmethodA(){// 对象局部创建局部使用不逃逸// 极有可能被栈上分配或标量替换MyObjectobjnewMyObject(Local Object);System.out.println(obj.getName());}publicMyObjectmethodB(){// 对象被返回逃逸到方法外部MyObjectobjnewMyObject(Escaped Object);returnobj;}privatestaticclassMyObject{privateStringname;publicMyObject(Stringname){this.namename;}publicStringgetName(){returnname;}}publicstaticvoidmain(String[]args){EscapeAnalysisDemodemonewEscapeAnalysisDemo();for(inti0;i1000000;i){// 循环调用使其成为热点demo.methodA();}// methodB 返回的对象可能会被 GCMyObjectodemo.methodB();System.out.println(o.getName());}}你运行这段代码在methodA里的MyObject在 JIT 优化下可能根本就不会在堆上产生对象大大减少了 GC 压力。3. 循环优化Loop Optimizations加速器JIT 对循环的优化也是重头戏毕竟很多热点代码都在循环里。*循环展开Loop Unrolling把循环体复制多份减少循环次数和条件判断的开销。比如for (int i 0; i 4; i) { doSomething(); }可能会被优化成doSomething(); doSomething(); doSomething(); doSomething();。*循环不变式外提Loop Invariant Code Motion把循环内不变的计算提到循环外面去执行。for (int i 0; i arr.length; i) { int x calculateStaticValue(); doSomething(x); }优化后int x calculateStaticValue(); for (int i 0; i arr.length; i) { doSomething(x); }4. 消除死代码Dead Code Elimination“减肥瘦身”如果 JIT 发现你的代码里有些部分永远不会被执行比如if (false) { ... }或者结果不会被使用它就会毫不留情地把这些代码从编译后的机器码中剔除就像健身教练帮你去掉赘肉一样5. 窥孔优化Peephole Optimization局部微调这是一种非常局部的优化JIT 会检查很小一段连续的机器指令序列看有没有更短、更快的等价指令序列可以替换。就像在显微镜下把细微的瑕疵都修复掉。三、 避坑指南给JIT点时间别“一梭子”打死 了解了 JIT 的神奇之处我们也要知道一些“避坑指南”。JIT 的优化是动态的需要时间“学习”你的程序所以*预热很重要JVM Warm-up你的程序刚启动时性能通常不是最好的因为 JIT 还没来得及把热点代码编译好。所以在做性能测试或基准测试时一定要给 JVM 足够长的预热时间让 JIT 充分发挥作用。*别过度优化有时候我们手动的一些“微优化”反而会干扰 JIT 的判断甚至可能因为引入额外的分支或复杂性导致 JIT 无法进行更高级的优化。相信 JIT它比你想象的更聪明。*关注 GCJIT 优化能减少 GC 频率但如果 GC 成了瓶颈那还得从 GC 本身比如选对 GC 算法着手。JIT 和 GC 是 Java 性能的两大支柱相辅相成。四、 写在最后 JVM 的“灵魂”所在JIT 编译器和热点代码优化可以说是 JVM 能够达到高性能的“灵魂”所在。它让 Java 从一个“解释执行”的慢郎中一跃成为企业级应用的主力军。每次当你看到你的 Java 服务在高峰期依然稳定如狗的时候除了你自己写得好当然也要给默默在背后“肝”优化的 JIT 编译器点个赞 它就像一个不知疲倦的幕后英雄总是在不断学习、不断进化让我们的代码跑得更快、更稳。所以下次再有人说 Java 慢的时候你可以给他科普一下 JIT 的这些“黑魔法”了咱们 Javaer就是这么自信好了今天的 JVM 内部探险就到这里。如果觉得有意思或者学到了新东西别忘了给我个“三连”啊咱们评论区接着唠… …文末好啦以上就是我这期的全部内容如果有任何疑问欢迎下方留言哦咱们下期见。… …学习不分先后知识不分多少事无巨细当以虚心求教三人行必有我师焉wished for you successed ⭐️若喜欢我就请关注我叭。⭐️若对您有用就请点赞叭。⭐️若有疑问就请评论留言告诉我叭。版权声明本文由作者原创转载请注明出处谢谢支持
返回列表