解释执行:由解释器一行一行翻译执行
编译执行:把字节码编译成机器码,直接执行机器码
那么如何查看自己的java是解释执行还是编译执行呢?
$ java -version
java version "1.8.0_251"
Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)
mixed mode 代表混合执行,部分解释执行、部分编译执行。
-Xint:设置JVM的执行模式为解释执行模式
$ java -Xint -version
java version "1.8.0_251"
Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, interpreted mode)
-Xcomp:JVM优先以编译模式运行,不能编译的,以解释模式运行
$ java -Xcomp -version
java version "1.8.0_251"
Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, compiled mode)
-Xmixed:以混合模式运行
一般情况下,我们的代码一开始一般由解释器解释执行。但是当虚拟机发现某个方法或代码块的运行特别频繁的时候,就会认为这些代码是热点代码(如何定位?)。为了提高热点代码的执行效率,会用即使编译器(也就是JIT)把这些热点代码编译城与本地平台相关的机器码,并进行各层次的优化(操作系统的不同、CPU架构的不同)
从JDK7开始,正式引入了分层编译的概念,可以细分为 5 种编译级别:
级别越高,应用启动越慢,优化的开销越高,峰值性能也越高。
基于采样的热点探测
周期性检查各个线程的栈顶,如果发现某一些方法总是出现在各个栈顶,那就说明是热点代码。
基于计数器的热点探测
大致思路是为每一个方法甚至是代码块建立计数器,然后统计执行的次数,如果超过一定的阈值,那就说明它是热点代码。Hotspot虚拟机采用的就是基于计数器的热点探测。
方法调用计数器(Invocation Counter)
用于统计方法被调用的次数,在不开启分层编译的情况下,在 C1 编译器下的默认阈值是 1500 次,在 C2 模式下是 10000次。也可以哦那个 -XX:CompileThreshold=X 指定阈值
回边计数器(Back Edge Counter)
当开启分层编译时,JVM会根据当前编译的方法数以及编译线程数来动态调整阈值,-XX:CompileThreshold、-XX:OnStackReplacePercentage 都会失效。
如果不做任何设置,方法调用次数统计的并不是方法被调用的绝对次数,而是一个相对的执行频,即一段时间之内方法被调用的次数。当超过一定的时间限度,如果方法的调用次数荏苒不足以让它提交给及时编译器编译,那这个方法的调用计数器就会减少一半,这个过程称为方法调用计数器热度的衰减,而这段时间就称为此方法统计的半衰周期。进行热度衰减的动作是在虚拟机进行垃圾手机是顺便进行的,可以使用虚拟机参数-XX:-UseCounterDecay来关闭热度衰减,让方法计数器统计方法调用的绝对次数,这样,只要系统运行时间足够长,绝大部分方法都会被编译成本地代码。另外,可以使用-XX:CounterHalfLifeTime参数设置半衰周期的时间,单位是秒。
package com.example;
public class InlineTest1 {
private static int add1(int x1, int x2, int x3, int x4) {
return add2(x1, x2) + add2(x3, x4);
}
public static int add2(int x1, int x2) {
return x1 + x2;
}
// 内联后
private static int addInline(int x1, int x2, int x3, int x4) {
return x1 + x2 + x3 + x4;
}
}
所谓的方法内联就是把目标方法的代码复制到发起调用的方法之中,避免发生真实的方法调用,从而减少压栈和出栈的作用
方法体足够小
热点方法:如果方法体小于325字节会尝试内联,可用 -XX:FreqInlineSize
修改方法体大小。
非热点方法:如果方法体小于35字节,也会尝试内联,可用-XX:MaxInlineSize
修改大小。
被调用方法运行时的实现可唯一确定
static方法、private方法及final方法,JIT可以唯一确定具体的实现代码。
public的实例方法,指向的实现可能是自身、父类、子类的代码,当且仅当JIT能够唯一确定方法的具体实现时,才有可能完成内联。
内联是用空间换时间的一种做法,也就是及时编译器在方法调用期间把方法调用连接在一起,但是经过内联的代码会变多,而增加的代码量取决于方法的调用次数以及方法本身的大小。
在一些极端情况下,内联可能会导致
public class InlineTest {
private final static Logger log = LoggerFactory.getLogger(InlineTest.class);
public static void main(String[] args) {
long cost = compute();
log.info("执行花费 {} ms", cost);
}
private static long compute() {
long start = System.currentTimeMillis();
int result;
Random random = new Random();
for (int i = 0; i< 10000000; i++) {
int a = random.nextInt();
int b = random.nextInt();
int c = random.nextInt();
int d = random.nextInt();
result = add1(a, b, c, d);
}
long end = System.currentTimeMillis();
return end - start;
}
public static int add1(int n1, int n2, int n3, int n4) {
return add2(n1, n2) + add2(n3, n4);
}
private static int add2(int n1, int n2) {
return n1 + n2;
}
}
内联启动JVM参数:-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining
不内联启动JVM参数:-XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining -XX:+FreqInlineSize=1
但是,一般来说不建议使用这些JVM参数,默认的就好,现代JVM是相当智能的。
逃逸分析:分析变量能否逃出它的作用域。
可以细分为 4 种场景:
代码示例:
/**
* @author fengxuechao
* @date 2022/2/15
*/
public class EscapeTest1 {
public static SomeClass someClass;
/**
* 全局变量赋值逃逸
*/
public void globalVariablePointerEscape() {
someClass = new SomeClass();
}
/**
* 方法返回值逃逸
* someMethod() {
* SomeClass someClass = methodPointerEscape();
* }
*/
public SomeClass methodPointerEscape() {
return new SomeClass();
}
/**
* 实例引用传递逃逸
*/
public void instancePassPointerEscape() {
this.methodPointerEscape()
.printClassName(this);
}
}
class SomeClass {
public void printClassName(EscapeTest1 escapeTest) {
System.out.println(escapeTest.getClass().getName());
}
}
JVM 会针对以上4种场景进行分析,然后会为对象做一个逃逸状态标识,一个对象主要有 3 种逃逸状态标识:
finialze()
方法,那么这个类的对象都会被标记为全局逃逸状态并且一定会放在堆内存中。标量:不能被进一步分解的量
聚合量:可以进一步分解的量
那么什么是标量替换呢?逃逸分析确定该对象不会被外部访问,并且对象可以进一步被分解,JVM不会创建该对象,而是创建它的成员变量来代替。
代码展示:
class Person {
int id;
int age;
}
public static void main(String[] args) {
Person person = new Person();
person.id = 1;
person.age = 18;
// 上面这段代码,标量替换后,原本的对象就不用分配内存空间了,就会优化成如下所示:
int id = 1;
int age = 18;
}
JVM 参数:-XX:+EliminateAllocaions
开启标量替换(JDK8 默认开启)
在 Java 中,绝大多数对象都是放在堆里面的,当对象无用时,由垃圾回收器回收。
栈上分配,顾名思义,就是通过逃逸分析,能够确认对象不会被外部访问,就在栈上分配对象,在栈上分配对象,就可以在栈帧出栈的时候销毁对象了,通过栈上分配就可以降低垃圾回收的压力。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-esYD0Fny-1644931608002)