Java编译期与运行期优化探究

一:  即时编译器优化技术一览 



1.编译器策略(compiler tactics)
延迟编译(delayed compilation)
分层编译(tiered compilation)
栈上替换(on-stack replacement)
延迟优化(delayed reoptimization)
静态单赋值表示(static single assignment representation)


2.基于性能监控的优化技术(profile-based techniques)
乐观空值断言(optimistic nullnuess assertions)
乐观类型断言(optimistic type assertions)
乐观类型增强(optimistic type strengthening)
乐观数组长度增强(optimistic array length strengthening)
裁剪未被选择的分支(untaken branch pruning)
乐观的多态内联(optimistic N-morphic inlining)
分支频率预测(branch frequency prediction)
调用频率预测(call frequency prediction)


3.基于证据的优化技术(proof-based techniques)
精确类型推断(exact type inference)
内存值推断(memory value inference)
内存值跟踪(memory value tracking)
常量折叠(constant folding)
重组(reassociation)
操作符退化(operator strength reduction)
空值检查消除(null check elimination)
类型检测退化(type test strength reduction)
类型检测消除(type test climination)
代数简化(algebraic simplification)
公共子表达式消除(common subexpression elimination)


4.数据流敏感重写(flow-sensitive rewrites)
条件常量传播(conditional constant propagation)
基于流承载的类型缩减转换(flow-carried type narrowing)
无用代码消除(dead code elimination)


5.语言相关的优化技术(language-specific techniques)
类型继承关系分析(class hicrarchy analysis)
去虚拟化(devirtualization)
符号常量传播(symbolic constant propagation)
自动装箱消除(autobox elimination)
逃逸分析(escape analysis)
锁消除(lock elision)
锁膨胀(lock coarsening)
消除反射(de-reflection)


6.内存及代码位置变换(memory and placement transformation)
表达式提升(expression hoisting)
表达式下沉(expression sinking)
冗余存储消除(redundant store elimination)
相邻存储合并(adjacent store fusion)
交汇点分离(merge-point splitting)


7.循环变换(loop transformations)
循环展开(loop unrolling)
循环剥离(loop peeling)
安全点消除(safepoint elimination)
迭代范围分离(iteration range splitting)
范围检查消除(range check elimination)
循环向量化(loop vectorization)


8.全局代码调整(global code shaping)
内联(inlining)
全局代码外提(global code motion)
基于热度的代码布局(heat-based code layout)
Switch调整(switch balancing)


9.控制流图变换(control flow graph transformation)
本地代码编排(local code scheduling)
本地代码封包(local code bundling)
延迟槽填充(delay slot filling)
着色图寄存器分配(graph-coloring register allocation)
线性扫描寄存器分配(linear scan register allocation)
复写聚合(copy coalescing)
常量分裂(constant splitting)
复写移除(copy removal)
地址模式匹配(address mode matching)
指令窥空优化(instruction peepholing)
基于确定有限状态机的代码生成(DFA-based code generator)


第二:   Java语言的"编译期" 浅析

Java语言的“编译期”是一段不确定的过程,因为它可能指的是前端编译器把java文件转变成class字节码文件的过程,也可能指的是虚拟机后端运行期间编译器(JIT)把字节码转变成机器码的过程。 


编译期优化指的是javac编译器将java文件转化为字节码的过程,而运行期间优化指的是JIT编译器所做的优化


编译期优化:


虚拟机设计团队把对性能的优化集中到了后端的即时编译器(JIT)中,这样可以让那些不是由javac编译器产生的class文件也同样能享受到编译器优化所带来的好处。但是javac做了许多针对编码过程的优化措施来改善程序员的编码风格和提高编码效率。许多新生的java语法特性,都是靠编译器的“语法糖”来实现,而不是依赖虚拟机的底层改进来支持。所以说,java中即时编译器在运行期间的优化过程对于程序的运行来说更重要,而前端编译器在编译期的优化过程对于程序编码来说关系更加密切。

javac编译器的编译过程大致可分为三个步骤:

1.解析与填充符号表过程;

2.插入式注解处理器的注解处理过程;

3.语义分析与字节码生成过程。

下面分别来介绍。

解析与填充符号表;

解析步骤包含了词法分析和语法分析两个过程,首先词法分析是将源代码的字符流转变成为标记集合(token),然后语法分析是根据token序列来构造抽象语法树(一种用来描述程序代码语法结构的树状表示方式)。完成词法分析和语法分析之后,下一步是填充符号表,符号表是由一组符号地址和符号信息构成的表格,符号表中所登记的信息在编译的不同阶段都要用到(比如语义分析中符号表所登记的内容将用于语义检查和产生中间代码,目标代码生成阶段当对符号名进行地址分配时,符号表是地址分配的依据)。

插入式注解处理器的注解处理过程:

插入式注解处理器可以看做是一组编译器的插件,在这些插件里面,可以读取、修改、添加抽象语法树中的任意元素。如果这些插件在处理注解期间对语法树进行了修改,那么编译器将 回到解析及填充符号表的过程重新处理,直到所有的插入式注解处理器都没有再对语法树进行修改为止。

语义分析与字节码生成过程:

语法分析之后,编译器获得了程序代码的抽象语法树表示,语法树能够表示结构正确的源程序的抽象,但是无法保证源程序是否符合逻辑,而语义分析主要是对结构上正确的源程序进行上下文有关性质的检查。

1.标注检查

    标注检查步骤检查的内容包括诸如变量使用前是否已被声明、变量与赋值之间的数据类型是否能够匹配,等等。还有一个重要的动作称为常量折叠也在此阶段完成。

2.数据及控制流分析

      数据及控制流分析是对程序上下文逻辑更进一步的验证,它可以检查出诸如程序局部变量在使用前是否有赋值、方法的每条路径是否有返回值、是否所有的受查异常都被正确处理了等问题。

3.解语法糖

        语法糖是指在计算机语言中添加某种语法,这种语法对语言的功能并没有影响,但是更方便程序员使用。java中的泛型,变长参数,自动拆箱与装箱,条件编译等就属于语法糖,它们在编译阶段就被还原成简单的语法结构(比如List和List在运行期间其实是同一个类)。

4.字节码生成

此过程是javac编译过程的最后一个阶段,字节码生成阶段将之前各个步骤所生成的信息转化成字节码写到磁盘中,另外还进行少量的代码添加和转换工作。


Java编译期与运行期优化探究_第1张图片

运行期优化:


在部分商用虚拟机中,java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块运行特别频繁,就会把这些代码认定为“热点代码”,为了提高热点代码的执行效率,在运行时,虚拟机就会把这些代码编译成与本地平台 相关的机器码,并进行各种层次的优化,完成这个任务的编译器称为即时编译器或JIT编译器。

即时编译器并不是虚拟机必须的部分,但是即时编译器编译性能的好坏、代码优化程度的高低确是衡量一款商用虚拟机优秀与否的最关键的指标之一。

众多主流的虚拟机都同时包含解释器和JIT编译器,解释器与JIT编译器各有优势:当程序需要迅速启动和执行时,解释器可以首先发挥作用,省去编译的时间,立即执行。当程序运行后,随着事件的推移,JIT编译器逐渐发挥作用,把越来越多的代码编译成本地代码之后,可以获取更高的执行效率。



     Java编译期与运行期优化探究_第2张图片



会被即时编译器编译的热点代码有两类:


1.被多次调用的方法体;

2.被多次调用的循环体。

即时编译器会以整个方法作为编译对象,将其编译成机器码。这种编译方式因为编译发生在方法执行过程之中,因此被称作栈上替换(OSR)。

判断一段代码是否是热点代码的方式(热点探测)有两种:

1.基于采样的热点探测:

此方法会周期性检查各个线程的栈顶,如果发现某个或某些方法经常出现在栈顶,那么这个方法就是热点方法。此方法的缺点是很难精确地确认一个方法的热度,容易受到诸如线程阻塞等因素影响。

2.基于计数器的热点探测:

此方法会为每个方法甚至是代码块建立计数器,统计方法的执行次数,如果执行次数超过一个阀值就认为它是热点方法。

注:默认设置下,执行引擎并不会同步等待编译请求完成,而是继续进入解释器按照解释方式执行字节码,直到提交的请求被编译器编译完成。当编译工作完成之后,这个方法的调用入口地址就会被系统自动改写成新的地址,下一次调用该方法时就会使用已编译的版本。也就是说,在编译器还未完成之前,执行引擎仍按照解释方式继续执行,而编译动作则在后台的编译线程中进行。

优化技术:

一般来说即时编译器所产生的本地代码会比javac产生的字节码更优秀。即时编译器采用了一系列的技术来优化代码,比如公共子表达式消除,数组范围内检查消除,方法内联,逃逸分析等。


小案例一:


Java编译器的两点优化:
1.对于short\byte\char三种类型,如果右侧赋值的数值没有超过范围,那么javac编译器将会自动隐含地为我们补上一个(short)(byte)(char)。如果超过了范围,编译器会自动报错。

2.在给变量进行赋值的时候,如果右侧的表达式中全都是常量,没有任何变量,那么编译器javac将会直接计算若干个常量表达式,得到结果。例如:short result = 3+5;编译之后得到的.class字节码文件中直接就是:short result = 13;右侧的常量结果数值没有超过左侧范围,所以正确。这称为“编译器的常量优化”。但是,一旦表达式中有变量参与,就不能进行这种优化了。


用volatile关键字防止变量被编译器优化


浅析Java关键字volatile:


一,什么是volatile关键字,作用是什么


​ volatile是java虚拟机提供的轻量级同步机制

​ 作用是:

    1.保证可见性

    2.禁止指令重排

    3.不保证原子性


二,什么是JMM



​ JMM(java 内存模型 Java Memory Model 简称JMM) 本身是一个抽象的概念,并不在内存中真实存在的,它描述的是一组规范或者规则,通过这组规范定义了程序中各个变量(实例字段,静态字段和构成数组对象的元素)的访问方式.

JMM的同步规定:
​ 1.线程解锁之前,必须把共享变量刷新回主存

​ 2.线程加锁锁之前,必须读取主存的最新值到自己的工作空间

​ 3.加锁解锁必须是 同一把锁

​ 由于 JMM运行程序的实体是线程.而每个线程创建时JMM都会为其创建一个自己的工作内存(栈空间),工作内存是每个线程的私有 数据区域.而java内存模型中规定所有的变量都存储在主内存中,主内存是共享内存区域,所有线程都可以访问,但线程的变量的操作(读取赋值等)必须在自己的工作内存中去进行,首先要 将变量从主存拷贝到自己的工作内存中,然后对变量进行操作,操作完成后再将变量操作完后的新值写回主内存,不能直接操作主内存的变量,各个线程的工作内存中存储着主内存的变量拷贝的副本,因IC不同的线程间无法访问对方的工作内存,线程间的通信必须在主内存来完成, 其简要访问过程如下图:


              Java编译期与运行期优化探究_第3张图片 

三,可见性



​ 可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

​ 通过前面的 JMM介绍,我们知道各个线程对主内存的变量的操作都是各个线程各自拷贝到自己的工作内存中进行操作,然后在写回主内存中

​ 这就可能存在一个线程a修改了共享变量X的值但还未写回主内存,又有一个线程b对共享变量X进行操作,但 此时线程a的工作内存的共享变量X对线程吧来说是不可见的,这种工作内存与主内存同步延迟的问题就造成了可见性问题


四,不保证原子性



​ 原子性:某个线程在执行某项业务时,中间不可被加塞或分割,需要整体完整。要么同时成功,要么同时失败

五,禁止指令重排

​ 计算机在执行程序时,为了提高性能,编译器和处理 器常常会对指令做重排,一般分为一下三种:

Java编译期与运行期优化探究_第4张图片

单线程的环境里指令重排确保最终执行的结果和代码顺序执行的结果一致,处理器在进行指令重排是必须 ,要考虑指令之间的数据依赖性;多线程的环境交替执行,由于编译器优化重排的存在,俩个线程使用变量能否保证一致性是无法确定的,无法预料的


主内存与工作内存 :

线程是 通过主内存 去进行线程间的 隐式通信 的,而线程对共享变量的写操作在 工作内存 中完成,由JMM控制 共享变量由工作内存写回到主内存的时机 。

JMM提供了一个保证内存可见性的原则: happens-before原则 。这个原则可以保证线程对共享变量的写操作对其它线程可见。如果在多线程环境下需要满足happens-before原则,就必须对共享变量添加某种特定的读写规则,否则会导致多线程环境下对共享变量的操作无法对其它线程可见,造成 缓存不一致 的现象。
 


                                            线程工作内存与主内存交互


             Java编译期与运行期优化探究_第5张图片 

你可能感兴趣的:(java,开发语言)