运行期优化

即时编译

什么是即时编译?

  • 当虚拟机发现某个方法或某段代码运行的特别频繁时,会把这段代码认为成热点代码;
  • 在运行时,虚拟机会将这段代码编译成平台相关的机器码,并进行各种层次的优化。

HotSpot 虚拟机内的即时编译器运作过程

我们主要通过以下 5 个问题来了解 HotSpot 虚拟机的即时编译器。

为什么要使用解释器与编译器并存的架构?

  • 解释器的优点:可以提高程序的响应速度(省去了编译的时间),并且节约内存。
  • 编译器的优点:可以提高执行效率。
  • 虚拟机参数设置:
    • 强制运行于解析模式:-Xint,编译器完全不工作;
    • 强制运行于编译模式:-Xcomp,当编译器编译失败时,解释执行还是会介入的。

为什么虚拟机要实现两个不同的 JIT 编译器?

  • Client Compiler(C1):不激进优化;
  • Server Compiler(C2):激进优化,如果激进优化不成立,再退回为解释执行或者 C1 编译器执行。

什么是虚拟机的分层编译?

分层编译就是根据编译器编译、优化的规模与耗时,划分出不同的编译层次,在代码运行的过程中,可以动态的选择将某一部分代码片段提升一个编译层次或者降低一个编译层次。

C1 与 C2 编译器会同时工作,许多代码可能会被多次编译。

目的: 在程序的启动响应时间和运行效率间达到平衡。

编译层次的划分:

  • 第 0 层:解释执行,不开启性能监控;
  • 第 1 层:将字节编译为机器码,但不进行激进优化,有必要时会加入性能监控;
  • 第 2 层及以上:将字节编译为机器码,会根据性能监控信息进行激进优化。

如何判断热点代码,触发编译?

1、什么是热点代码?

  • 被多次调用的方法;
  • 被多次执行的循环体;
    • 虽然被判断为热点代码的是循环体,不过因为虚拟机的即时编译是以方法为单位的,所以编译器还是会将循环体所在的方法整个作为编译对象。

我们发现,判断热点代码的一个要点就是: 多次执行 。那么虚拟机是如何知道一个方法或者一个循环体被多次执行的呢?

2、什么是 “多次” 执行?

  • 基于采样的热点探测

    • 虚拟机周期检查各个线程的栈顶,如果发现一个方法经常出现在栈顶,则该方法为热点方法。
    • 优点: 容易获取方法的调用关系,且简单高效。
    • 缺点: 无法精准的判断一个方法的热度,并且容易受到线程阻塞的影响,如果一个方法由于它所在的线程被阻塞的缘故而一直出现在栈顶,我们并不能认为这个方法被调用的十分频繁。
  • 基于计数器的热点探测
    • 虚拟机为每一个方法(或代码块)建立一个计数器,一旦执行次数超过一定阈值,就将其判为热点代码。
    • 优点: 精确严谨。
    • 缺点: 不能直接获取方法的调用关系,且实现复杂。
    • HotSpot 使用的是这个,并且还为每个方法建立了两个计数器。

2.1、HotSpot 中每个方法的 2 个计数器

  • 方法调用计数器

    • 统计方法被调用的次数,处理多次调用的方法的。
    • 默认统计的不是方法调用的绝对次数,而是方法在一段时间内被调用的次数,如果超过这个时间限制还没有达到判为热点代码的阈值,则该方法的调用计数器值减半。
      • 关闭热度衰减:-XX: -UseCounterDecay(此时方法计数器统计的是方法被调用的绝对次数);
      • 设置半衰期时间:-XX: CounterHalfLifeTime(单位是秒);
      • 热度衰减过程是在 GC 时顺便进行。
  • 回边计数器
    • 统计一个方法中 “回边” 的次数,处理多次执行的循环体的。

      • 回边:在字节码中遇到控制流向后跳转的指令(不是所有循环体都是回边,空循环体是自己跳向自己,没有向后跳,不算回边)。
    • 调整回边计数器阈值:-XX: OnStackReplacePercentage(OSR比率)
      • Client 模式:回边计数器的阈值 = 方法调用计数器阈值 * OSR比率 / 100;
      • Server 模式:回边计数器的阈值 = 方法调用计数器阈值 * ( OSR比率 - 解释器监控比率 ) / 100;

2.2、HotSpot 热点代码探测流程

热点代码编译的过程?

虚拟机在代码编译未完成时会按照解释方式继续执行,编译动作在后台的编译线程执行。

禁止后台编译:-XX: -BackgroundCompilation,打开后这个开关参数后,交编译请求的线程会等待编译完成,然后执行编译器输出的本地代码。

经典优化技术介绍

公共子表达式消除【语言无关】

如果一个表达式 E 已经计算过了,并且从先前的计算到现在,E 中所有变量值都没有发生变化,则 E 为公共子表达式,无需再次计算,直接用之前的结果替换。

举例

有表达式int d = (c * b) * 12 + a + (a + b * c),这段代码交给Javac编译器不会进行任何优化。

当这段代码进入到虚拟机即时编译器后,编译器检测到c * b和b * c是一样的表达式,因此这条公式变为int d = E * 12 + a + (a + E)

还可能变为int d = E * 13 + a * 2

数组范围检查消除【语言相关】

在循环中使用循环变量访问数组,如果可以判断循环变量的范围在数组的索引范围内,则可以消除整个循环的数组范围检查

方法内联【最重要】

目的是:去除方法调用的成本(如建立栈帧等),并为其他优化建立良好的基础,所以一般将方法内联放在优化序列最前端,因为它对其他优化有帮助。

类型继承关系分析(Class Hierarchy Analysis,CHA):用于确定在目前已加载的类中,某个接口是否有多于一种的实现,某个类是否存在子类、子类是否为抽象类等。

  • 对于非虚方法:

    • 直接进行内联,其调用方法的版本在编译时已经确定,是根据变量的静态类型决定的。
  • 对于虚方法: (激进优化,要预留“逃生门”)
    • 向 CHA 查询此方法在当前程序下是否有多个目标可选择;

      • 只有一个目标版本:

        • 先对这唯一的目标进行内联;
        • 如果之后的执行中,虚拟机没有加载到会令这个方法接收者的继承关系发生改变的新类,则该内联代码可以一直使用;
        • 如果加载到导致继承关系发生变化的新类,就抛弃已编译的代码。
      • 有多个目标版本:
        • 使用内联缓存,未发生方法调用前,内联缓存为空;
        • 第一次调用发生后,记录调用方法的对象的版本信息;
        • 之后的每次调用都要先与内联缓存中的对象版本信息进行比较;
          • 版本信息一样,继续使用内联代码;
          • 版本信息不一样,说明程序使用了虚方法的多态特性,这时取消内联,查找虚方法进行方法分派。

逃逸分析【最前沿】

基本行为

分析对象的作用域,看它有没有能在当前作用域之外使用:

  • 方法逃逸:对象在方法中定义之后,能被外部方法引用,如作为参数传递到了其他方法中。
  • 线程逃逸:赋值给 static 变量,或可以在其他线程中访问的实例变量。

对于不会逃逸到方法或线程外的对象能进行优化

  • 栈上分配: 对于不会逃逸到方法外的对象,可以在栈上分配内存,这样这个对象所占用的空间可以随栈帧出栈而销毁,减小 GC 的压力。
  • 标量替换(重要):
    • 标量:基本数据类型和 reference。
    • 不创建对象,而是将对象拆分成一个一个标量,然后直接在栈上分配,是栈上分配的一种实现方式。
    • HotSpot 使用的是标量替换而不是栈上分配,因为实现栈上分配需要更改大量假设了 “对象只能在堆中分配” 的代码。
  • 锁消除: 不会逃逸到线程外的方法不需要进行同步。

虚拟机参数

  • 开启逃逸分析:-XX: +DoEscapeAnalysis
  • 开启标量替换:-XX: +EliminateAnalysis
  • 开启锁消除:-XX: +EliminateLocks
  • 查看分析结果:-XX: PrintEscapeAnalysis
  • 查看标量替换情况:-XX: PrintEliminateAllocations

优化案例

原始代码:

static class B {
int value;
final int get() {
return value;
}
}
public void foo() {

y = b.get();

// ...do stuff...

z = b.get();

sum = y + z;

}

第一步优化: 方法内联(一般放在优化序列最前端,因为对其他优化有帮助)

目的:

  • 去除方法调用的成本(如建立栈帧等)
  • 为其他优化建立良好的基础

    public void foo() {
    y = b.value;
    // ...do stuff...
    z = b.value;
    sum = y + z;
    }

第二步优化: 公共子表达式消除

public void foo() {
y = b.value;
// ...do stuff... // 因为这部分并没有改变 b.value 的值
// 如果把 b.value 看成一个表达式,就是公共表达式消除
z = y; // 把这一步的 b.value 替换成 y
sum = y + z;
}

第三步优化: 复写传播

public void foo() {
y = b.value;
// ...do stuff...
y = y; // z 变量与以相同,完全没有必要使用一个新的额外变量
// 所以将 z 替换为 y
sum = y + z;
}

第四步优化: 无用代码消除

无用代码:

  1. 永远不会执行的代码
  2. 完全没有意义的代码
public void foo() {
y = b.value;
// ...do stuff...
// y = y; 这句没有意义的,去除
sum = y + y;
}

深入理解java虚拟机笔记Chapter11的更多相关文章

  1. Java内存区域与内存溢出异常——深入理解Java虚拟机 笔记一

    Java内存区域 对比与C和C++,Java程序员不需要时时刻刻在意对象的创建和删除过程造成的内存溢出.内存泄露等问题,Java虚拟机很好地帮助我们解决了内存管理的问题,但深入理解Java内存区域,有 ...

  2. 深入理解java虚拟机笔记Chapter12

    (本节笔记的线程收录在线程/并发相关的笔记中,未在此处提及) Java内存模型 Java 内存模型主要由以下三部分构成:1 个主内存.n 个线程.n 个工作内存(与线程一一对应) 主内存与工作内存 J ...

  3. 深入理解Java虚拟机笔记

    1. Java虚拟机所管理的内存 2. 对象创建过程 3. GC收集 4. HotSpot算法的实现 5. 垃圾收集器 6. 对象分配内存与回收细节 7. 类文件结构 8. 虚拟机类加载机制 9.类加 ...

  4. 深入理解java虚拟机笔记Chapter7

    虚拟机类的加载机制 概述 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类的加载机制. 类加载的时机 J ...

  5. 深入理解java虚拟机笔记之一

    Java的技术体系主要有支撑java程序运行的虚拟机,提供各开发领域接口支持Java API,java编程语言及许多第三方java框架( 如Spring,Structs等)构成. 可以把Java程序设 ...

  6. 深入理解Java虚拟机笔记——虚拟机类加载机制

    目录 概述 动态加载和动态连接 类加载的时机 类的生命周期 被动引用 例子一(调用子类继承父类的字段) 例子二(数组) 例子三(静态常量) 类加载的过程 加载 验证 准备 解析 符号引用 直接引用 初 ...

  7. 【转载】深入理解Java虚拟机笔记---运行时栈帧结构

    栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈(Virtual Machine Stack)的栈元素.栈帧存储了方法的局部变量表,操作 ...

  8. 深入理解java虚拟机笔记Chapter8

    运行时栈帧结构 栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素.栈帧存储了方法 ...

  9. 深入理解java虚拟机笔记Chapter2

    java虚拟机运行时数据区 首先获取一个直观的认识: 程序计数器 线程私有.各条线程之间计数器互不影响,独立存储. 当前线程所执行的字节码行号指示器.字节码解释器工作时通过改变这个计数器值选取下一条需 ...

随机推荐

  1. 基于 RTF specification v1.7 的 RTF 文件解析及 OLE 对象提取(使用 Python 开发)

    0x01 Office RTF 文件介绍 RTF 文件也称富文本格式(Rich Text Format, 一般简称为 RTF),意为多文本格式是由微软公司开发的跨平台文档格式.大多数的文字处理软件都能 ...

  2. [CTF]猪圈密码

    [CTF]猪圈密码 -------------------- 百度百科 本词条由"科普中国"百科科学词条编写与应用工作项目 审核 . https://baike.baidu.com ...

  3. .NET之默认依赖注入

    介绍 不要依赖于具体的实现,应该依赖于抽象,高层模块不应该依赖于底层模块,二者应该依赖于抽象.简单的说就是为了更好的解耦.而控制反转(Ioc)就是这样的原则的其中一个实现思路, 这个思路的其中一种实现 ...

  4. php 获取某文件夹(比如共享文件夹)下图片并下载并压缩成zip

    1.前端部分:直接请求 2.后端php //zip下载public function downZip(){ $pro_code = "test"; //zip名称 //获取列表 $ ...

  5. JDBC核心技术(获取数据库链接、数据库事务、数据库链接池)

    @ 目录 前言 数据的持久化 Java数据存储技术 JDBC介绍 JDBC体系结构 获取数据库链接 Driver接口 加载注册JDBC驱动 获取数据库链接 数据库链接方式(实例) 方式一:代码中显示出 ...

  6. 发现数据结构与算法之美的第n次重新学习 ——— 初遇数据结构与算法(了解)

    你的数据结构怎么学的?提起数据结构,计算机与软件,it行业内无人不知,无人不晓.但是,当你真正的去通过数据结构与算法内容去实践内容时,真的能联系起来吗?那肯定的 不管是考研还是做项目,数据结构都是必学 ...

  7. CSS中的颜色、长度、角度、时间

    一.颜色的表示方法 颜色是通过对红.绿和蓝光的组合来显示的. 1.颜色名 1 <!DOCTYPE html> 2 <html lang="en"> 3 &l ...

  8. 048.Python前端css

    一 CSS介绍 1.1  CSS语法 CSS 规则由两个主要的部分构成:选择器,以及一条或多条声明. selector { property: value; property: value; prop ...

  9. shell基础之shell相关概念

    一.脚本介绍 1.脚本简单地说就是一条条的文字命令(一些指令的堆积),这些文字命令是可以看到的(如可以用记事本打开查看.编辑). 常见的脚本: JavaScript(JS,前端),VBScript, ...

  10. kind:Kubernetes in Docker,单机测试 Kubernetes 群集的最佳方案

    请访问原文发布链接:https://sysin.org/article/kind/,查看最新版. 作者:gc(at)sysin.org,主页:www.sysin.org 2021.04.28 更新,k ...