首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

Java 运行期优化 —— 附示例代码、截图证明

  • 25-03-07 20:01
  • 2853
  • 7817
blog.csdn.net

目录

 

运行期优化

一、即时编译

1.1 逃逸分析

1.2 方法内联

1.3 字段优化

二、反射优化


运行期优化

Java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”(Hot Spot Code)。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器(Just In Time Compiler,下文中简称JIT编译器)。

由于Java虚拟机规范没有具体的约束规则去限制即时编译器应该如何实现,所以这部分功能完全是与虚拟机具体实现(Implementation Specific)相关的内容,如无特殊说明,本文提及的编译器、即时编译器都是指HotSpot虚拟机内的即时编译器,虚拟机也是特指HotSpot虚拟机。

解释器与编译器两者各有优势:当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即执行。在程序运行后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。当程序运行环境中内存资源限制较大(如部分嵌入式系统中),可以使用解释执行节约内存,反之可以使用编译执行来提升效率。

下面将通过代码示例,来阐述运行期优化。


一、即时编译

1.1 逃逸分析

先来看个例子

  1. public class T01_RunTime_EscapeAnalysis {
  2. public static void main(String[] args) {
  3. for (int i = 0; i < 200; i++) {
  4. long start = System.nanoTime();
  5. for (int j = 0; j < 1000; j++) {
  6. new Object(); // 循环创建对象
  7. }
  8. long end = System.nanoTime();
  9. System.out.printf("%d\t%d\n", i, (end - start));
  10. }
  11. }
  12. }

执行时间片段如下:

  1. 0 86861
  2. 1 89557
  3. 2 68333
  4. 3 64021
  5. 4 67292
  6. 5 63446
  7. -------------
  8. 67 22369
  9. 68 44501
  10. 69 30545
  11. 70 17963
  12. 71 21750
  13. -------------
  14. 195 13104
  15. 196 14453
  16. 197 23107
  17. 198 13574
  18. 199 14051

测试结果:我们可以看到同样的代码,为什么后续执行的时间越来越短呢?原因是什么呢?这就需要先了解 JVM 即时编译知识。

由于即时编译器编译本地代码需要占用程序运行时间,要编译出优化程度更高的代码,所花费的时间可能更长;而且想要编译出优化程度更高的代码,解释器可能还要替编译器收集性能监控信息,这对解释执行的速度也有影响。为了在程序启动响应速度与运行效率之间达到最佳平衡,HotSpot虚拟机还会逐渐启用分层编译(Tiered Compilation)的策略。

JVM 将执行状态分成了 5 个层次:

  • 0层,解释执行(Interpreter),将字节码解释为机器码
  • 1层,使用 C1 即时编译器编译执行(不带 profiling)
  • 2层,使用 C1 即时编译器编译执行(带基本的 profiling)
  • 3层,使用 C1 即时编译器编译执行(带完全的 profiling)
  • 4层,使用 C2 即时编译器编译执行

profiling 是指在运行过程中收集一些程序执行状态的数据,即信息统计工作,例如【方法的调用次数】,【循环的回边次数】等。

 即时编译器(JIT)与解释器的区别

  • 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
  • JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,下次遇到相同的代码,直接执行,无需再编译
  • 解释器是将字节码解释为针对所有平台都通用的机器码
  • JIT 会根据平台类型,生成平台特定的机器码

对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速度。执行效率上简单比较一下 :Interpreter < C1(可以提升5倍左右) < C2(可以提升10~100倍),总的目标是发现热点代码 (hotspot名称的由来)优化之。

刚才的一种优化手段称之为【逃逸分析】,发现新建的对象是否逃逸。可以使用 -XX:-DoEscapeAnalysis 关闭,默认打开,再运行刚才的示例观察结果。会才发现后续的运行时间没有大缩短了。


1.2 方法内联

  1. private static int square(final int i) {
  2. return i * i;
  3. }

 方法内联:如果发现 square() 是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、粘贴到调用者的位置

        System.out.println(square(9));

还能够进行常量折叠(constant folding)的优化

        System.out.println(81);

内联测试代码如下:

  1. // 运行期优化 —— 方法内联
  2. public class T02_RunTime_Inlining {
  3. // -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining // 查看 JVM 对代码的内联情况
  4. // -XX:CompileCommand=dontinline,*T02_RunTime_Inlining.square // 禁用内联
  5. // -XX:+PrintCompilation
  6. public static void main(String[] args) {
  7. int x = 0;
  8. for (int i = 0; i < 500; i++) {
  9. long start = System.nanoTime();
  10. for (int j = 0; j < 1000; j++) {
  11. x = square(9);
  12. }
  13. long end = System.nanoTime();
  14. System.out.printf("%d\t%d\t%d\n", i, x, (end - start));
  15. }
  16. }
  17. private static int square(final int i) {
  18. return i * i;
  19. }
  20. }

上述代码运行片段结果如下:

  1. 1 81 79805
  2. 2 81 35501
  3. 3 81 35220
  4. 4 81 31679
  5. 5 81 35823
  6. --------------------------
  7. 91 81 7316
  8. 92 81 7322
  9. 93 81 7327
  10. 94 81 8118
  11. 95 81 7441
  12. --------------------------
  13. 131 81 61
  14. 132 81 54
  15. 133 81 53
  16. 134 81 54
  17. 135 81 52

如果在上述代码加入 VM 参数(查看内联方法):-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining,再次运行

如果我们禁用内联:-XX:CompileCommand=dontinline,*T02_RunTime_Inlining.square,再次测试,耗时片断如下:

  1. 1 81 49707
  2. 2 81 31806
  3. 3 81 22157
  4. 4 81 35130
  5. 5 81 35269
  6. ----------------------
  7. 91 81 8425
  8. 92 81 8722
  9. 93 81 13402
  10. 94 81 8650
  11. 95 81 8596
  12. ----------------------
  13. 131 81 5365
  14. 132 81 5444
  15. 133 81 5633
  16. 134 81 4731
  17. 135 81 5293

相比第一次测试,到了130多次后,耗时再没有下降,因为内联关闭了


1.3 字段优化

JMH 基准测试请参考: http://openjdk.java.net/projects/code-tools/jmh/

创建 maven 工程,添加依赖如下

  1. org.openjdk.jmh
  2. jmh-core
  3. 1.21
  4. org.openjdk.jmh
  5. jmh-generator-annprocess
  6. 1.21

字段优化代码示例如下:

  1. // 运行期优化 —— 字段优化
  2. // 热身,先热身再优化
  3. @Warmup(iterations = 5, time = 1)
  4. // 5轮测试
  5. @Measurement(iterations = 5, time = 1)
  6. @State(Scope.Benchmark)
  7. public class T03_RunTime_FieldOptimize {
  8. int[] elements = randomInts(1_000);
  9. private static int[] randomInts(int size) {
  10. ThreadLocalRandom random = ThreadLocalRandom.current();
  11. int[] values = new int[size];
  12. for (int i = 0; i < size; i++) {
  13. values[i] = random.nextInt();
  14. }
  15. return values;
  16. }
  17. @Benchmark
  18. public void test1() {
  19. for (int i = 0; i < elements.length; i++) {
  20. doSum(elements[i]);
  21. }
  22. }
  23. @Benchmark
  24. public void test2() {
  25. int[] local = this.elements;
  26. for (int i = 0; i < elements.length; i++) {
  27. doSum(elements[i]);
  28. }
  29. }
  30. @Benchmark
  31. public void test3() {
  32. for (int element : elements) {
  33. doSum(element);
  34. }
  35. }
  36. static int sum = 0;
  37. @CompilerControl(CompilerControl.Mode.INLINE) // 控制调用方法时是不是要进行方法内联;允许内联
  38. static void doSum(int x) { sum += x;}
  39. public static void main(String[] args) throws RunnerException {
  40. Options opt = new OptionsBuilder()
  41. .include(T03_RunTime_FieldOptimize.class.getSimpleName())
  42. .forks(1)
  43. .build();
  44. new Runner(opt).run();
  45. }
  46. }

开启方法内联:CompilerControl.Mode.INLINE,每个方法2轮热身,5轮测试。结果如下(每秒吞吐量,分数越高的更好):

接下来禁用 doSum 方法内联:CompilerControl.Mode.DONT_INLINE

  1. @CompilerControl(CompilerControl.Mode.DONT_INLINE) // 控制调用方法时是不是要进行方法内联;
  2. static void doSum(int x) { sum += x;}

关闭方法内联,每个方法2轮热身,5轮测试。如果如下:吞吐量都有一定程度的下降

分析:

在上述的示例中,doSum() 方法是否内联会影响 elements 成员变量读取的优化:

如果 doSum() 方法内联了,test1 方法会被优化成下面的样子(伪代码)

  1. @Benchmark
  2. public void test1() {
  3. // elements.length 首次读取会缓存起来 -> int[] local
  4. for (int i = 0; i < elements.length; i++) { // 后续 999 次 求长度 <- local
  5. // doSum(elements[i]);
  6. doSum(elements[i]); // 1000 次取下标 i 的元素 <- local
  7. }
  8. }

可以节省 1999 次 Field 字段读取操作

但如果 doSum() 方法没有内联,则不会进行上面的优化

本地变量访问长度、数据时,不需要去 class 元数据那里找,在本地变量就可以找到了,相当于手动优化。但是方法内联是由虚拟机来优化的。所以,test3 方法与test2 方法是等价的,test1 方法是运行期间优化了,test2 方法是手动优化了, test3 方法的 foreach 是 编译期间优化了。


二、反射优化

通过“反射”我们可以动态的获取到对象的信息以及灵活的调用对象方法等,但是在使用的同时又伴随着另一种声音的出现,那就是“反射”很慢,要少用。那么 JVM 是怎样做了反射优化的呢?下面我们一起来分析一下,上示例代码:

  1. // 运行期优化 —— 反射优化
  2. public class T04_RunTime_Reflect {
  3. public static void foo() {
  4. System.out.println("foo...");
  5. }
  6. public static void main(String[] args) throws Exception {
  7. Method foo = T04_RunTime_Reflect.class.getMethod("foo");
  8. // 前16次调用效率较低,第17次调用效率较高
  9. for (int i = 0; i < 16; i++) {
  10. System.out.printf("%d\t", i);
  11. foo.invoke(null);
  12. }
  13. System.in.read();
  14. }
  15. }

查看 MethodAccessor 的实现类,进一步查看反射优化,会发现有个15次的阈值

下面,我们从源码的角度来分析反射底层是如何实现优化的:

第一步:先下断点到 foo.invoke(null); 运行程序到此行

第二步:点击进去 invoke() 方法实现,找到 MethodAccessor 的实现类

第三步:继续打开NativeMethodAccessorImpl 实现类并定位到 invoke() 方法,打下断点。如下图所示:

 第四步:通过 Idea 查看 Evaluate,来查看反射多次调用后,由 JVM 动态生成的实现类名 - GeneratedMethodAccessor1,这个名字在后面需要用的。且此时 this.numInvocations 已经到了 16了。

 


注意:接下来,我们通过 阿里的 arthas 工具来进行调试。需要在 Run 模式下运行程序。 

第一步:以 Run 模式运行程序

接着需要下载好 arthas工具:官网链接 https://github.com/alibaba/arthas/blob/master/README_CN.md

下载方式:curl -O https://arthas.aliyun.com/arthas-boot.jar  

第二步:运行 arthas:java -jar arthas-boot.jar

第三步:查看帮助,找到进行反编译的指令 jad (Decompile class)

第四步:通过 jad sun.reflect.GeneratedMethodAccessor1 将 JVM 对反射优化后的类的字节码反编译出来

  1. package sun.reflect;
  2. import com.jvm.t11_runtime_optimize.T04_RunTime_Reflect;
  3. import java.lang.reflect.InvocationTargetException;
  4. import sun.reflect.MethodAccessorImpl;
  5. public class GeneratedMethodAccessor1
  6. extends MethodAccessorImpl {
  7. /*
  8. * Loose catch block
  9. * Enabled aggressive block sorting
  10. * Enabled unnecessary exception pruning
  11. * Enabled aggressive exception aggregation
  12. * Lifted jumps to return sites
  13. */
  14. public Object invoke(Object object, Object[] arrobject) throws InvocationTargetException {
  15. // 比较奇葩的做法,如果有参数,那么抛非法参数异常
  16. block4: {
  17. if (arrobject == null || arrobject.length == 0) break block4;
  18. throw new IllegalArgumentException();
  19. }
  20. try {
  21. // 可以看到,已经是直接调用了
  22. T04_RunTime_Reflect.foo();
  23. // 因为没有返回值;如果方法有返回值,它会拿到返回值然后返回
  24. return null;
  25. }
  26. catch (Throwable throwable) {
  27. throw new InvocationTargetException(throwable);
  28. }
  29. catch (ClassCastException | NullPointerException runtimeException) {
  30. throw new IllegalArgumentException(super.toString());
  31. }
  32. }
  33. }

优化生成的 GeneratedMethodAccessor1 类也继承了 MethodAcessorImpl,它里面的 invoke() 方法是怎样写的呢?invoke() 方法本来我们理解应该是反射调用,但实际在它生成的 invoke() 里面变成了 T04_RunTime_Reflect.foo() ,因为在T04_RunTime_Reflect 类中的 foo() 方法是静态方法,所以使用:类名.静态方法名 来调用,这还是不是反射调用呢?已经不是反射调用了。所以第17次开始,JVM 虚拟机已经将我们的反射方法调用转换为 静态方法调用。

注意:

通过查看 ReflectionFactory 源码可知

  • sun.reflect.noInflation 可以用来禁用膨胀(直接生成 GeneratedMethodAccessor1,但首次生成比较耗时,如果仅反射调用一次,不划算)
  • sun.reflect.inflationThreshold 可以修改膨胀阈值

 


文章最后,给大家推荐一些受欢迎的技术博客链接:

  1. Hadoop相关技术博客链接
  2. Spark 核心技术链接
  3. JAVA相关的深度技术博客链接
  4. 超全干货--Flink思维导图,花了3周左右编写、校对
  5. 深入JAVA 的JVM核心原理解决线上各种故障【附案例】
  6. 请谈谈你对volatile的理解?--最近小李子与面试官的一场“硬核较量”
  7. 聊聊RPC通信,经常被问到的一道面试题。源码+笔记,包懂
  8. 深入聊聊Java 垃圾回收机制【附原理图及调优方法】

 


欢迎扫描下方的二维码或 搜索 公众号“10点进修”,我们会有更多、且及时的资料推送给您,欢迎多多交流!

                                           

       

 

注:本文转载自blog.csdn.net的不埋雷的探长的文章"https://blog.csdn.net/weixin_32265569/article/details/108098924"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

101
推荐
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2025 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top