关于jvm:深入理解jvm-编译优化上

42次阅读

共计 4342 个字符,预计需要花费 11 分钟才能阅读完成。

前言

编译优化的内容还是不少的,当然次要的内容集中在后端的编译下面,为了管制篇幅的长度所以这里抉择拆分为高低两局部解说,咱们平时写的代码和理论运行时候的代码成果是齐全不一样的,理解编译优化的细节是有必要的。

概述

  1. 理解 javac 的根本编译过程以及根本的解决细节
  2. 理解根本的前端优化伎俩:语法糖和泛型的实现
  3. 理解前后端编译的内容以及局部后端编译的内容。

Javac 的编译过程

javac 的工程代码并不属于 java se api 的一部分,同时因为 jdk9 的版本之后模块化被独自分离出来了,书中应用了 jdk9 的版本来解说对于 javac 的编译过程。

筹备工作

包所在的地位(jdk9):JDK_SRC_HOM E/src/jdk.comp iler/share/classes/com/sun/tools/javac

如果须要搭建一个 javac 的工程只有新建一个工程并且把上面门路的内容复制到工程的上面即可。

J D K SR C H O M E / l a n gt o o l s / s r c / s h a r e / c l a s s e s / c o m / s u n / *

拷贝实现之后,一个反对 javac 命令的工程就搭建好了

javac 的编译步骤

对于 javac 的编译步骤根本如下,须要留神的是这里蕴含了 jdk5 版本中的注解处理器的内容:

  1. 筹备:初始化插入式注解处理器
  2. 解析和填充符号表过程

    1. 词法剖析
    2. 填充符号表
  3. 插入式注解处理器处理过程:

    1. 插入式注解处理器的执行阶段
  4. 剖析与字节码生成(语法分析是 IDE 罕用局部)

    1. 标注查看(数据分析,常量折叠优化)
    2. 数据流和数据分析(上下文语义剖析查看)
    3. 解语法糖(由 desagrc 办法触发)
    4. 字节码生成

上面是书中对于整个编译过程的一张图表演示,能够看到程序不是固定的,而是会存在更换程序的状况:

前端优化

注解处理器

注解处理器的步骤是在 jdk5 当中新增的内容,在 Javac 源码中,插入式注解处理器的初始化过程是在 initPorcessAnnotations()办法中实现的,而它 的执行过程则是在 processAnnotations()办法中实现。这个办法会判断是否还有新的注解处理器须要执 行,如 果 有 的 话,通 过 c o m . s u n . t o o l s . j a v a c . p r o c e s s i n g. J a v a c P r o c e s s i n g- E n v i r o n m e n t 类 的 d o P r o c e s s i n g() 方 法 来生成一个新的 JavaComp iler 对象,对编译的后续步骤进行解决。

语法糖

java 在降级的过程中引入了很多的语法糖写法,比方 jdk5 的加强 for 循环和泛型,jdk7 的泛型菱形标记和 try-catch-resource,jdk8 的 lambada 表达式等,这些语法糖对于 jdk 的易用性给予了很多反对。这里挑几个重点的降级进行形容:

泛型

泛型的启发来源于 pizza 的后身 scala 语言的作者 Martin Odersky,当他捣鼓出泛型这个货色 之后,立马被 java 官网邀请开发 java 的泛型,可怜的 Martin Odersky 受制于 java 的语法限度以及向后兼容的个性,最初做进去的成绩反而更加相似 C# 的泛型(挺讥刺的),最终的后果就到了当初 java 官网还在背着这个技术偷懒的债。

扯远了,泛型置信所有的 java 开发者都很相熟了,这里不再进行独自介绍。通常状况下实现泛型有上面的两种方法:

  • 泛化类型以前放弃不变,平行退出泛化新类型
  • 已有类型泛型化,不退出任何泛型类型。

java 应用的是第二种形式,原因无他,只是因为偷懒而已,在过后如果有更多工夫探讨的话抉择第一种是更好的抉择也会有更多的解决方案,上面来简略理解一下泛型的基本特征以及须要实现的内容:

### 类型擦除

​ 首先,java 引入了类型擦除的机制,java 的泛型在初始阶段叫做裸类型(父类型),裸类型能够看作是 jdk5 之前的类型即不带尖括号的类型,在实现裸类型下面有两种实现形式:

  • 由虚拟机进行真正的结构
  • 编译时还原,在元素拜访的时候类型强转。

没错,java 实现的形式也是应用了第二种形式,强转的实现相比 第一种办法要简略很多,然而也会带来上面的问题:

  • 原始类型的反对变麻烦,java 用主动的类型转换代替间接导致了主动拆装箱的时候效率非常的低下
  • 在运行阶段无奈读取到泛型的类型,java 的泛型只能算是一个“伪造”泛型。

泛型的擦除机制决定了 java 的泛型反对更多的是服务于编译器。

留神:1. 擦除只是 code 字节码擦除。2. 元数据保留擦除前的信息。

### 泛化后的属性

Sinature 属性::存储的是办法在字节码层面的非凡签名,属性中保留参数化的类型信息而不是原始的类型,

值类型的反对:值类型也称之为 valueType 也就是能够定义根底数据类型的类型。

## 条件编译的实现

​ 条件编译能够简略了解为通过 if 语句这个指令进行实现,java 天生不反对条件编译,然而 C 和 C ++ 外面却是能够实现的。

​ Java 语言中条件编译的实现,也是 Java 语言的一颗语法糖,依据布尔常量值的虚实,编译器将会把 分支中不成立的代码块打消掉,这一工作将在编译器解除语法糖阶段 (com.sun.tools.javac.comp .Lower 类中) 实现。

​ 最初条件编译上有个历史事件就是之前所说的 Shenandoah 收集器被 jdk 官网用条件编译给抹除,导致这款收集器不被商用 jdk 反对,也只能在 openJDK 下面应用。

通过下面的内容学习和理解,能够发现前端的编译作用是比拟小的,能够算是是语法糖的一部分,而后端优化就没有那么简略了,上面咱们来看下后端优化是如何实现的。

后端优化

即时编译器

即便编译器的重要位置自不用说,到当初还是支流编译器的 Hotspot 就能够阐明即时编译器的重要性,而 Hotspot 外面一项重要的优化就是即便编译器,在理解即时编译器之前,咱们须要弄清楚上面的问题:

  • 为什么解释器和即时编译器并存
  • 为什么要多个编译器
  • 什么时候用解释器,什么时候用即时编译器
  • 哪些代码为本地代码,如何编译
  • 内部如何察看后果

通过解决下面的问题,咱们就能够大抵理解即时编译的核心内容。

即时编译的形式:面向办法而不是面向部分代码,这种办法在字节码序列号替换的形式被称为栈上替换,办法还在栈桢的时候被编译器进行隐式替换。

为什么会并存解释器和编译器?

并不是所有的即时编译器都是用的解释器和编译器并存的模式,然而目前支流的的几款产品中根本都存在这种共存的运行模式,他有什么作用呢?首先,它能够作为一个逃生门,在通常的状况下放弃失常的配合操作,然而一旦编译器忙不过来的时候或者本地代码过多的状况下,就能够应用解释器“兜底”,能够保障任何状况下总是能够失常的运行代码。正所谓男女搭配,干活不累。

为什么有多个呢?

在 Hotspot 的编译器下有两个编译器:

  • C1: 客户端编译器:效率高,十分快,然而品质个别
  • C2: 服务端编译器:品质高然而效率要低一些

编译器为什么不止一个还有多个,这又是无关历史的话题,在晚期的工作模式上面,解释器会依据服务器的资源以及用户指定的匹配前端编译器解决来提高效率,所以存在多个也是能够了解的。

分层编译

咱们不再须要理解以前的工作原理,而是要理解 jdk7 之后彻底实现的分层编译伎俩:

  1. 纯解释器模式:第一层
  2. 客户端编译器执行,开启局部监控:第二层
  3. 客户端编译器执行,开启残缺监控:第三层
  4. 服务端编译为本地代码:第四层

当然下面的步骤不是齐全固定的,依据理论状况会做程序的调整,上面是书中给出的一张图:

热点代码探测

热点探测有两种形式:基于采样的热点探测 (Sample Based Hot Spot Code Detection) 和基于计数器的热点探测 (Counter Based Hot Spot Code Detection),在 HotSpot 虚拟机中应用的是第二种基于计数器的热点探测办法,为了实现热点计数,HotSpot 为每个办法筹备了 两类计数器: 办法调用计数器(Invocation Counter) 和回边计数器(Back Edge Counter,“回边”的意思 就是指在循环边界往回跳转)。

热点代码探测是 Hotspot 又一项“灵魂”后端优化,热点代码又被称之为屡次调用的代码或者屡次执行循环体的代码,然而 hotspot 是如何判断的呢?如何获取某个办法执行多少次?以及怎么算“够久”?

首先咱们先答复执行多少次的问题,hotspot 应用的是两种计数器来实现:办法调用计数器和回边计数器。

而够久略微简单一些,办法调用和回边计数的断定形式是不一样的。上面用一个简略的列表来阐明一下触发办法调用器热点代码的断定条件:

### 办法调用计数器

办法调用器:客户端编译器 15000 次,服务端编译器 10000 次
条件:回边 + 办法调用 >= 下面的阈值
留神:工夫范畴内的调用次数。统计办法:半衰周期。

### 回边计数器

办法调用计数器好懂一些,这里不做过多解释,上面咱们补充一下回边计数器的细节,回边计数器就是指统计循环代码中执行的次数,当然不是单纯的计算循环体的执行次数,而是应用上面的公示计算:

客户端模式(默认为 13995):办法调用计数器阈值(-XX: C o m p i l e T h r e s h o l d) 乘 以 O SR 比 率 (– X X : O n St a c k R e p l a c e P e r c e n t a ge) 除 以 1 0 0。其 中 – X X : OnStackRep lacePercentage 默认值为 933

服务端模式(默认为 10700)办法调用计数器阈值 (-XX: C o m p i l e T h r e s h o l d) 乘 以 (O SR 比 率 ( – X X : O n St a c k R e p l a c e P e r c e n t a ge) 减 去 解 释 器 监 控 比 率 (– X X : InterpreterProfilePercentage) 的差值)除以 100。其中 -XX:OnStack ReplacePercentage 默认值为 140,- X X : I n t e r p r e t e r P r o f i l e P e r c e n t a ge 默 认 值 为 3 3。

当回边办法触发到到阈值的时候,会触发一个叫做“栈上替换”的操作。并且回边计数器没有半衰周期的概念,当达到绝对值的条件的时候就会触发,而如果这个数字始终增长达到计数器的下限并且溢出,回边计数器会重置并且顺带把办法计数器的值为归 0。最初在回边计数达到阈值的时候,会略微升高以后回边计数器的值让下一次的代码仍旧执行循环(不然栈上替换完了,循环也执行完了就没有意义了)。

## 结构图比照:

咱们依据下面的形容来看下两个计数器的计算逻辑结构图:

办法调用

回边计数器

前后端编译概览

总结

本节咱们讲述了 javac 指令的底层执行过程,以及前端优化和后端优化,前端优化次要是对于 java 的语法糖优化以及一项重要的优化注解生成器。在后续的文章中咱们介绍了局部后端编译优化的形式,即便编译器,以及热点代码探测,在即时编译外面咱们讲述了分层编译的性能。最初咱们用结构图讲述了编译的内容。

写在最初

本文讲述了对于后端编译的局部,下一节将会讲述对于后端编译的另外一部分内容。

正文完
 0