jvm--运行期优化;jit(代码片段)

MinggeQingchun MinggeQingchun     2022-12-03     612

关键词:

一、即时编译(JIT)

JIT:Just In Time Compiler,即时编译器

这是针对解释型语言而言的,而且并非虚拟机必须,是一种优化手段。Hotspot就有这种技术,Java虚拟机标准对JIT的存在没有作出任何规范,这是虚拟机实现的自定义优化技术。

HotSpot虚拟机的执行引擎在执行Java代码是可以采用 解释执行 和 编译执行 两种方式的

如果采用的是编译执行方式,那么就会使用到JIT,而解释执行就不会使用到JIT。

HotSpot中的编译器是javac,它的工作就是将java代码编译为可执行的class文件,这部分工作是完全独立的,完全不需要运行时参与,所以java程序的编译是半独立实现的。有了字节码,就由解释器来进行解释执行,这是早期java虚拟机的工作流程;后来,java虚拟机会将执行频率高的方法或者语句块通过JIT编译成本地机器码,提高了代码执行的效率

(一)JIT工作原理

当JIT编译启用时(默认是启用的),JVM读入.class文件解释后,将其发给JIT编译器。JIT编译器将字节码编译成本机机器代码。

通常javac将程序源代码编译,转换成java字节码,JVM通过解释字节码将其翻译成对应的机器指令,逐条读入,逐条解释翻译。很显然,经过解释执行,其执行速度必然会比可执行的二进制字节码程序慢。为了提高执行速度,引入了JIT技术。

在运行时JIT会把翻译过的机器码保存起来,以备下次使用,因此从理论上来说,采用该JIT技术可以,可以接近以前纯编译技术

(二)分层编译(Tiered Compilation)

Tiered Compilation是Java7中出现的,目的是整合C1的快速编译和C2的快速执行。因为C2使用了“激进”的优化手段,编译较慢。Java7以前,一般要求快速启动的GUI程序会选择C1,偏好性能的服务器程序使用C2

热点探测

HotSpot虚拟机中有两个编译器,一个是给客户端用的叫client Compiler,另一个是服务器用的叫Server Compiler。

一般的,把Client Compiler也叫C1编译器,Server Compiler叫C2编译器或Opto编译器

虚拟机会根据自身版本与宿主机的硬件性能自动选择运行模式,也可以使用 “-client”或“-server”参数去强制指定虚拟机运行在Client模式或Server模式

热点代码有两类

  • 被多次执行的方法
  • 多次执行的循环体

虚拟机为每个代码块和方法设置了计数器,执行一次就加1。超过限定次数就认为是热点代码,开始JIT处理。给JIT去处理只是一个请求,并不会立即同步等待结果。因为JIT编译比较耗时,在编译完成前会继续解释执行。编译器处理都是以方法为单位,所以第一类热点代码是标准的JIT编译方式;对于第二种热点代码,JIT编译器会处理包含该循环的方法

HotSpot虚拟机有两种计数器(方法会同时记录这两个计数),它们的阈值并不同

1、调用次数计数器,可以通过-XX:CompileThreadhold参数指定阈值,不指定默认C1是1500次,C2是1万次

2、字节码中向之前跳转的指令叫“回边”,回边次数是回边计数器。明显这个针对的是第二类热点代码。它的阈值是算出来的,公式如下

OSR 阈值 = CompileThreshold * 
((OnStackReplacePercentage - InterpreterProfilePercentage)/100)

第一个参数CompileThreshold就是调用计数器,后面两个也都可以通过-XX指定。默认InterpreterProfilePercentage是33,而OnStackReplacePercentage的默认值在客户端和服务器模式不一样,分别是933和140,所以阈值分别是13500和10700

// -XX:+PrintCompilation -XX:-DoEscapeAnalysis
    public static void main(String[] args) 
        for (int i = 0; i < 200; i++) 
            long start = System.nanoTime();
            for (int j = 0; j < 1000; j++) 
                new Object();
            
            long end = System.nanoTime();
            System.out.printf("%d\\t%d\\n",i,(end - start));
        
    

上述代码在后面循环次数中时间花费比较少

图片来自百度

因为JVM 将执行状态分成了 5 个层次

0 层,解释执行(Interpreter)

1 层,使用 C1 即时编译器编译执行(不带 profiling)

2 层,使用 C1 即时编译器编译执行(带基本的 profiling)

3 层,使用 C1 即时编译器编译执行(带完全的 profiling)

4 层,使用 C2 即时编译器编译执行

注:

profiling 是指在运行过程中收集一些程序执行状态的数据,例如【方法的调用次数】,【循环的 回边次数】等 

(三)即时编译器(JIT)与解释器的区别

1、解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释

2、JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需 再编译

3、解释器是将字节码解释为针对所有平台都通用的机器码

4、JIT 会根据平台类型,生成平台特定的机器码

对于占据大部分的不常用的代码,我们无需耗费时间将其编译成机器码,而是采取解释执行的方式运 行;另一方面,对于仅占据小部分的热点代码,我们则可以将其编译成机器码,以达到理想的运行速 度。 执行效率上简单比较一下 Interpreter < C1 < C2

https://docs.oracle.com/en/java/javase/12/vm/java-hotspot-virtual-machine-performance-enhancements.html#GUID-F33D8BD0-5C4A-4CE8-8259-FD9D73C7C7C6

(四)内联

方法内联就是把被调用方函数代码"复制"到调用方函数中,来减少因函数调用开销的技术

一个简单的两数相加程序,被内联前的代码

private int add4(int x1, int x2, int x3, int x4)   
    return add2(x1, x2) + add2(x3, x4);  
  

private int add2(int x1, int x2)   
    return x1 + x2;  

运行一段时间后,代码被内联翻译成

private int add4(int x1, int x2, int x3, int x4)   
    return x1 + x2 + x3 + x4;  
 

JVM会自动的识别热点方法,并对它们使用方法内联优化 ;一段代码需要执行多少次才会触发JIT优化呢?通常这个值由-XX:CompileThreshold参数进行设置

  • 使用client编译器时,默认为1500;
  • 使用server编译器时,默认为10000

一个方法就算被JVM标注成为热点方法,JVM仍然不一定会对它做方法内联优化。其中有个比较常见的原因就是这个方法体太大了,分为两种情况。

  • 如果方法是经常执行的,默认情况下,方法大小小于325字节的都会进行内联(可以通过** -XX:MaxFreqInlineSize=N**来设置这个大小)
  • 如果方法不是经常执行的,默认情况下,方法大小小于35字节才会进行内联(可以通过** -XX:MaxInlineSize=N **来设置这个大小)

可以通过增加这个大小,以便更多的方法可以进行内联;但是除非能够显著提升性能,否则不推荐修改这个参数。因为更大的方法体会导致代码内存占用更多,更少的热点方法会被缓存,最终的效果不一定好。

如果想要知道方法被内联的情况,可以使用下面的JVM参数来配置

-XX:+PrintCompilation //在控制台打印编译过程信息
-XX:+UnlockDiagnosticVMOptions //解锁对JVM进行诊断的选项参数。默认是关闭的,开启后支持一些特定参数对JVM进行诊断
-XX:+PrintInlining //将内联方法打印出来

想要对热点的方法使用内联的优化方法,最好尽量使用final、private、static这些修饰符修饰方法,避免方法因为继承,导致需要额外的类型检查,而出现效果不好情况 

(五)字段优化

JMH 基准 OpenJDK: jmh

创建 maven 工程,添加依赖如下

<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <jmh.version>1.0</jmh.version>
    <uberjar.name>benchmarks</uberjar.name>
  </properties>

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.11</version>
      <scope>test</scope>
    </dependency>

    <dependency>
      <groupId>org.openjdk.jmh</groupId>
      <artifactId>jmh-core</artifactId>
      <version>$jmh.version</version>
    </dependency>
    <dependency>
      <groupId>org.openjdk.jmh</groupId>
      <artifactId>jmh-generator-annprocess</artifactId>
      <version>$jmh.version</version>
      <scope>provided</scope>
    </dependency>
  </dependencies>
@Warmup(iterations = 2, time = 1)
@Measurement(iterations = 5, time = 1)
@State(Scope.Benchmark)
public class Benchmark1 

    int[] elements = randomInts(1_000);

    private static int[] randomInts(int size) 
        Random random = ThreadLocalRandom.current();
        int[] values = new int[size];
        for (int i = 0; i < size; i++) 
            values[i] = random.nextInt();
        
        return values;
    

    @Benchmark
    public void test1() 
        for (int i = 0; i < elements.length; i++) 
            doSum(elements[i]);
        
    

    @Benchmark
    public void test2() 
        int[] local = this.elements;
        for (int i = 0; i < local.length; i++) 
            doSum(local[i]);
        
    

    @Benchmark
    public void test3() 
        for (int element : elements) 
            doSum(element);
        
    

    static int sum = 0;

    @CompilerControl(CompilerControl.Mode.INLINE)
    static void doSum(int x) 
        sum += x;
    


    public static void main(String[] args) throws RunnerException 
        Options opt = new OptionsBuilder()
                .include(Benchmark1.class.getSimpleName())
                .forks(1)
                .build();

        new Runner(opt).run();
    

启用 doSum 的方法内联,测试结果如下(每秒吞吐量,分数越高的更好):

# Run complete. Total time: 00:00:28

Benchmark                Mode  Samples        Score  Score error  Units
c.m.Benchmark1.test1    thrpt        5  3543660.510   220838.607  ops/s
c.m.Benchmark1.test2    thrpt        5  3349451.189   913399.544  ops/s
c.m.Benchmark1.test3    thrpt        5  3472374.929   602064.401  ops/s

禁用 doSum 方法内联 

@CompilerControl(CompilerControl.Mode.DONT_INLINE)
static void doSum(int x) 
    sum += x;
# Run complete. Total time: 00:00:28

Benchmark                Mode  Samples       Score  Score error  Units
c.m.Benchmark1.test1    thrpt        5  421998.239   185020.935  ops/s
c.m.Benchmark1.test2    thrpt        5  505350.741    14294.063  ops/s
c.m.Benchmark1.test3    thrpt        5  463303.567   180720.536  ops/s

 如果 doSum 方法内联了,刚才的 test1 方法会被优化成下面的样子

@Benchmark
public void test1() 
    // elements.length 首次读取会缓存起来 -> int[] local
    for (int i = 0; i < elements.length; i++)  // 后续 999 次 求长度 <- local
        sum += elements[i]; // 1000 次取下标 i 的元素 <- local
    

二、反射优化

Java的反射技术,使静态语言的java具备了动态语言的某些特质。反射,让java动态,编程的时候更加灵活,能够动态获取信息以及动态调用对象方法。其实,Java基础技术中的代理,注解也都是依托反射才 能得以实现并应用广泛,另外我们常用的Spring、myBatis等技术框架也都是依托反射才能得以实现

public class Reflect1 

    public static void test() 
        System.out.println("test...");
    

    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException, IOException 
        Method test = Reflect1.class.getMethod("test");
        for (int i = 0; i <= 16; i++) 
            System.out.printf("%d\\t", i);
            test.invoke(null);
        
        System.in.read();
    

test.invoke 前面 0 ~ 15 次调用使用的是 MethodAccessor 的 NativeMethodAccessorImpl 实现 

package sun.reflect;
import java.lang.reflect.InvocationTargetException;

import java.lang.reflect.Method;
import sun.reflect.misc.ReflectUtil;

class NativeMethodAccessorImpl extends MethodAccessorImpl 

    private final Method method;
    private DelegatingMethodAccessorImpl parent;
    private int numInvocations;
    
    NativeMethodAccessorImpl(Method method) 
        this.method = method;
    

    public Object invoke(Object target, Object[] args)
        throws IllegalArgumentException, InvocationTargetException 
        // inflationThreshold 膨胀阈值,默认 15
        if (++this.numInvocations > ReflectionFactory.inflationThreshold()
            && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass()))
        
            // 使用 ASM 动态生成的新实现代替本地实现,速度较本地实现快 20 倍左右
            MethodAccessorImpl generatedMethodAccessor =
                (MethodAccessorImpl)
                    (new MethodAccessorGenerator())
                    .generateMethod(
                        this.method.getDeclaringClass(),
                        this.method.getName(),
                        this.method.getParameterTypes(),
                        this.method.getReturnType(),
                        this.method.getExceptionTypes(),
                        this.method.getModifiers()
                );
            this.parent.setDelegate(generatedMethodAccessor);
        
        // 调用本地实现
        return invoke0(this.method, target, args);
    

    void setParent(DelegatingMethodAccessorImpl parent) 
        this.parent = parent;
    

    private static native Object invoke0(Method method, Object target, Object[]args);

当调用到第 16 次(从0开始算)时,会采用运行时生成的类代替掉最初的实现,可以通过 debug 得到 类名为 sun.reflect.GeneratedMethodAccessor1 

使用阿里的 arthas 工具

java -jar arthas-boot.jar

再输入【jad + 类名】来进行反编译

$ jad sun.reflect.GeneratedMethodAccessor1

jvm晚期运行期优化

 最近听我的导师他们讨论Java的即时编译器(JIT),当时并不知道这是啥东西,所以就借着周末的时间,学习了一下!一、概述  在部分的商用虚拟机(SunHotSpot)中,Java程序最初是通过解释器(Interpreter)进行解释执行的... 查看详情

运行期优化(代码片段)

前言  在部分的商用虚拟机中,Java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块运行特别频繁的时候,就会把这些代码认定为“热点代码”。为了提高热点代码的执行效率,在运行时,虚拟机将会... 查看详情

jvm理论:(四/2)编译过程——晚期(运行期)

一、解释器与编译器  当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”。为了提高热点代码的执行效率,在运行时,虚拟机将会把这些代码编译成与本地平台相关的机器码,并进行各种... 查看详情

深入了解jvm——运行期优化

本文为《深入理解Java虚拟机》第十一章内容的学习笔记,部分内容经过二次加工。若对相关知识感兴趣,推荐购书深入阅读。若认为文章涉嫌侵权,请联系作者及时删除。本作品采用知识共享署名-非商业性使用-相同... 查看详情

运行期优化

  在部分商用虚拟机中,Java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块运行地特别频繁,就会把这些代码块认定为“热点代码”,为了提高热点代码的执行效率,在运行时,虚拟机会把这些... 查看详情

晚期(运行期)优化(代码片段)

摘自《深入理解Java虚拟机:JVM高级特性与最佳实践》(第二版)       从计算机程序出现的第一天起,对效率的追求就是程序天生的坚定信仰,这个过程犹如一场没有终点、永不停歇的F1方程式竞赛,... 查看详情

深入了解jvm——运行期优化

本文为《深入理解Java虚拟机》第十一章内容的学习笔记,部分内容经过二次加工。若对相关知识感兴趣,推荐购书深入阅读。若认为文章涉嫌侵权,请联系作者及时删除。本作品采用知识共享署名-非商业性使用-相同... 查看详情

jvm理论:(四/1)编译过程——早期(编译期)(代码片段)

...译器:把*.java文件转变成*.class文件的过程。  后端运行期编译器(JIT编译器):把字节码转变成机器码的过程。  静态提前编译器(AOT编译器):直接把*.java文件编译成本地机器代码的过程。  Javac这类编译器对代码的运... 查看详情

晚期(运行期)优化(代码片段)

在部分的商用虚拟机(SunHotSpot、IBMJ9)中,Java程序最初是通过解释器(Interpreter)进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些代码认定为“热点代码”(HotSpotC... 查看详情

jvmday05类加载阶段类加载器运行期优化(代码片段)

...载器双亲委派模式线程上下文类加载器自定义类加载器运行期优化即时编译反射优化类加载阶段类的完整生命周期包括7个部分:加载——验证——准备——解析——初始化——使用——卸载1、加载将类的字节码载入方法区... 查看详情

晚期(运行期)优化

晚期(运行期)优化晚期运行期优化StartHotSpot虚拟机内的即时编译器几个问题解释器与编译器编译对象与触发条件编译过程ClientCompilerServerCompiler查看及分析即时编译结果编译优化技术公共子表达式消除数组边界检查消除方法内联逃... 查看详情

jvm系列之:jit中的virtualcall接口(代码片段)

...VirtualCall的定义并举例分析了VirtualCall在父类和子类中的优化。JIT对类可以进行优化,那么对于interface可不可以做同样的优化么?一起来看看吧。最常用的接口ListList应该是大家最最常用的接口了,我想这个大家应该不会反驳。pub... 查看详情

jvm调优手段(代码片段)

...f0c;只提供了纯文本控制台环境的服务器上,它将是运行期定位虚拟机性能问题的首选工具。语法:jstat-optionpid其中option参数有如下:-class(类加载器) 查看详情

运行期优化

概述:    部分商用虚拟机中,Java程序最初是通过解释器对.class文件进行解释执行的,当虚拟机发现某个方法或代码块运行地特别频繁的时候,就会把这些代码认定为热点代码HotSpotCode(这也是我们使用的虚拟机HotSpot名称... 查看详情

小师妹学jvm之:jit中的printassembly(代码片段)

...解java代码的执行过程?想不想对你的代码进行进一步的优化和性能提升?如果你的回答是yes。那么这篇文章非常适合你,因为本文将会站在离机器码最近的地方来观看JVM的运行原理:Assembly。使用PrintAssembly小师妹:F师兄,上次... 查看详情

双管齐下,jvm内部优化与jvm性能调优(代码片段)

文章目录一、前言二、编译时优化2.1Javac编译器2.2Java语法糖2.2.1泛型和泛型擦除2.2.2自动装箱、自动拆箱、遍历循环2.2.3条件编译三、运行时优化(核心:JIT编译器/即时编译器)3.1HotSpot虚拟机内的JIT编译器3.1.1编译器... 查看详情

字节码执行方式--解释执行和jit

...易技术产品运营经验。1、两种执行方式:解释执行(运行期解释字节码并执行)强制使用该模式:-Xint编译为机器码执行(将字节码编译为机器码并执行,这个编译过程发生在运行期,称为JIT编译)强制使用该模式:-Xcomp,下面... 查看详情

字节码执行方式--解释执行和jit

...易技术产品运营经验。1、两种执行方式:解释执行(运行期解释字节码并执行)强制使用该模式:-Xint编译为机器码执行(将字节码编译为机器码并执行,这个编译过程发生在运行期,称为JIT编译)强制使用该模式:-Xcomp,下面... 查看详情