0
点赞
收藏
分享

微信扫一扫

JVM之几种常见的JIT优化

奋斗De奶爸 2022-03-12 阅读 117
java

目录

一、公共子表达式消除(经典的JIT优化技术)

二、方法内联

三、逃逸分析

四、三种逃逸分析优化方式

         1、 对象的栈上内存分配

2、标量替换

3、同步锁消除

一、公共子表达式消除(经典的JIT优化技术)

        1、概述

        如果一个表达式E已经经过计算,并且从先前的计算到本次计算E中的所有变量值保持不变,那么E在此次计算中就是公共子表达式。此时只需要使用之前E的计算结果即可。

        2、分类

  • 局部公共子表达式消除:仅限于程序基本块(方法,循环等)中
  • 全局公共子表达式消除:包含多个基本块的优化。

        3、优化示例

package jvm.study;

/**
* @author ghCode
* @Email:2085264964@qq.com
* 公共子表达式消除
*/

public class publicExtendsExpression {
public static void main(String[] args) {
for(int i=0;i<1000000;i++){
sum(1,2,3);
}
}
public static void sum(int a, int b, int c){
/*计算d的值时,由于是循环执行,每次传入的参数又不变,那么JIT就会对该表达式中的公共子表达式进行优化*/
int d=(b*c)*12+a+(a+b*c);//其中c*b为公共子表达式
/*所以上式等同于:int d=E*12+a+(a+E) -------消除
* int d=E*13+a*2 -------代码再优化*/

}
}

二、方法内联

        1、概述

        在使用JIT进入即时编译时,将方法调用直接使用方法体中进行代码替换,减少方法调用时的压栈入栈的开销;同时为之后的一些优化手段提供条件。如果JVM检测到一些方法被频繁执行,就会把方法调用替换成方法本身。

        2、好处

        方法执行时会从方法区压入栈形成栈帧,当循环或者递归调用次数过多时,将会导致栈内存溢出(java.lang.StackOverflowError异常),而将方法的调用替换成方法体本身,可以减少压栈操作,节省资源。

        3、示例

package jvm.study;

/**
* @author ghCode
* @Email:2085264964@qq.com
* 方法内联
*/

public class MethodInAssociated {
public static void main(String[] args) {
long start1=System.currentTimeMillis();
for (int i=0;i<10000000;i++){
int e=alSum(1,2,3,4);//调用嵌套方法多次计算(JIT自动内联进行方法体替换)
}
long end1=System.currentTimeMillis();
System.out.println("JIT内置方法内联时间花费="+(end1-start1));
long start2=System.currentTimeMillis();
for (int i=0;i<10000000;i++){
int e=alSum2(1,2,3,4);//多次计算(方法体替换成内联后的方法体,模拟JIT方法内联)
}
long end2=System.currentTimeMillis();
System.out.println("模拟JIT内置方法内联时间花费="+(end2-start2));
}
public static int alSum(int a,int b,int c,int d){
return sum(a,b)+sum(c,d);//在alSum方法中嵌套调用sum方法
}
public static int sum(int a,int b){
return a+b;
}
public static int alSum2(int a,int b,int c,int d){
return a+b+c+d;
}
}

        经过多次测试,两种时间花费相差都在1毫秒内,说明JIT对代码进行了优化。减少了alSum()方法中对于sum()方法的入栈所带来的时间和性能上的消耗。 

三、逃逸分析

        1、概述

        逃逸分析是一种可以有效减少java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。

        2、作用

        通过逃逸分析,java HotSpot编译器能够分析出一个新的对象的引用的使用范围从而决定是否要将这个对象分配到堆内存中(大多数情况下,我们都会认为对象在堆内存中,但其实这不是绝对的,逃逸分析就会改变这一“定论”)。

        3、方法逃逸

        分析对象的动态作用域,当一个对象在方法中被定义后,他可能被外部方法所引用,例如作为调用参数传递到其他地方中,称为方法逃逸。

        4、逃逸分析包括

  • 全局变量赋值逃逸
  • 方法返回值逃逸
  • 实例引用发生逃逸
  • 线程逃逸

        5、 示例(以方法返回值逃逸为例)

package jvm.study;

/**
* @author ghCode
* @Email:2085264964@qq.com
* 方法返回值逃逸
*/

public class ReturnEscapeAnalysis {
public static void main(String[] args) {
StringBuffer b=stringAdd("这是方法返回值","逃逸分析");
}
public static StringBuffer stringAdd(String s1,String s2){
StringBuffer buffer=new StringBuffer();
buffer.append(s1);
buffer.append(s2);
return buffer;
}
}

        在上面的代码中,stringAdd()方法中的buffer对象作为返回值在主函数中被使用了,赋值给了对象b,那么此时,我们称buffer发生了方法返回值逃逸;若将stringAdd()方法的返回值改为String类型,然后return buffer.toString();那么此时buffer对象并没有也不能被外部所使用,所以就没有发生方法返回值逃逸。

        6、开启和关闭逃逸分析。

        在jkd1.7之后JVM默认开启逃逸分析。开启与关闭命令如下:

  • 开启配置:-XX:+DoEscapeAnalysis
  • 关闭配置:-XX:-DoEscapeAnalysis

        开启方式(IDEA):  先运行一次方法,构建一下代码,再配置。                  

四、三种逃逸分析优化方式

        1、 对象的栈上内存分配

               ① 一般情况下,对象和数组元素的内存分配是在堆内存上进行的。JIT编译器可以在编译期间根据逃逸分析结果,决定是否将对象的内存分配从堆内存转化为栈。这么做有两个好处:

  • 堆内存中的对象会涉及到垃圾回收操作,而垃圾回收是一个很耗性能的操作,而栈中的对象不会参与到垃圾回收机制。
  • 堆对于对象的存储消耗空间较大,堆中的一个对象包括对象头、实例数据、对齐填充;而栈中的对象只有实例数据。

               ②命令行查看java程序执行过程中堆内存内容命令:jps   和    jmap -histo [数字]

               ③示例

package jvm.study;

/**
* @author ghCode
* @Email:2085264964@qq.com
* 逃逸分析测试之栈上内存分配。
*/

public class EscapeAnalysis {
public static void main(String[] args) {
long start=System.currentTimeMillis();
for (int i=0;i<1000000;i++){
newObject();
}
long end =System.currentTimeMillis();
System.out.println("expend:"+(end-start)+"ms");//未开启逃逸分析,expend=17ms,实例化一百万个Student对象。
//开启逃逸分析,expend=5ms,实例化100541个对象,几乎少了十倍!
try {
Thread.sleep(100000);//为什么要进行线程的等待:等待状态使得方法运行未结束,对象还存在堆内存中
//才可以进行命令行分析,否则方法执行结束,堆内存就销毁了,无法进行对比。
}catch (Exception e){
e.printStackTrace();
}
}
public static void newObject(){
Student student=new Student();//方法返回值为void,所以对象student没有逃逸,进行栈上内存分配优化。
}
static class Student{
}
}

                 (1)未开启逃逸分析时,耗时expend=17ms,此时java堆内存如下:

                此时可以发现,堆内存中实例化了一百万个Student对象,这是十分耗时且耗空间的。 

                 (2)开启逃逸分析后,耗时expend=5ms,此时java堆内存如下:

                此时堆内存中的Student对象只有约十一万个,相比没有开启逃逸分析减少了接近十倍,而速度也快了12ms,当数据量跟大时,差距还会更明显,优化的作用也就体现的更加重要。

2、标量替换

         ①什么是标量:标量是指一个无法再分解成更小数据更小单位的数据。

         ②概述:在JIT优化中,如果经过逃逸分析,发现一个对象没有逃逸,不会被外界访问到,那么就会将该对象拆解成若干个成员变量。

         ③示例    

package jvm.study;

/**
* @author ghCode
* @Email:2085264964@qq.com
* 标量替换
*/

public class ScalarReplace {
public static void main(String[] args) {
newScalar();
}
public static void newScalar(){
Scalar s=new Scalar();
int a= s.a;
int b=s.b;
}
static class Scalar{
private int a=1;
private int b=0;
}
}

       此时newScalar()方法中的对象s并没有发生逃逸,所以在其中使用对象s时,会将s替换成s中的两个成员变量a和b。

    public static void newScalar(){
int a=1;
int b=0;
}

 3、同步锁消除

        ①概述:基于逃逸分析算法,当加锁(synchornized)的变量不会发生逃逸,是线程私有的,那么就没必要加锁,JIT就会对其进行优化,将同步锁去掉,以减少加锁和解锁造成的资源开销。

        ②示例:

package jvm.study;

/**
* @author ghCode
* @Email:2085264964@qq.com
* 同步锁消除
*/

public class ReturnEscapeAnalysis {
public static void main(String[] args) {
StringBuffer b=stringAdd("同步锁","消除");
}
public static String stringAdd(String s1,String s2){
StringBuffer buffer=new StringBuffer();
buffer.append(s1);
buffer.append(s2);
return buffer.toString();
}
}

        在StringBuffer类的apeend()方法中,通过查看源码可以发现加了同步锁,在上面的代码中,buffer对象没有发生逃逸,所以在JIT编译时会将append的同步锁去掉。

         

  

        

 

 

 

举报

相关推荐

0 条评论