小视频做网站怎么赚钱,专业企业app开发制作,花都区网站建设,郑州前端培训机构【0】README
0.1#xff09;本文部分文字描述转自 “深入理解 jvm”#xff0c;旨在学习 晚期#xff08;运行期#xff09;优化 的基础知识#xff1b;
【1】概述 1#xff09;即时编译器#xff08;JITjust in time compiler#xff09;定义#xff1a;为了提高…【0】README
0.1本文部分文字描述转自 “深入理解 jvm”旨在学习 晚期运行期优化 的基础知识
【1】概述
1即时编译器JITjust in time compiler定义为了提高热点代码的执行效率在运行时虚拟机将把这些代码编译成与 本地平台相关的机器码并进行各种层次的优化完成这个任务的编译器称为即时编译器2热点代码当虚拟机发现某个方法或代码块运行特别频繁就会把这些代码认定为热点代码干货——即时编译器和热点代码的定义Attention本文提及的编译器即时编译器都是指HotSpot 虚拟机内的即时编译器虚拟机也是特指HotSpot虚拟机【2】 HotSpot 虚拟机内的即时编译器0我们需要解决以下问题problem1为何HotSpot虚拟机要使用解释器与编译器并存的架构problem2为何HotSpot虚拟机要实现两个不同的即时编译器problem3程序何时使用解释器执行何时使用编译器执行problem4哪些程序代码会被编译为本地代码 如何编译为本地代码problem5如何从外部观察即时编译器的编译过程和编译结果 【2.1】解释器与编译器1解释器与编译器各有优势1.1解释器的优势当程序需要迅速启动和执行的时候解释器可以首先发挥作用省去编译的时间立即执行1.2编译器的优势在程序运行后随着时间的推移编译器逐渐发挥作用把越来越多的代码编译成本地代码之后可以获得更高的执行效率 2HotSpot虚拟机中内置了两个即时编译器分别为Client Compiler 和 Server Compiler 或简称为C1 和 C2 编译器用于可以使用 -client 或 -server 参数去强制指定虚拟机运行在Client 模式或 Server模式3混合模式解释器和编译器搭配使用的方式在虚拟机中叫做 混合模式干货——虚拟机运行模式包括混合模式解释模式编译模式3.1解释模式使用参数 -Xint 强制虚拟机运行在解释模式 这时编译器完全不介入工作3.2编译模式使用参数 -Xcomp 强制虚拟机运行在编译模式 这时将优先采用编译方式执行程序但解释器仍然要求在编译无法进行的情况下介入执行过程3.3通过虚拟机的 -version 命令的输出结果显示出这3种模式 4分层编译为了在程序启动相应速度与运行效率之间达到最佳平衡HotSpot 虚拟机还会逐渐启用分层编译的策略分层编译工具编译器编译优化的规模与耗时划分出不同的编译层次包括4.1第0层程序解释执行解释器不开启性能监控功能可触发第1层编译。4.2第1层也成为C1编译将字节码编译为本地代码进行简单、可靠的优化如有必要时将加入性能监控的逻辑。4.3第2层或2层以上也成为C2编译也是将字节码编译为本地代码但是会启动一些编译耗时较长的优化甚至会根据性能监控信息进行一些不可靠的激进优化。 4.4事实上 用Client Compiler获取更高的编译速度用Server Compiler来获取更好的编译质量在解释执行的时候也无须再承担收集性能监控信息的任务。干货——Client Compiler 和 Server Compiler的用途 【2.2】编译对象与触发条件1在运行过程中会被即时编译器编译的热点代码有两类即干货——引入热点代码1.1被多次调用的方法1.2被多次执行的循环体 2JIT编译方式和OSR编译方式干货——JIT编译方式和OSR编译方式2.1JIT编译方式对于第一种情况由于是由方法调用触发的编译因此编译器会以整个方法作为编译对象这种编译也是虚拟机中标准的JIT编译方式2.2OSR编译方式尽管编译动作是由循环体所触发的但编译器依然会以整个方法作为编译对象这种编译方式因为编译发生在方法执行过程之中因此形象地被称为栈上替换简称为OSR编译即方法栈帧还在栈上方法就被替换了。3触发条件3.1问题一被多次调用在这里的多次具体是多少次3.2问题二并且虚拟机如何统计一个方法或一段代码被执行过多少次 3.3解决了以上问题就回答了即时编译器被触发的条件 4热点探测4.1定义判断一段代码是不是热点代码是不是需要触发即时编译这样的行为称为热点探测4.2主要的热点探测判断方式method有两种如下干货——热点探测判断方式methodmethod1基于采样的热点探测虚拟机会周期性地检查各种线程的栈顶如果发现某个或者某些方法经常出现在栈顶那这个方法就是“热点方法”。 优点实现简单、高效还可以很容易地获取方法调用方法。 缺点很难精确地确认一个方法的热度因为容易受到线程阻塞或别的外界因素的影响而扰乱热点探测。 method2基于计数器的热点探测虚拟机会为每个方法建立计数器统计方法的执行次数如果执行次数超过一定的阙值就认为它是热点方法。 优点统计结构相对来说更加精确与严谨。 缺点实现起来麻烦需要为每个方法及建立并维护计数器而且不能直接获取到方法的调用关系。 5在HotSpot中使用的是第二种方法基于计数器的热点探测法因此它为每个方法准备了两类计数器方法调用计数器和回边计数器统计一个方法中循环体代码执行的次数在字节码中中遇到控制流向后跳转的指令称为“回边”。建立回边计数器的目的是为了触发OSR编译。。干货——引入方法调用计数器和回边计数器5.1方法调用计数器统计方法被调用次数5.1.1定义这个计数器就用于统计方法被调用的次数他的默认阈值在 Client模式下是1500次在 Server模式下是10000次可以通过 虚拟机参数 -XX: CompileThreshold 来认为设定AttentionJIT编译的交互过程 A1当一个方法被调用时会先检查该方法是否存在被JIT 编译过的版本如果存在则优先使用编译后的本地代码来执行。如果不存在已被编译过的版本则将此方法的调用计数器值加1然后判断方法调用计数器与回边计数器值之和是否超过方法计数器的阈值。若超过了则将会向即时编译器提交一个该方法的代码编译请求A2如果不做任何设置执行引擎并不会同步等待编译请求完成而是继续进入解释器按照解释方式执行字节码直到提交的请求被编译器编译完成。当编译工作完成之后这个方法的调用入口地址就会被系统自动改写成新的下一次调用该方法时就会使用已变异的版本整个JIT 编译的交互过程如下图所示 5.2回边计数器统计方法循环体代码执行次数5.2.1作用是统计一个方法中循环体代码执行的次数在字节码中遇到控制流向后跳转的指令称为”回边“显然建立回边计数器统计的目的是为了触发OSR 编译栈上替换编译干货——回边计数器的作用5.2.2设置参数 -XX:OnStackReplacePercentage 来简介调整回边计数器的阈值回边计数器的阈值的计算公式如下 case1虚拟机运行在Client模式下阈值方法调用计数器阈值*OSR比率/100case2虚拟机运行在Server模式下阈值方法调用计数器阈值*OSR比率-解释器监控比率/100 5.2.3回边计数器触发即时编译的过程当解释器遇到一条回边指令时会先查找将要执行的代码片段是否有已经编译好的version如果有他将会优先执行已编译的代码否则就把回边计数器的值加1然后判断方法调用计数器与回边计数器之和是否超过回边计数器的阈值。当超过阈值的时候将会提交一个OSR编译请求并且把回边计数器的值降低一些以便继续在解释器中执行循环等待编译器输出编译结果整个执行过程如下图所示 Attention图11-2 和 图11-3都仅仅描述了 Client VM 的即时编译技术对于Server VM来说case 或许还更加复杂 【2.3】编译过程1无论是方法调用产生的即时编译请求还是OSR编译请求虚拟机在代码编译器还未完成之前都将按照解释方式继续执行而编译动作则在后台的编译线程中进行2在后台执行编译的过程中编译器做了什么事情呢3对于Client Compiler来说它是一个简单快速的三段式编译器干货——Client 编译器的编译过程step1在第一个阶段一个平台独立的前端将字节码构造成一种高级中间代表表示High Level Intermediate Representation,HIR。step2在第二个阶段一个平台相关的后端从 HIR中产生低级中间代码表示Low Level Intermediate Representation, LIRstep3在第三个阶段最后是在平台相关的后端使用线性扫描算法在LIR 上分配寄存器并在LIR上做窥孔优化然后产生机器代码Conclusion整个 Client Compiler 的大致执行过程如下图所示 4Server Compiler该编译器则是专门面向服务端的典型应用并为服务端的性能配置特别调整过的编译器也是充分优化过的高级编译器几乎能达到GNU C 编译器使用 -O2参数时的优化强度【2.4】查看及分析即时编译结果如何从外部观察虚拟机的即时编译行为1看个荔枝// 测试JIT代码优化过程
public class JITCompileResultTest {public static final int NUM 15000;public static int doubleValue(int i) {// 这个空循环用于后面演示 JIT代码优化过程for(int j0; j010000; j);return i * 2;}public static long calculateSum() {long sum 0;for (int i 0; i 100; i) {sum doubleValue(i);}return sum;}public static void main(String[] args) {for (int i 0; i NUM; i) {calculateSum();}}
} 对上述代码的分析Analysis 使用参数 -XX: PrintCompilation 要求虚拟机在即时编译时将被编译成本地代码的方法名称打印出来此处有文字省略 【3】编译优化技术0java程序员有一个共识以编译方式执行本地代码比解释方式更快之所以有这样的共识除去虚拟机解释执行字节码时额外消耗时间的原因外还有一个重要的原因是 虚拟机设计团队几乎把对代码的所有优化措施都集中在了即时编译器中。所以编译器产生的本地代码会比Javac 产生的字节码更加优秀干货——编译器产生的本地代码会比Javac 产生的字节码更加优秀【3.1】优化技术概览1看个荔枝1.0源代码step1优化第一步进行方法内联主要目的有两个一是去除方法调用的成本二是为其他优化建立良好的基础方法内联膨胀后可以便于在更大范围上采取后续的优化手段 step2优化第二步进行冗余访问消除看成是公共子表达式的消除 step3复写传播 因为在这段程序的逻辑中并没有必要使用一个额外的变量z它与变量y 是完全相等的因此可以使用 y 来代替 step4无用代码消除无用代码可能是永远不会被执行的代码也可能是完全没有意义的代码因此它又形象地称为 Dead Code Conclusion我们将继续查看如下的几项最具有代表性的优化技术是如何运作的分别是干货——几项最具有代表性的编译优化技术 C1公共子表达式消除C2数组范围检查消除C3方法内联C4逃逸分析 【3.2】公共子表达式消除 1定义如果一个表达式E 已经计算过了并且从先前的计算到现在E中所有变量的值都没有发生变化那么E 的这次出现就称为了公共子表达式 2看个荔枝 2.1如下表达式 int d (c*b)*12 a (ab*c); 2.2当这段代码进入到虚拟机即时编译器后它将进行如下优化编译器检测到 c*b 与 b*c 是一样的表达式且在计算期间b与c的值不会改变故这条表达式就可能被视为 int d E * 12 a (aE); 【3.3】 数组边界检查 1定义在java语言中访问数组元素的时候系统将会自动进行上下界的范围检查即检查i 必须满足 大于等于0且小于数组长度否则抛出一个运行时异常 java.lang.ArrayIndexOutOfBoundsException 2如果编译器只要通过数据流分析就可以判定循环变量的取值范围永远在区间 [0,foo.length]内那在整个循环中就可以吧数组的上下界检查消除 3problemsolution problem在安全检查方面java要比C 或C做更多的事情这些事情成为了一种隐式开销如果处理不好它们就很可能成为一个 java语言比 C/C 更慢的因素solution要消除这些隐式开销其中之一还要避免思路——隐式异常处理java中空指针检查和算术运算中除数为零的检查都采用了这种思路干货——隐式开销隐式异常处理的概念 4隐式开销的荔枝 // 隐式异常处理
public class ImplicitExceptionHandle {static class B {int value;}// 隐式异常优化之前的java伪代码beforepublic int before_optimize() {B b new B();if(b ! null) {return b.value;} else {throw new NullPointerException();}}// 隐式异常优化之后的java伪代码afterpublic int after_optimize() {try {B b new B();return b.value;}catch(segment_fault) {uncommon_trap();} }
} 【3.4】方法内联 1定义
把目标方法的代码“复制”到发起调用的方法之中避免发生真实的方法调用。干货——方法内联定义
2如果不是即时编译器做了一些特别的努力按照经典编译原理的优化理论大多数的java方法都无法进行内联2.1无法内联的原因只有使用 invokespecial 指令调用的私有方法 实例构造器父类方法以及使用invokestaitc指令进行调用的静态方法才是在编译期进行解析的除了上述4种方法外其他的java方法调用都需要在运行时进行方法接收者的多态选择并且都有可能存在多于一个版本的方法接收者简而言之java语言中默认的实例方法是虚方法2.2对于一个虚方法 编译期做内联的时候根本无法确定应该使用哪个方法版本 补充-Complementary虚方法非虚方法与虚方法干货——虚方法与非虚方法定义 C1非虚方法只要能被invokestatic 和 invokespecial 指令调用的方法都可以在解析阶段中确定唯一的调用版本符合这个条件的有静态方法私有方法实例构造器父类方法4类他们在类加载的时候就会把符号引用解析为对该方法的直接引用。这些方法称为非虚方法C2虚方法其他的方法称为虚方法除去final方法 3解决虚方法的内联问题 对上图的分析Analysis A1CHA类型继承关系分析Class Hierarchy Analysis, CHA这是一种基于整个应用程序的类型分析技术它用于确定在目前已经加载的类中某个接口是否有多于一种的实现某个类是否存在子类子类是否为抽象类等信息A2方法内联Inline Cache工作原理是在未发生方法调用之前内联缓存状态为空当第一次调用发生后缓存记录下方法接收者的版本信息并且每次进行方法调用时都比较接收者版本如果以后进来的每次调用的方法接收者版本都是一样的那这个内联还可以一直用下去。如果发生了方法接收者不一致的 case 就说明程序真正使用了虚方法的多态特性这时才会取消内联查找虚方法表进行方法分派 【3.5】逃逸分析 1方法逃逸线程逃逸 干货——逃逸分析包括方法逃逸线程逃逸 1.1方法逃逸分析对象的动态作用域当一个对象在方法中被定义后它可能被外部方法所引用例如作为调用参数传递给其他方法称为方法逃逸1.2线程逃逸甚至还有可能被外部线程访问到比如赋值给类变量或可以在其他线程中访问到的实例变量称为线程逃逸 2如果能证明一个对象不会逃逸到方法或线程之外即别的方法或线程无法通过任何途径访问到这个对象则可能为这个变量进行一些高效的优化如下所示干货——保证对象不逃逸到方法或线程之外就可以进行高效优化 2.1栈上分配如果确定一个对象不会逃逸出方法之外那让这个对象在栈上分配内存将是一个不错的主意对象所占用的内存空间就可以随栈帧出栈而销毁2.2同步消除线程同步本身是一个相对耗时的过程如果逃逸分析能够确定一个变量不会逃逸出线程那这个变量的读写肯定不会有竞争对这个变量实施的同步措施就可以消除掉2.3标量替换 2.3.1标量定义它是指一个数据已经无法再分解成更小的数据来表示了java虚拟机中的原始数据类型都不能再进一步分解他们就可以成为标量2.3.2聚合量定义如果一个数据可以继续分解它被称为聚合量2.3.3标量替换如果把一个java对象拆散根据程序访问的情况将其使用到的成员变量恢复原始类型来访问就叫做标量替换那程序真正执行的时候将可能不创建这个对象而改为直接创建它的若干个被这个方法使用到的成员变量来代替。 Attention A1逃逸分析技术还不成熟不推荐使用A2不成熟的原因无法保证逃逸分析的性能收益必定高于它的消耗 3如果有需要并确认对程序运行有益用户可以手动开启逃逸分析3.1用户使用参数 -XX: DoEscapeAnalysis 来手动开启逃逸分析3.2开启之后可以通过参数 -XX: PrintEscapeAnalysis 来查看分析结果3.3有了逃逸分析支持后用户可以使用参数 -XX: EliminateAllocations 来开启标量替换3.4使用 XX:EliminateLocks 来开启同步消除3.5使用参数 -XXPrintElimianteAllocations 查看标量的替换情况Attention尽管目前逃逸分析的技术不是很成熟但它却是即时编译器优化技术的一个重要发展方向在今后的虚拟机中逃逸分析技术肯定会支撑起一些列实用有效的优化技术干货——逃逸分析技术前途无量 【4】java 与 C/C 的编译器对比Comparison0java虚拟机的即时编译器与C/C 的静态优化编译器相比会由于以下原因而导致输出的本地代码有一些劣势C1因为即时编译器运行占用的是用户程序的运行时间具有很大的时间压力它能提供的优化手段严重受制于编译成本 C2java语言是动态的类型安全语言这就意味着需要由虚拟机来确保程序不会违反语言语义后访问非结构化内存。从实现层面上来看这就意味着虚拟机必须频繁地进行动态检查如实例方法访问时检查空指针数组元素访问时检查上下界访问类型转换时检查基础关系等。总体上仍然要消耗不少的时间C3java语言中没有virtual关键字但使用虚方法的频率却远远大于 C 或 C这意味着运行时对方法接收者进行多态选择的频率要远远大于C或C也意味着 即时编译器在进行一些优化时的难度要远远大于C 或 C 的静态优化编译器C4java语言是可以动态扩展的语言运行时加载新的类可能改变程序类型的继承关系 这使得很多全局的优化都难以进行因为编译器无法看见程序的全貌许多全局的优化措施都只能以激进优化的方式来完成编译器不得不时刻注意并随着类型的变化而在运行时撤销或重新进行一些优化C5java语言中对象的内存分配都是在堆上进行的只有方法中的局部变量才能在栈上分配而C 或 C的对象则有多种内存分配方式既可能在堆上分配也可能在栈上分配如果可以在栈上分配线程私有的对象将减轻内存回收的压力另外C或C中主要由用户程序代码来回收分配的内存这就不存在无用对象筛选的过程因此在效率上也比垃圾收集机制要高 Conclusion以上java的劣势并不是说 java 不如 C或C了C1java 语言上的这些性能上的劣势都是为了换取开发效率上的优势而付出的代价动态安全 动态扩展 垃圾回收这些拖后腿的特性都为 java 的开发效率做出了贡献C2许多优化时 Java的即时编译器能做而C 或 C的静态优化编译器不能做或做的不好的C3java编译器另外一个红利是由它的动态性带来的 由于C或C编译器所有优化都在编译器完成以运行期性能监控为基础的优化措施它都无法进行