深入理解java虚拟机笔记Chapter11
运行期优化
即时编译
什么是即时编译?
- 当虚拟机发现某个方法或某段代码运行的特别频繁时,会把这段代码认为成热点代码;
- 在运行时,虚拟机会将这段代码编译成平台相关的机器码,并进行各种层次的优化。
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 查询此方法在当前程序下是否有多个目标可选择;
- 只有一个目标版本:
- 先对这唯一的目标进行内联;
- 如果之后的执行中,虚拟机没有加载到会令这个方法接收者的继承关系发生改变的新类,则该内联代码可以一直使用;
- 如果加载到导致继承关系发生变化的新类,就抛弃已编译的代码。
- 有多个目标版本:
- 使用内联缓存,未发生方法调用前,内联缓存为空;
- 第一次调用发生后,记录调用方法的对象的版本信息;
- 之后的每次调用都要先与内联缓存中的对象版本信息进行比较;
- 版本信息一样,继续使用内联代码;
- 版本信息不一样,说明程序使用了虚方法的多态特性,这时取消内联,查找虚方法进行方法分派。
- 只有一个目标版本:
- 向 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;
}
第四步优化: 无用代码消除
无用代码:
- 永远不会执行的代码
- 完全没有意义的代码
public void foo() {
y = b.value;
// ...do stuff...
// y = y; 这句没有意义的,去除
sum = y + y;
}
深入理解java虚拟机笔记Chapter11的更多相关文章
- Java内存区域与内存溢出异常——深入理解Java虚拟机 笔记一
Java内存区域 对比与C和C++,Java程序员不需要时时刻刻在意对象的创建和删除过程造成的内存溢出.内存泄露等问题,Java虚拟机很好地帮助我们解决了内存管理的问题,但深入理解Java内存区域,有 ...
- 深入理解java虚拟机笔记Chapter12
(本节笔记的线程收录在线程/并发相关的笔记中,未在此处提及) Java内存模型 Java 内存模型主要由以下三部分构成:1 个主内存.n 个线程.n 个工作内存(与线程一一对应) 主内存与工作内存 J ...
- 深入理解Java虚拟机笔记
1. Java虚拟机所管理的内存 2. 对象创建过程 3. GC收集 4. HotSpot算法的实现 5. 垃圾收集器 6. 对象分配内存与回收细节 7. 类文件结构 8. 虚拟机类加载机制 9.类加 ...
- 深入理解java虚拟机笔记Chapter7
虚拟机类的加载机制 概述 虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验.转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这就是虚拟机的类的加载机制. 类加载的时机 J ...
- 深入理解java虚拟机笔记之一
Java的技术体系主要有支撑java程序运行的虚拟机,提供各开发领域接口支持Java API,java编程语言及许多第三方java框架( 如Spring,Structs等)构成. 可以把Java程序设 ...
- 深入理解Java虚拟机笔记——虚拟机类加载机制
目录 概述 动态加载和动态连接 类加载的时机 类的生命周期 被动引用 例子一(调用子类继承父类的字段) 例子二(数组) 例子三(静态常量) 类加载的过程 加载 验证 准备 解析 符号引用 直接引用 初 ...
- 【转载】深入理解Java虚拟机笔记---运行时栈帧结构
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈(Virtual Machine Stack)的栈元素.栈帧存储了方法的局部变量表,操作 ...
- 深入理解java虚拟机笔记Chapter8
运行时栈帧结构 栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区中的虚拟机栈(Virtual Machine Stack)的栈元素.栈帧存储了方法 ...
- 深入理解java虚拟机笔记Chapter2
java虚拟机运行时数据区 首先获取一个直观的认识: 程序计数器 线程私有.各条线程之间计数器互不影响,独立存储. 当前线程所执行的字节码行号指示器.字节码解释器工作时通过改变这个计数器值选取下一条需 ...
随机推荐
- HR:“最喜欢阿里出来的程序员了,技术又好又耐艹!” 我:???
面试造火箭,进厂拧螺丝?真的是这样吗? 缘起 估计不少同学都是被标题吸引进来的.事先声明,这句话不是我虚构的,而是我实实在在从同事的口中听到的,而且还不止一次. 当时的场景就是很正常的交谈,别人也并没 ...
- 【python】Leetcode每日一题-颠倒二进制位
[python]Leetcode每日一题-颠倒二进制位 [题目描述] 颠倒给定的 32 位无符号整数的二进制位. 示例1: 输入: 00000010100101000001111010011100 输 ...
- Jenkins+Git的搭建和自动部署
前言 Jenkins在工作中都使用过,之前都是运维去搭建部署,弄好了之后给我一个网址去构建项目就可以了,所以也都是一直没了解过安装过程. 今天在自己的服务器上搭建了一遍,中间有遇到很多坑,特在此归纳总 ...
- windows的SEH异常处理以及顶层异常处理
前言 windows的SEH结构化异常处理是基于线程的,传统的SEH结构化异常会基于堆栈形成一条包含异常回调函数地址的链(SEH链).而fs:[0](TEB的第一个字段)指向这条链的链头,当有异常发生 ...
- Codeforces Round #688 (Div. 2)
A. Cancel the Trains 题意:给定两个数组,找出这两个数组中有多少重复元素,然后输出 思路:直接找 代码: 1 #include<iostream> 2 #include ...
- Codeforces Round #660 (Div. 2)
A. Captain Flint and Crew Recruitment 题意:定义了一种数(接近质数),这种数可以写成p*q并且p和q都是素数,问n是否可以写成四个不同的数的和,并且保证至少三个数 ...
- [JavaScript之BOM与DOM]
[JavaScript之BOM与DOM] BOM(Browser Object Model)是指浏览器对象模型,它使 JavaScript 有能力与浏览器进行"对话". DOM ( ...
- [bug] python3 pip 安装 MarkupSafe==1.0 失败:ImportError:cannot import name 'Feature' from 'setpools'
解决 先升级pip到最新版本 python -m pip install --upgrade pip 再升级setuptools pip install --upgrade pip setuptool ...
- [Linux] Linux命令行与Shell脚本编程大全 Part.1
终端 tty(teletypewriters):控制台,早期计算机通过电传打字机作为输入设备 Console:控制台终端,即显示器 Ctrl+Alt+T:图形界面终端 Ctrl+Alt+F2:tty2 ...
- ansible-一键完成LNMP架构_期中架构
ansible-一键完成LNMP架构 ansible剧本托管地址 https://github.com/Gshelldong/ansible.git 网站架构图 ansible一键完成lnmp架构 a ...