java性能优化--编译器优化进阶(编译线程、内联、逃逸分析)

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第24天,点击查看活动详情

前面的文章提到过编译阈值,当方法调用次数,和循环执行次数的和,达到编译阈值时,server编译器就会对热点代码进行编译。本篇文章来简单看下内部的细节。本章提到的内容不建议手动优化,仅作为了解原理即可。

编译线程

当达到编译阈值时,方法或循环就是进入编译队列,有后台线程异步的获取队列的代码进行编译。

编译队列不是严格的先进先出,执行次数越多的代码具有更高的优先级。这也是在上一章节,我们通过标志PrintCompilation查看被编译方法时,compilationg_id不完全按顺序递增的原因。

使用不同的编译器,在不同平台下会有不同的线程数,与平台的cpu数有关。

通常来说,使用client编译器,则会开启一个线程;使用server编译器,则会开启两个线程。当开启分层编译时,将会开启多个线程,在不同的平台的CPU数量下,线程数也会不同,在分层编译器中,会将client编译器称为C1编译器,将server编译器称为C2编译器,我们下满就这么称呼它们,简单列举几个情况:

cpu数量 C1 C2
1 1 1
2 1 1
4 1 2
8 1 2
16 2 6
32 3 7
64 4 8
128 4 10

我们通过标志CICompilerCount来查看当前jvm的线程数量,这是jvm处理编译队列的总线程数:

[root@hecs-402944 opt]# jinfo -flag CICompilerCount 11210
-XX:CICompilerCount=2
复制代码

我的服务器是2核,所以是2。其中包含一个client线程和server线程。

内联

方法内联是编译器当中做的最重要的性能优化。我们熟悉的java实体类,通常都会为每个属性添加getter和setter方法,这种方法的调用相比于直接访问变量,会有较大的性能开销。

jvm当中方法调用,存在于虚拟机栈的栈帧当中,整个调用链较长,会有较大的性能损耗。

有如下的代码:

static class Student {
    private String name;

    private String firstName;
    
    private String secondName;
    
    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getSecondName() {
        return secondName;
    }

    public void setSecondName(String secondName) {
        this.secondName = secondName;
    }
}

public static void main(String[] args) {
    Student student = new Student();
    student.setName(student.getFirstName() + student.getSecondName());
}
复制代码

在jvm帮助我们在编译时进行内联后,就会有如下的代码:


public static void main(String[] args) {
    Student student = new Student();
    student.name = student.firstName + student.secondName;
}
复制代码

如上所示,已经将get和set方法的调用进行替换为直接属性的调用。

可以通过如下方式查看内联属性:

[root@hecs-402944 opt]# jinfo -flag Inline 11210
-XX:+Inline
复制代码

默认是开启的,此参数对性能影响巨大,不建议关闭。

内联的条件

触发方法内联是有条件的,不是所有的代码都会被内联。

是否可以被内联取决于方法代码是否够热和它的大小

  • 当代码小于325字节,并且被频繁调用时,会发生内联。资料显示可以通过MaxFreqInlineSize设置此值,但是我在java8当中发现此值不存在。
    [root@hecs-402944 opt]# jinfo -flag MaxFreqInlineSize 11210
    no such flag 'MaxFreqInlineSize'
    复制代码
  • 在上一条的基础上,只有代码小于35字节时,才会发生内联。可以通过MaxInlineSize设置。
    [root@hecs-402944 opt]# jinfo -flag MaxInlineSize 11210
    -XX:MaxInlineSize=35
    复制代码

逃逸分析

逃逸分析简单来说就是检查变量,查看变量的使用位置,检测其是否被其他范围所使用。如果它没有超出范围,那就是一个局部变量,我们能够针对这个变量做更多的优化。

如果被外部的方法调用,则称之为方法逃逸。 如果被外部的线程调用,则称之为线程逃逸。

在java8当中逃逸分析是默认被开启的:

[root@hecs-402944 opt]# jinfo -flag DoEscapeAnalysis 11210
-XX:+DoEscapeAnalysis
复制代码

开启逃逸分析后,server编译器将会进行很激进的优化:

  • 锁省略:对于对象内部的方法锁synchronized,在编译时会被优化掉,对象被new()时,其基本上不会被多个线程同时调用。
  • 变量保存在寄存器:对于对象内的属性变量,当不会发生逃逸时,会将变量放在寄存器当中,而不是内存当中。

当然还有很多复杂的优化,这里不过多介绍,因为不建议我们针对逃逸分析做优化。

但是我们可以就逃逸分析解决一些问题:

  • 开启逃逸分析后,可能会有某些代码编译时报错,如果我们发现,最好的做法是简化这部分代码,而不是关闭逃逸分析。

猜你喜欢

转载自juejin.im/post/7111091157240643615