标签:处理过程 分配 关注 scanner 依据 内联 加载 bsp 虚方法
晚期(运行期)优化
概述:
Java程序最初 通过解释器 进行解释执行的。
热点代码:某个方法或代码块的运行频繁
即时编译器:运行时,热点代码编译成 本地平台相关的机器码。并进行优化
衡量一款商用虚拟机优秀(虚拟机技术水平体现):
HotSpot虚拟机内的即时编译器
解释器与编译器
优点:
编译器
程序运行后,代码 => 本地代码 随时间成正比
分类
C1:Client Compiler
强制指定虚拟机运行在Client模式:-client
C2:Server Compiler
强制指定虚拟机运行在Server模式:-server
解释器
不需编译。立即执行
逆优化:编译器激进优化容灾
激进优化不成立(如加载了新类后类型继承结构出现变化,出现罕见陷阱) 可通过逆优化 回滚 解释状态继续执行
程序运行环境内存资源限制较大,使用解释执行 节约内存
程序运行环境内存资源限制较小,使用编译执行 提升效率
工作方式
混合模式:默认解释器与C1或C2编译器配合工作。
取决虚拟机运行的模式
根据自身版本 宿主机 硬件性能 自动选择运行模式
解释模式:全部 使用 解释方式 执行
强制指定虚拟机运行与解释模式:-Xint
编译模式:优先采用 编译方式 执行
解释器 在 编译无法进行 情况 介入执行过程
强制指定虚拟机运行与编译模式:-Xcomp
分层编译
为什么分层编译?
即时编译器 编译本地 代码 需占用程序运行时间
编译 优化程序更高 代码 时间长
解释器 可能替 编译器 收集性能监控信息
原理:根据编译器编译。优化的规模与耗时
包括
0层:
程序解释执行,解释器不开启性能监控,可触发第一层编译
1层:(C1编译)
将字节码编译为本地代码,进行简单,可靠的优化
如果有必要加入性能监控的逻辑
2层或2层以上(C2编译)
将字节码编译为本地代码,
启用编译耗时优化
性能监控信息进行不可靠激进优化
编译对象与触发条件
运行时即时编译器编译的热点代码
包括
被多次调用的方法
即一个方法被调用多次,
方法体内的代码执行多次
JIT编译方式
由方法调用触发的编译
编译器会以整个方法作为编译对象
被多次执行的循环体
即一个方法只被调用过一次或者几次,
但是方法体内部存在循环次数较多的循环体
栈上替换(OSR)
由循环体触发的编译,
编译器会以整个方法(而不是单独的循环体)作为编译对象
这种编译方式发生在方法执行过程之中
热点探测
判断一端代码是不是热点代码
是不是需要触发即时编译
这种行为即为热点探测
包括
基于采样的热点探测
虚拟机会周期性的检查各个线程的栈顶
如果发现某个(或某些)方法经常出现在栈顶
即为热点方法
优点
实现简单,高效
很容易获取方法调用关系(将调用堆栈展开即可)
缺点
很难精确地确认一个方法的热度
容易受到线程阻塞或别的外界因素的影响
从而扰乱热点探测
HotStop使用:
基于计数器的热点探测
虚拟机会为每个方法(甚至是代码块)建立计数器
统计方法的执行次数,
如果执行次数超过阈值
即为热点方法
优点
统计结果相对来说更加精确和严谨
缺点
实现复杂,需要为每个方法建立并维护计数器
不能 直接获取到方法的调用关系
包括
方法调用计数器
默认阈值:
Client模式:1500次
Server模式:10000次
通过-XX:CompileThreshold指定
运行流程
1.当一个方法被调用,首先检查该方法是否存在被JIT编译过的版本
2.1存在:优先使用编译后的本地代码来执行
2.2不存在,将该方法的调用计数器值+1
2.2.1判断方法调用计数器与回边计数器值之和是否超过方法调用计数器的阈值
2.2.2如超过,则向即时编译器提交该方法代码编译的请求
注“如不做任何设置。执行引擎并不会等待编译请求完成
而是继续进入解释器按照解释方式执行字节码
当提交的请求被编译器编译完成
该方法的调用入口地址会被系统自动改写成新的
下一次调用该方法时就会使用已编译的版本
方法调用计数器热度的衰减
如不做任何设置,方法调用计数器统计的并不是方法被调用的绝对次数
而是一个相对的执行频率(即一段时间内方法被调用的次数)
当超过一定时间限制。如果方法的调用次数仍然不足以让他提交给即时编译器编译
则该方法的调用计数器就会被减少一半???ā?苉
方法统计半衰周期
即方法调用计数器热度衰减的时间
关闭热度衰减
-XX:UseCounterDecay
设置半衰周期时间(单位秒)
-XX:CounterHafLifeTime
回边计数器
作用
统计一个方法中循环体代码执行的次数(回边次数)
建立回边计数器统计的目的就是为了触发OSR编译
设置回边计数器的阈值
设置方法调用计数器阈值:-XX:CompileThreshord
计算公式
虚拟机运行在Client模式
方法调用计数器阈值(CompileThreshold)×OSR比率(OnstackReplacePercentage)/100
OSR比率:默认966
回边计数器阈值:默认13995
虚拟机运行在Server模式
方法调用计数器阈值)×(OSR比率-解释器监控比率)/100
OSR比率:默认140
解释器监控比率:默认33
回边计数器阈值:默认10700
运行过程
1.当虚拟机遇到一条回边指令时,首先查找将要执行的代码片段是否有已经编译好的版本
2.1如果有,将会优先执行已编译好的代码
2.2如果没有,则把回边计数器的值加1
2.2.1判断放到调用计数器与回边计数器值之和是否超过回边计数器的阈值
2.2.2如果超过,则提交一个OSR的编译请求,并且把回边计数器的值降低
以便等待编译器输出编译结果
注:回边计数器没有技术热度衰减,因此这个计数器统计的就是该方法循环执行的绝对次数
编译过程
在默认设置下,虚拟机在代码编译器还未完成之前,都仍然将按照解释方式继续执行
而编译动作则在后台的编译线程中执行
禁止后台编译后,执行线程向虚拟机提交编译请求后将会一直等待
直到编译过程完成后在开始执行编译器输出的本地代码
设置虚拟机禁止后台编译:-XX:BackgroundCompliation
Client Compiler
简单快速的三段式编译器,
主要关注局部性的优化,
而放弃了许多耗时较长的全局优化手段
第一阶段:前端(平台独立)将字节码构造成高级中间代码表示HIR)
静态单分配代表代码值
可以使得在HIR的构造过程中和后进行的优化动作更容易实现
在此之前编译器会在字节码上完成一部分优化:方法内联,常量传播等
第二阶段:后端(平台无关)从HIR中产生一种低级中间代码表示(LIR)
在此之前会在HIR上完成一些优化,
如:空值检查消除,范围检查消除,使HIR标识达到更高效
第三阶段:后端(平台相关)使用线性扫描算法在LIR上分配寄存器,
并做窥孔优化,产生机器代码
Server Compiler
面向服务端的典型应用,为服务端的性能配置特别调整过的编译器,充分优化过的高级编译器
执行所有的经典优化动作
如:无用代码剔除,循环表达式外提,消除巩固子表达,常量传播,基本块重排序
实施与java语言特性相关的优化技术
如:范围检查消除,空值检查消除
根据解释器或Client Compiler提供的性能监控信息,进行不稳定激进优化
如:守护内敛,分支频率预测等
ServerCompiler的寄存器分配器是一个全局着色分配器,
它可以充分利用某些处理器架构上的大寄存器集合
编译优化技术
优化技术概览
优化前原始代码
内联后代码
冗余存储消除的代码
复写传播的代码
进行无用代码消除的代码
公共子表达式消除:
语言无关的经典优化技术之一
公共子表达式消除是一个普遍应用与各种编译器的经典优化技术
全局公共子表达式消除
概念
优化的范围涵盖了多个基本块
局部公共子表达式消除
概念
优化仅限于程序的基本块内
概念
如果一个表达式E已经计算过了,并且从先前的计算到现在E中的所有变量的值都没有发生变化,那么E即为公共子表达式
例
javac编译后
未做任何优化
当这段代码进入到虚拟机即时编译器后,它将进行如下优化:
编译器检测到c*b与b*c”是一样的表达式,而且在计算期间b与c的值是不变的
编译器还可能(取决于哪种虚拟机的编译器以及具体的上下文而定)进行另外一种优化:
代数化简,把表达式变为
实现
对于公共子表达式,没有必要花时间再对它进行计算,只需要直接用前面计算过的表达式结果替代E就可以了
数组范围检查消除:
语言相关的经典优化技术之一
数组边界检查消除是即时编译器中的一项语言相关的经典优化技术
原因
由于java语言中访问数组元素时,系统将会自动进行上下界的范围检查,这必定会造成性能负担。
为了安全,数组边界检查是必须做的,
但数组边界检查是否必须一次不漏的执行则是可以“商量”的事情。
实现
如编译器通过数据流分析判定数组下标的取值永远在[0,数组.length)之内,
就可以把数组的上下界检查消除
从更高的角度看,大量安全检查使编写java程序更简单,但也造成了更多的隐式开销,
对于这些隐式开销,除了尽可能把运行期检查提到编译期完成的思路之外,还可以使用隐式异常处理
虚拟机会注册一个SegmentFault信号的异常处理器(uncommon_trap()),
这样x不为空时,不会额外消耗一次对foo判空的开销。
这个过程必须从用户态转到内核态中处理,结束后再回到用户态,速度远比一次判空检查慢。
foo极少为空的时候,隐式异常优化是值得的,但假如foo经常为空的话,这样的优化反而会让程序更慢,
还好HotSpot虚拟机足够“聪明”,它会根据运行期收集到的Profile信息自动选择最优方案。
方法内联:
最重要的优化技术之一
作用
消除方法调用的成本
为其他优化手段建立良好的基础
虚方法内联问题
虚方法可能存在多余一个版本的接受者(最多在去除被final修饰的方法)
即java语言中默认的实例方法是虚方法
解决方法
类型继承关系分析(CHA)
基于整个应用程序的类型分析技术,
用于确定在目前已加载的类中,某个接口是否有多于一种的实现
某个类是否存在子类,子类是否为抽象类等信息
只有一个
守护内联
可以进行内联,不过这种内联属于激进优化,需要预留一个“逃生门”,称为守护内联(GuardedInlining)
如果程序的后续执行过程中,虚拟机一直没有加载到会令这个方法的接收者的继承关系发生变化的类,
那这个内联游湖的代码就可以一直使用下去。
否则,就需要抛弃已经编译的代码,退回到解释状态执行,或者重新进行编译
多个方法
内联缓存
工作原理
1.在未发生方法调用之前,内联缓存状态为空
2.当第一次调用发生后,缓存记录下方法接收者的版本信息
3.并且每次进行方法调用时都比较接收者版本
4.1如果以后进来的每次调用的方法接收者版本都是一样的,那这个内联还可以一直用下去。
4.2如果发生了方法接收者不一致的情况,就说明程序真正使用了虚方法的多态特性,这时才会取消内联,查找虚方法表进行方法分派
注:在许多情况下虚拟机进行的内联都是一种激进优化,激进优化的手段在高性能的商用虚拟机中很常见,
除了内联之外,
对于出现概率很小(通过经验数据或解释器收集到的性能监控信息确定概率大小)的隐式异常,
使用率很大的分支等都可以被激进优化移除
如果真的出现了小概率事件,这时才会从逃生门回到解释状态重新执行
非虚方法
直接进行内联,这时候的内联是有稳定前提保障的
逃逸分析:
最前沿的优化技术之一
作用
为其他优化手段提供依据的分析技术
基本行为
分析对象动态作用域
方法逃逸
当一个对象在方法中被定义后,他可能被外部方法引用(作为调用参数传递到其他方法中)
线程逃逸
当一个对象在方法中被定义后,他可能被外部线程访问到(赋值给类变量或可以在其他线程中访问到的实例变量)
如果一个对象不会逃逸到方法或线程外
(别的方法或者线程无法通过任何途径访问到这个对象)
则可能为这个变量进行一些高效的优化
栈上分配(Stack Allocation)
原因
Java堆中的对象对于各个线程都是共享和可见的,只要持有这个对象的引用,就可以访问堆中存储的对象数据。
虚拟机的垃圾收集系统可以回收堆中不再使用的对象,但回收动作无论是筛选可回收对象,还是回收和整理内存都需要耗费时间。
实现
将对象在栈上分配内存,这样就可以使对象所占内存空间随栈帧出栈而销毁,减小垃圾收集系统的压力
注:HotSpot虚拟机目前的实现方式导致栈上分配实现起来比较复杂,因此在HotSpot中暂时还没有做这项优化
同步消除(Synchronization Elimination)
对象无法被其他线程访问,这个变量的读写肯定不会有竞争,对这个变量实施的同步措施也就可以消除掉
标量替换(Scalar Replacement)
标量
指一个数据已经无法再分解成更小的数据来表示
例
Java虚拟机中的原始数据类型(int、long等数值类型以及reference类型等)
聚合量
如果一个数据可以继续分解
概念
把一个Java对象拆散,根据程序访问的情况,将其使用到的成员变量恢复原始类型来访问
实现
如果逃逸分析证明一个对象不会被外部访问,并且这个对象可以被拆散的话,
那程序真正执行的时候将可能不创建这个对象,而改为直接创建它的若干个被这个方法使用到的成员变量来代替。
优点
可以让对象的成员变量在栈上(栈上存储的数据,有很大的概率会被虚拟机分配至物理机器的高速寄存器中存储)分配和读写
可以为后续进一步的优化手段创建条件
缺点
主要是不能保证逃逸分析的性能收益必定高于它的消耗。
如果要完全准确地判断一个对象是否会逃逸,需要进行数据流敏感的一系列复杂分析,从而确定程序各个分支执行时对此对象的影响。 这是一个相对高耗时
的过程
如果分析完后发现没有几个不逃逸的对象,那这些运行期耗用的时间就白白浪费了
JVM设置参数
开启逃逸分析:-XX:+DoEscapeAnalysis
查看分析结果:-XX:+PrintEscapeAnalysis
开启标量替换:-XX:+EliminateAllocations
查看标量的替换情况:-XX:+PrintEliminateAllocations
开启同步消除:+XX:+EliminateLocks
java与C/C++编译器对比
java:即时编译期
1.即时编译器运行时占用的是用户程序的运行时间,因此即时编译器不敢随便引入大规模的优化技术
2.java语言是动态的类型安全语言,这就意味着虚拟机必须频繁地进行安全检查
3.java语言中虚方法的使用频率远远大于C/C++语言,
导致即时编译器在进行一些优化时的难度要远大于C/C++的静态优化编译器
4.java语言时可以动态扩展的语言,运行时加载新的类可能改变程序类型的继承关系,
导致许多全局的优化措施都只能以激进优化的方式来完成
5.java虚拟机中对象的内存分配都是在堆上进行的
C++:静态编译器
1.编译的时间成本在静态优化编译器中并不是主要的关注点
2.C/C++的对象则有多种分配方式,而且C/C++中主要由用户程序代码来回收分配的内存,
因此运行效率上比垃圾收集机制要高
java语言相对C/C++的劣势都是为了换取开发效率上的优势而付出的代价,
而且还有许多优化是java的即时编译器能做而C/C++的静态优化编译器不能做或者不好做的,
如别名分析、调用频率预测、分支频率预测、裁剪为被选择的分支等
标签:处理过程 分配 关注 scanner 依据 内联 加载 bsp 虚方法
原文地址:https://www.cnblogs.com/lllllht/p/9184343.html