编译线程
当达到编译阈值时,方法或循环就是进入编译队列,在后台异步地获取队列的代码进行编译。
编译队列不是严格的先进先出,执行次数越多的代码具有更高的优先级。这也是在上一章节,我们通过标志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()时,其基本上不会被多个线程同时调用。
- 变量保存在寄存器:对于对象内的属性变量,当不会发生逃逸时,会将变量放在寄存器当中,而不是内存当中。
当然还有很多复杂的优化,这里不过多介绍,因为不建议我们针对逃逸分析做优化。
但是我们可以就逃逸分析解决一些问题:
- 开启逃逸分析后,可能会有某些代码编译时报错,如果我们发现,最好的做法是简化这部分代码,而不是关闭逃逸分析
原文链接:https://juejin.cn/post/7111091157240643615