美文网首页
运行期优化

运行期优化

作者: 我可能是个假开发 | 来源:发表于2024-01-26 13:42 被阅读0次

一、分层编译

public class JIT1 {
    // -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)与解释器的区别:

  • 解释器是将字节码解释为机器码,下次即使遇到相同的字节码,仍会执行重复的解释
  • JIT 是将一些字节码编译为机器码,并存入 Code Cache,下次遇到相同的代码,直接执行,无需再编译
  • 解释器是将字节码解释为针对所有平台都通用的机器码
  • JIT 会根据平台类型,生成平台特定的机器码

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

以上代码在C2编译器中执行了逃逸分析的优化,因为new Object不会逃离出该方法,则不会再继续创建对象,时间就降低了很多。

可以使用 -XX:-DoEscapeAnalysis 关闭逃逸分析。

二、方法内联(Inlining)

private static int square(final int i) {
  return i * i;
}
System.out.println(square(9));

如果发现 square 是热点方法,并且长度不太长时,会进行内联,所谓的内联就是把方法内代码拷贝、粘贴到调用者的位置:System.out.println(9 * 9);
还能够进行常量折叠(constant folding)的优化:System.out.println(81);

public class JIT2 {
    // -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining -XX:CompileCommand=dontinline,*JIT2.square
    // -XX:+PrintCompilation

    public static void main(String[] args) {

        int x = 0;
        for (int i = 0; i < 500; i++) {
            long start = System.nanoTime();
            for (int j = 0; j < 1000; j++) {
                x = square(9);

            }
            long end = System.nanoTime();
            System.out.printf("%d\t%d\t%d\n",i,x,(end - start));
        }
    }

    private static int square(final int i) {
        return i * i;
    }
}
  • -XX:+UnlockDiagnosticVMOptions -XX:+PrintInlining (解锁隐藏参数)打印
    inlining 信息
  • -XX:CompileCommand=dontinline,*JIT2.square 禁止某个方法 inlining
  • -XX:+PrintCompilation 打印编译信息

三、字段优化

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

<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>

编写基准测试代码:

import org.openjdk.jmh.annotations.*;
import org.openjdk.jmh.runner.Runner;
import org.openjdk.jmh.runner.RunnerException;
import org.openjdk.jmh.runner.options.Options;
import org.openjdk.jmh.runner.options.OptionsBuilder;

import java.util.Random;
import java.util.concurrent.ThreadLocalRandom;

@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.DONT_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 的方法内联,测试结果如下(每秒吞吐量,分数越高的更好):

Benchmark Mode Samples Score Score error Units
t.Benchmark1.test1 thrpt 5 2420286.539 390747.467 ops/s
t.Benchmark1.test2 thrpt 5 2544313.594 91304.136 ops/s
t.Benchmark1.test3 thrpt 5 2469176.697 450570.647 ops/s

接下来禁用 doSum 方法内联

@CompilerControl(CompilerControl.Mode.DONT_INLINE)
static void doSum(int x) {
  sum += x;
}

测试结果如下:

Benchmark Mode Samples Score Score error Units
t.Benchmark1.test1 thrpt 5 296141.478 63649.220 ops/s
t.Benchmark1.test2 thrpt 5 371262.351 83890.984 ops/s
t.Benchmark1.test3 thrpt 5 368960.847 60163.391 ops/s

分析:
在刚才的示例中,doSum 方法是否内联会影响 elements 成员变量读取的优化:
如果 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
  }
}

可以节省 1999 次 Field 读取操作,但如果 doSum 方法没有内联,则不会进行上面的优化

四、反射优化

public class Reflect1 {

    public static void foo() {
        System.out.println("foo...");
    }

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

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

class NativeMethodAccessorImpl extends MethodAccessorImpl {
    private final Method method;
    private DelegatingMethodAccessorImpl parent;
    private int numInvocations;

    NativeMethodAccessorImpl(Method var1) {
        this.method = var1;
    }

    public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
        // inflationThreshold 膨胀阈值,默认 15
        if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
            // 使用 ASM 动态生成的新实现代替本地实现,速度较本地实现快 20 倍左右
            MethodAccessorImpl var3 = (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(var3);
        }
        // 调用本地实现
        return invoke0(this.method, var1, var2);
    }

    void setParent(DelegatingMethodAccessorImpl var1) {
        this.parent = var1;
    }

    private static native Object invoke0(Method var0, Object var1, Object[] var2);
}

当调用到第 16 次(从0开始算)时,会采用运行时生成的类代替掉最初的实现,可以通过 debug 得到类名为 sun.reflect.GeneratedMethodAccessor1
可以使用阿里的 arthas 工具查看详细信息:

java -jar arthas-boot.jar
[INFO] arthas-boot version: 3.1.1
[INFO] Found existing java process, please choose one and hit RETURN.
* [1]: 13065 com.hcx.jvm.reflect.Reflect1

sun.reflect.noInflation 可以用来禁用膨胀(直接生成GeneratedMethodAccessor1,但首次生成比较耗时,如果仅反射调用一次,不划算)sun.reflect.inflationThreshold 可以修改膨胀阈值

相关文章

  • 《深入理解Java虚拟机-JVM高级特性与最佳实践》学习总结(第

    第十一章 晚期(运行期)优化 目录: 11.1 运行期优化什么?11.2 解释器与编译器的分工11.3 HotSp...

  • JVM运行期优化及逃逸分析实战

    运行期优化 楼主最近在网上看到一篇写关于JVM运行期优化的博客,经过整理,现在分享给大家: 我们知道,Java 是...

  • Java运行期优化

    部分的商用虚拟机中,Java程序最初是通过解释器进行解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会...

  • JVM运行期优化

    一、前言 JVM运行期的优化主要是指程序在编译成字节码之后,JVM通过解释器去解释执行,再针对程序运行的资源占用等...

  • 晚期(运行期)优化

    JIT:Java程序最初是由解释器解释执行的,当虚拟机发现某个方法或代码块的运行特别频繁时,就会把这些“热点代码”...

  • 晚期(运行期)优化

    HotSpot的即时编译器 解释器与编译器 编译对象与触发条件 编译过程 编译优化技术 如果还对其他的经典编译优化...

  • 《深入理解JVM虚拟机》 - 编译

    优化分为编译期优化和运行期优化,前者着重编码优化(语法糖),后者着重运行效率。语法糖syntactic sugar...

  • 要点提炼| 理解JVM之程序编译&代码优化

    本篇将介绍程序编译时期的代码优化手段,分成两个阶段: 概述 早期(编译期)优化 晚期(运行期)优化 1.概述 a....

  • 9.运行期优化

    运行期优化 本章讲述针对《008.编译期优化.md》中第二类编译过程的优化。 1. 概述 Java程序最初是通过解...

  • openstack虚拟机(centos7)简单优化

    基本优化 源优化 运维常用软件下载 远程连接优化 时间同步优化 颜色提示符优化

网友评论

      本文标题:运行期优化

      本文链接:https://www.haomeiwen.com/subject/xllhndtx.html