前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >写一些友好的代码(下),对虚拟机友好

写一些友好的代码(下),对虚拟机友好

作者头像
星尘的一个朋友
发布2021-08-05 15:49:21
4820
发布2021-08-05 15:49:21
举报

写一些友好的代码(下),对虚拟机友好

关于编码,我想这应该是程序员最熟悉不过的事儿了。也是作为一个程序员最基本的职能了,而恰恰是最常做的事,最容易产生自信。而这种自信,有时便会成为理所应当的坏习惯。

之前听郑雨迪(Oracle 高级研究员)说他的工作就是怎么让程序员写的代码在虚拟机上跑的更快,听起来很伟大,但细想想,作为程序员的我们。是不是也可以贡献一点力量呢?

上一篇我整理了一些对人友好的代码内容,简单来说就是不要写一些让人看不懂,或者看着不舒服的代码。

这一篇我想要分享的内容,就是让我们通过学习虚拟机的一些执行过程来优化我们的代码,为虚拟机分担一些压力。也想借此文章谈谈自己理解的 Java 程序员为什么要去学习虚拟机。

听过很多朋友说过,我做开发根本都碰不到虚拟机,学这有啥用?还有很多朋友自己在被问到,或者自己正在学习虚拟机时也会觉得“你问我这东西是啥意思?故意难我?我学玩虚拟机能干啥?到底学这东西有啥用?”。这些疑问让我来回答的话,那就是今天的题目了。为了写出对机器友好的代码。

无奈的虚拟机

随着时代的发展,我发现越来越明显的一件事,就是更多人,不喜欢 ”浪费时间“。这里的浪费时间想要说的意思是,懒。

当你注册一个应用程序的账号时候,如果他的流程过于繁琐。那么他可能就因此失去你这个用户。又或者一个游戏如果他的玩法过于复杂,那自然也没什么人会选择继续玩下去。但假如,这个应用是你必须要注册的,而且没有其他的备选应用去选择,那么即使他注册需要再多的流程,在怎么繁琐,你也是要硬着头皮去做,而且边做边觉得让你”恶心“。

就像我们的 JAVA 虚拟机,即使你的代码写的在怎么糟糕,只要语法没有问题,虚拟机都要去完成代码的执行。但你有没有想过,你写的代码,可能也是虚拟机没有选择余地的那种,他没办法不执行,所以它也边做边觉得“恶心”呢?

程序编译和代码优化

如果不想让虚拟机“恶心”,我们一起看看虚拟机是怎么处理我们写下的代码吧,从中理解一些对虚拟机友好的代码吧。

编译器

Java 程序编译有两个意思,一个是将源码编译成 class 格式,一个是将 class 格式文件内容编译成可以被机器可直接执行的本地机器码格式。

在这个过程共分为三类编译器

  • 前端编译器:jdk 的 javac、eclipse 的 JDT
  • 即时(JIT)编译器:hotspot 的 c1 c2 graal
  • 提前(AOT)编译器:jdk 的 jaotc GNU Compiler for the Java(GCJ)、Excelsior JET。
前端编译 (Javac 编译)

这里可以参考之前的文章《Java 类的一生》当中的 Javac 的孕育了解更多内容。下面说下前面文章没有提到的内容。

语法糖

可以帮助我们提高开发效率、程序严谨,没有实质的功能提升。

  1. 泛型 使参数动态化
  2. 自动拆装箱、增强 for 循环、变长参数
  3. 条件编译优化
代码语言:javascript
复制
public static void main (String args[]) {
    if (true) {
        System.out.println(1);
    } else {
        System.out.println(2);
    }
}

条件编译优化后

代码语言:javascript
复制
public static void main (String args[]) {
    System.out.println(1);
}

Java语言还有不少其他的语法糖,如内部类、枚举类、断言语句、数值字面量、对枚举和字符串的switch支持、try语句中定义和关闭资源、Lambda表达式(从JDK 8开始支持,Lambda不能算是单纯的语法糖,但在前端编译器中做了大量的转换工作)

后端编译(JVM中的编译)

IR:程序语言的一种中间表示形式(Intermediate Representation)

后端编译包括

  • 即时编译技术 JIT
  • 提前编译技术 AOT
即时编译

带着下面问题来看后面的内容

  • 为何HotSpot虚拟机要使用解释器与即时编译器并存的架构?
  • 为何HotSpot虚拟机要实现两个(或三个)不同的即时编译器?
  • 程序何时使用解释器执行?何时使用编译器执行?
  • 哪些程序代码会被编译为本地代码?如何编译本地代码?
  • 如何从外部观察到即时编译器的编译过程和编译结果?

在解释执行的过程中,当虚拟机发现某部分代码执行频繁,称为热点代码。虚拟机就会把这部分代码提前变成本地可以执行的机器码以提高执行效率。

解释器与编译器

解释执行

当程序需要迅速启动和执行的时候,解释器可以首先发挥作用,省去编译的时间,立即运行。

编译执行

当程序启动后,随着时间的推移,编译器逐渐发挥作用,把越来越多的代码编译成本地代码,这样可以减少解释器的中间损耗,获得更高的执行效率

逆优化

当程序执行过程中的类不断的变化,发生一些特殊情况(优化效果不如不优化)便会发生逆优化操作继续使用解释执行。

在这里插入图片描述
在这里插入图片描述

即时编译器分为客户端编译器 C1 和服务端编译器 C2 还有目前在 JDK 10 中处于实验阶段的 Graal编译器(为了替换 C2

可以通过参数来强制虚拟机仅使用 C1C2 执行

代码语言:javascript
复制
-client”或“-server

解释器与编译器搭配使用的方式在虚拟机中被称为“混合模式”(MixedMode).可以通过 java -version 命令查看

  • 使用参数“-Xint”强制虚拟机运行于“解释模式”(Interpreted Mode),这时候编译器完全不介入工作,全部代码都使用解释方式执行。
  • 使用参数“-Xcomp”强制虚拟机运行于“编译模式”(CompiledMode),这时候将优先采用编译方式执行程序,但是解释器仍然要在编译无法进行的情况下介入执行过程。
在这里插入图片描述
在这里插入图片描述

使用参数切换执行方式

在这里插入图片描述
在这里插入图片描述
分层编译

hotspot 中包含多个即时编译器,C1(客户端编译器) C2(服务端编译器) Graal(JDK10引入的实验性质编译器,目标是替换 C2)

JDK 7 以前需要程序特性选择即时编译器

  • 执行时间短、需要快速启动的程序选择 编译速度快 的C1 编译器(参数 -client)
  • 执行时间长、对峰值有要求的,选择 执行速度快 的 C2 编译器 (参数 -server)

0 层

解释执行,不开启性能监控

1 层

执行C1 编译的代码,这部分代码是由 C1 快速编译成本地代码,进行一些基本的简单优化(如:方法内联、公共子表达式消除、冗余代码消除,包括冗余访问和冗余存储冗余赋值)

2 层

执行C1 编译的代码,这部分代码是带有方法执行次数和循环回边次数的 profiling (性能监控状态的数据)

3 层

执行 C1 编译的代码,这部分为带所有 profiling 的 C1 代码。

4 层

执行 C2 编译的代码,进行更加复杂的优化内容,同时可能会根据 profiling 做一些激进的优化。

总结

分层编译也可大致分为三层:

  • 0层:解释执行
  • 1层:C1 编译执行 对应 1/2/3 层
  • 2层0.:C2 编译执行 对应 4 层

其中 1 层和 4 层为最终状态层,当一个方法进入到 C1 的 1 层编译优化后或到达了 C2 的 4 层编译优化后虚拟机在之后的执行中是不会再发出编译请求的了。

编译的对象和条件

编译器编译的对象一般为方法级。

常说“热点代码”会被虚拟机通过即时编译技术进行优化,那什么样的代码才算热点代码?

  • 被多次调用的方法
  • 多次执行的循环体(具体有回边次数值)

辨别方法多次调用:

  1. 基于采样
    • 虚拟机会周期检测各个线程的栈顶,如果某个方法经常出现在栈顶,则视为热点代码
      • 特点:简单、高效
      • 缺点:不准
  2. 基于计数器:hotspot 使用这种方式
    • 为每一个方法维护一个调用次数计数器
      • 特点:准确
      • 缺点:复杂

识别循环体多次:

根据循环的回边次数进行优化的技术又称为 OSR(On Stack Replacement) —— 栈上替换,因为这部分代码是在执行过程中被优化的,在非方法入口处进行解释执行的代码和编译后的代码进行替换。这个触发条件是有参数 (-XX:CompileThreshold)控制的,计算公式如下

CompileThreshold * (OnStackReplacePercentage - InterpreterProfilePercentage)/ 100

参数 -XX:InterpreterProfilePercentage 默认值 33

C1 情况下 OnStackReplacePercentage 默认值 933

C2 情况下 OnStackReplacePercentage 默认值 140

所以在默认情况下, C1 的循环回边次数达到 13500 时会启用 OSR 技术进行即时编译优化代码, C2 则在 10700 时启用。

通常情况下,程序代码不会触发 OSR,在测试中常见

循环回边次数触发的 OSR 技术优化的对象为 代码块。

方法调用次数的优化对象为 整个方法。

提前编译

关于提前编译,我这里只是简单的理解一下。如果对这个感兴趣的话,需要继续找资料深入。我就简单说下我的理解在这里。

Java 程序在早期就拥有这项技术,就是提前把字节码文件编译成机器码。这样就可以加快执行速度。 但是 Java 又想拥有跨平台的小目标,所以也就不好推进。不过另一个弟兄,Android 做的很好,不过后来是因为提前编译时间太长(安装包安装时间),又不得不使用解释执行和即时编译技术。

编译优化技术

除了上面的两个“热点代码”优化,编译器是如何优化普通的字节码的?首先两个关键的技术 方法内联 逃逸分析

方法内联

将方法的调用,优化成调用者内部代码,省去入栈出栈操作。例如 set get 方法均会被方法内联所优化掉

逃逸分析

分析一个对象的动态作用域,举例来说:

  • 一个对象当做参数传给了另外方法称为 方法逃逸
  • 对一个类变量进行了赋值称为 线程逃逸

通过这两种逃逸方式,可以进行相关优化操作,比如:

  • 一个对象如果没有发生 方法逃逸 ,可以进行栈上分配或者 标量替换(hotspot 使用这种方式进行优化代码)
  • 如果一个变量没有发生过 线程逃逸 ,可以进行 同步(锁)消除 优化。

通过逃逸分析,我们可以利用到一点,就是锁消除,即不加锁。而不用是去加了锁,然后等到真正执行时由虚拟机去优化。

循环优化
  1. 循环无关代码外提:将循环过程中不变的代码外提至循环外,减少重复的冗余计算
  2. 循环展开:再循环过程中进行多次迭代,减少循环次数。特殊形式是完全展开;
  3. 循环判断外提:无关循环的判断移到循环外。
  4. 循环剥离:循环的前几次或后几次一般可以被优化掉,减少循环次数。
冗余代码消除

还有一些其他的优化手段了解一下。

  1. 数组范围检查消除
  2. 空值检查消除
  3. 自动装箱消除

学习虚拟机的思考

关于虚拟机的学习东西很多,而且很枯燥。能坚持学完除了兴趣,还要有耐心。虽然自己只学习了一些虚拟机的皮毛,但再去写代码的时候,也会有很多帮助,比如你定义的常量值,在 javac 的时候就会被替换成具体的值。也是那为什么你替换了一个常量值文件导致这个值没生效的原因。比如在写代码的时候也能利用一些优化知识来写一些相对精致的代码。比如对自己写下的代码它将来会发生什么比较清楚等等。总之这些价值都不能够直接的体现出来,因为只有你自己才知道。

不要因为一时看不到结果而放弃一些追求,凡事坚持下去终会有结果。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2021-04-13 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写一些友好的代码(下),对虚拟机友好
    • 无奈的虚拟机
      • 程序编译和代码优化
        • 编译器
        • 编译的对象和条件
      • 提前编译
        • 编译优化技术
          • 方法内联
          • 逃逸分析
          • 循环优化
          • 冗余代码消除
        • 学习虚拟机的思考
        相关产品与服务
        应用性能监控
        应用性能监控(Application Performance Management,APM)是一款应用性能管理平台,基于实时多语言应用探针全量采集技术,为您提供分布式性能分析和故障自检能力。APM 协助您在复杂的业务系统里快速定位性能问题,降低 MTTR(平均故障恢复时间),实时了解并追踪应用性能,提升用户体验。
        领券
        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档