JVM5️⃣优化:编译期、运行期

1、编译期优化

Java 编译器,在编译期会自动生成和转换一些代码。

也称语法糖(syntactic sugar)

1.1、默认构造

public class Demo1 {
}

编译后相当于

public class Demo1 {
    // 默认构造
    public Candy1() {
        super(); // 调用父类 Object 的无参构造
        <init>":()V
    }
}

1.2、自动拆装箱

JDK 5 引入自动拆装箱

// 源代码1
public class Demo2 {
    public static void main(String[] args) {
        Integer x = 1;
        int y = x;
    }
}
  • < JDK 5:无法通过编译,必须修改为以下代码。

  • >= JDK 5:编译期自动转换

    // 源代码2
    public class Demo2 {
        public static void main(String[] args){
            Integer x = Integer.valueOf(1);
            int y = x.intValue();
        }
    }
    

1.3、泛型

1.3.1、泛型擦除

JDK 5 引入泛型,编译为字节码后会将泛型擦除

泛型信息在编译为字节码后就丢失了,实际的类型都会当作 Object 类型来处理。

public class Demo3 {
    public static void main(String[] args) {
        List<Integer> list = new ArrayList<>();
        
        // 实际调用 List.add(Object e)
        list.add(10); 
        // 实际调用 Object obj = List.get(int index);
        Integer x = list.get(0); 
    }
}
  • 在取值时,编译器会在字节码中做一个类型转换的操作。

    // 将 Object转为Integer
    Integer x = (Integer)list.get(0);
    
  • 若变量 x 是基本类型,则相当于如下操作:

    // 先将Object转为Integer,再拆箱
    int x = ((Integer)list.get(0)).intValue();
    

1.3.2、本地变量类型表

泛型擦除,将字节码上的泛型信息擦除了。

但是本地变量类型表仍保留了方法参数泛型的信息,可以通过反射获取

stack=2, locals=3, args_size=1
    0: new #2					// class java/util/ArrayList
    3: dup
    4: invokespecial #3 		// Method java/util/ArrayList."<init>":()V
    7: astore_1
    8: aload_1
    9: bipush 10
    11: invokestatic #4 		// Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer;
    14: invokeinterface #5, 2 	// InterfaceMethod java/util/List.add:(Ljava/lang/Object;)Z
    19: pop
    20: aload_1
    21: iconst_0
    22: invokeinterface #6, 2 	// InterfaceMethod java/util/List.get:(I)Ljava/lang/Object;
    27: checkcast #7 			// class java/lang/Integer
    30: astore_2
    31: return
LineNumberTable:
    line 8: 0
    line 9: 8
    line 10: 20
    line 11: 31
LocalVariableTable:
    Start	Length	Slot	Name	Signature
    0		32		0		args	[Ljava/lang/String;
    8		24		1		list	Ljava/util/List;
LocalVariableTypeTable:
    Start	Length	Slot	Name	Signature
    	8	24		1		list	Ljava/util/List<Ljava/lang/Integer;>;

反射获取

定义一个方法:方法返回值、参数列表都是泛型集合。

public Set<Integer> test(List<String> list, Map<Integer, Object> map) {
}

反射代码

// 获取指定参数类型的方法
Method test = Demo3.class.getMethod("test", List.class, Map.class);
// 获取方法的泛型参数
Type[] types = test.getGenericParameterTypes();

for (Type type : types) {
    // 参数化类型(即泛型)
    if (type instanceof ParameterizedType) {
        // 打印
        ParameterizedType parameterizedType = (ParameterizedType) type;
        System.out.println("原始类型 - " + parameterizedType.getRawType());
        
        Type[] arguments = parameterizedType.getActualTypeArguments();
        
        for (int i = 0; i < arguments.length; i++) {
            System.out.printf("泛型参数[%d] - %s\n", i, arguments[i]);
        }
    }
}

输出结果

原始类型 - interface java.util.List
泛型参数[0] - class java.lang.String
原始类型 - interface java.util.Map
泛型参数[0] - class java.lang.Integer
泛型参数[1] - class java.lang.Object

1.4、可变参数

JDK 5 引入。

public class Demo4 {
    public static void foo(String... args) {
        String[] array = args;
        System.out.println(array);
    }
    public static void main(String[] args) {
        foo("hello", "world");
    }
}

可变参数...,实际上就是一维数组

编译后相当于

public class Demo4 {
    public static void foo(String[] args) {
        String[] array = args; // 直接赋值
        System.out.println(array);
    }
    public static void main(String[] args) {
        foo(new String[]{"hello", "world"});
    }
}

:如果调用方法时没有传入任何参数,则相当于创建并传递了一个空数组,而不是传 null

// 源代码
public static void main(String[] args) {
    foo();
}
// 编译后相当于
public static void main(String[] args) {
    foo(new String[]{});
}

1.5、foreach 循环

  • 数组:转换为 for。
  • 集合:转换为迭代器。

1.5.1、数组

JDK 5 引入。

public class Demo5 {
    public static void main(String[] args) {
         // 数组赋初值的简化写法,也是编译期优化!
        int[] array = {1, 2, 3, 4, 5};
        
        for (int e : array) {
            System.out.println(e);
        }
    }
}

数组 foreach 被转换为 for 循环。

public class Demo5 {
    public Demo5() {
    }
    
    public static void main(String[] args) {

        int[] array = new int[]{1, 2, 3, 4, 5

        for(int i = 0; i < array.length; ++i) {
            int e = array[i];
            System.out.println(e);
        }
}

1.5.2、集合

public class Demo5 {
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1,2,3,4,5);
        
        for (Integer i : list) {
            System.out.println(i);
        }
    }
}

集合 foreach 被转换为迭代器循环

public class Demo5 {
    public Demo5() {
    }
    
    public static void main(String[] args) {
        List<Integer> list = Arrays.asList(1, 2, 3, 4, 5);
        
        Iterator iter = list.iterator();
        while(iter.hasNext()) {
            Integer e = (Integer)iter.next();
            System.out.println(e);
        }
    }
}

1.6、枚举类

JDK 5 引入了枚举类。

enum Gender {
    MALE, FEMALE
}

枚举类被转换为一个继承了 Enum 的最终类。(单例 + 工厂)

  • name:枚举常量名称

  • ordinary:序号,从 0 开始

    public final class Gender extends Enum<Sex> {
        public static final Gender MALE;
        public static final Gender FEMALE;
        private static final Gender[] $VALUES;
        static {
            MALE = new Gender("MALE", 0);
            FEMALE = new Gender("FEMALE", 1);
            $VALUES = new Gender[]{MALE, FEMALE};
        }
    
        private Gender(String name, int ordinal) {
            super(name, ordinal);
        }
        public static Gender[] values() {
            return $VALUES.clone();
        }
        public static Gender valueOf(String name) {
            return Enum.valueOf(Gender.class, name);
        }
    }
    

1.7、switch

JDK 7 开始,switch 可以搭配字符串和枚举类

1.7.1、switch 字符串

public class Demo6 {
    public static void choose(String str) {
        switch (str) {
            case "hello": {
                System.out.println("h");
                break;
            }
            case "world": {
                System.out.println("w");
                break;
            }
        }
    }
}

switch 字符串,被转换为两个搭配整数的 switch-case。

  1. 获取字符串的 hashCode,比较 hashCode 和 equals()

  2. 根据第一个 switch-case 的结果,执行 case 块中的代码

    public class Candy6_1 {
        public Candy6_1() {
        }
        
        public static void choose(String str) {
            byte x = -1;
            switch(str.hashCode()) {
                case 99162322: // hello 的 hashCode
                    if (str.equals("hello")) {
                        x = 0;
                    }
                    break;
                case 113318802: // world 的 hashCode
                    if (str.equals("world")) {
                        x = 1;
                    }
            }
            switch(x) {
                case 0:
                    System.out.println("h");
                    break;
                case 1:
                    System.out.println("w");
            }
        }
    }
    

1.7.2、switch 枚举

enum Gender {
    MALE, FEMALE
}

public class Demo7 {
    public static void foo(Gender gender) {
        switch (gender) {
            case MALE:
                System.out.println("男"); break;
            case FEMALE:
                System.out.println("女"); break;
        }
    }
}

编译后相当于

  • 创建一个合成类,映射枚举的 ordinary 与 数组元素
  • switch 字符串,被转换为 switch ordinary。
public class Candy7 {
    static class $MAP {
        // 数组大小即为枚举元素个数,里面存储case用来对比的数字
        static int[] map = new int[2];
        static {
            // 枚举的 ordinal 从 0 开始
            map[Sex.MALE.ordinal()] = 1;
            map[Sex.FEMALE.ordinal()] = 2;
        }
    }
    public static void foo(Sex sex) {
        int x = $MAP.map[sex.ordinal()];
        switch (x) {
            case 1:
                System.out.println("男");
                break;
            case 2:
                System.out.println("女");
                break;
        }
    }
}

1.8、try-with-resources

JDK 7 新增,资源对象需实现 AutoCloseable 接口

  • 可以不用写 finally 块,编译器会自动释放资源的代码。

  • 常见的有 InputStream、OutputStream、Connection、Statement、ResultSet 等接口。

    try(资源变量 = 创建资源对象){
    } catch( ) {
    }
    

示例

public class Demo9 {
    public static void main(String[] args) {
        try(InputStream is = new FileInputStream("d:\\1.txt")) {
            System.out.println(is);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

编译后相当于在 try 块中创建资源对象,并再次使用 try-catch 语句释放资源

分析 try 块

  1. 创建资源对象

  2. 在 try 内部再使用 try-catch 语句执行外部 try 块代码,若捕获异常则抛出。

  3. 若无异常,在 finally 中判断资源是否为空,非空则需要释放

  4. 判断内部 try 块中是否发生异常

    • 没有则直接关闭资源。

    • 有,则再使用一次 try-catch 语句执行资源释放操作。

      • 若在资源释放过程中,再次发生异常,则作为压制异常添加。

      • 目的不覆盖掉 try 块中的异常,保留所有可能出现的异常

        public class Demo9 {
            public Demo9() {
            }
            public static void main(String[] args) {
                try {
                    InputStream is = new FileInputStream("d:\\1.txt");
                    // 可能出现的异常
                    Throwable t = null;
                    try {
                        System.out.println(is);
                    } catch (Throwable e1) {
                        t = e1;
                        throw e1;
                    } finally {
                        // 资源非空,需要释放
                        if (is != null) {
                            // 内部try块发生异常
                            if (t != null) {
                                try {
                                    is.close();
                                } catch (Throwable e2) {
                                    // 资源释放出现异常,作为被压制异常添加
                                    t.addSuppressed(e2);
                                }
                            } else {
                                is.close();
                            }
                        }
                    }
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
        

1.9、方法重写桥接

子类可以重写父类的方法。

重写方法的返回值,必须是与父类返回值类型相同,或父类返回值的子类型

class A {
    public Number m() {
        return 1;
    }
}

class B extends A {
    @Override
    public Integer m() {
        return 2;
    }
}

Class B 编译后相当于生成一个桥接方法,作为真正的重写方法。

桥接方法调用子类中声明的方法

class B extends A {
    public Integer m() {
        return 2;
    }
    // 真正重写了父类 public Number m() 方法
    public synthetic bridge Number m() {
        return m();
    }
}

1.10、匿名内部类

定义匿名内部类

public class Demo11 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("ok");
            }
        };
    }
}

编译后相当于生成一个额外类,通过构造器创建。

public class Demo11 {
    public static void main(String[] args) {
        Runnable runnable = new Candy11$1();
    }
}

// 额外类
final class Demo11$1 implements Runnable {
    Candy11$1() {
    }
    public void run() {
        System.out.println("ok");
    }
}

定义匿名内部类,且引用局部变量

内部类引用了方法参数 x。

public class Candy11 {
    public static void test(final int x) {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                System.out.println("ok:" + x);
            }
        };
    }
}

编译后相当于生成一个额外类,将局部变量作为构造器参数传入。

public class Candy11 {
    public static void test(final int x) {
        Runnable runnable = new Candy11$1(x);
    }
}

// 额外类
final class Candy11$1 implements Runnable {
    int val$x;
    Candy11$1(int x) {
        this.val$x = x;
    }
    public void run() {
        System.out.println("ok:" + this.val$x);
    }
}

在 Java SE 中,当匿名内部类引用局部变量时,局部变量必须是 final 的

原因:局部变量作为构造器参数传入额外类,并且额外类生成后就不会再改变,因此局部变量也不允许改变

2、运行期优化

2.1、说明

早期优化编译期优化

晚期优化:运行期优化

image-20220305113523460

2.1.1、JVM 结构

两个结构,一个概念

  • 解释器(Interpreter):对 Java 程序进行解释执行(平台无关性)
  • 即时编译器(Just In Time Compiler):为提高热点代码的执行效率,将热点代码编译成与本地相关的机器码,并进行各层次的优化(平台有关性)
    • Client Compiler:也叫 C1 编译器
    • Server Compiler:也叫 C2 编译器
  • 热点代码(Hot Spot Code):当 JVM 发现某个方法或代码块的运行特别频繁,就认定是热点代码。

2.1.2、工作机制

注:JIT 编译器对于代码的优化建立在字节码或机器码之上,而不是修改 Java 源码

  • 当程序启动时:解释器可以首先发挥作用,省去编译的时间,直接将代码解释执行。
  • 随着代码执行次数越来越多:JIT 编译器开始发挥作用,将代码编译成本地机器码,提高执行效率。

2.1.3、分层编译

Tiered Compilation

了解一个概念:Profiling:性能监控功能。

  • 部分开启:方法调用次数、回边次数统计等;
  • 完全开启:除了部分开启的统计信息外,还包括分支跳转、虚方法调用版本等全部的统计信息。

根据编译器编译、优化的规模与耗时,划分出不同的编译层次。

使用 执行方式 开启 Profiling
第 0 层 解释器 解释 不开启
第 1 层 C1 编译器 编译 不开启
第 2 层 C1 编译器 编译 部分开启
第 3 层 C1 编译器 编译 完全开启
第 4 层 C2 编译器 编译 开启,并进行激进优化

2.2、优化技术

编译器的优化技术有多种,如下所示。

image-20220305122111120

image-20220305122143424

image-20220305122209222

2.2.1、实例

通过示例,展示其中的几种优化技术。

假设 foo() 是热点代码

static Class A {
    int value;
    final int getValue() {
        return value;
    }
}

public void foo () {
    // a是A的对象实例
    x = a.getValue();
    y = a.getValue();
    sum = x + y;
}
  1. 方法内联(Inlining)

    • 去除方法调用成本:如查找方法版本、建立栈帧

    • 为其他优化建立良好的基础,因此通常作为优化序列的前面

      public void foo () {
          x = a.value;
          y = a.value;
          sum = x + y;
      }
      
  2. 冗余访问消除(Redundant Loads Elimination)

    • 由于 x 和 y 访问同一个变量(假设变量值没有被修改),可以保证 x 和 y 的值相同。

    • 因此对 y 赋值时,不用去访问 value 变量,直接将局部变量 x 的值赋给 y 即可。

    • a.value 是一项表达式,此项优化可以视为公共子表达式消除(Common SubexpressionElimination)

      public void foo () {
          x = a.value;
          y = x;
          sum = x + y;
      }
      
  3. 复写传播(Copy Propagation)

    • 在程序的逻辑中,x 和 y 完全相等,没有必要用一个额外的变量 y。

    • 因此用 x 来代替 y。

      public void foo() {
          x = A.value;
          x = x;
          sum = x + x;
      }
      
  4. 无用代码消除(Dead Code Elimination)

    • 永远不会被执行的代码

    • 完全没有意义的代码。

      public void foo() {
          x = A.value;
          sum = x + x;
      }
      

经过四次优化后,代码效果完全一致,但是代码省略了许多语句。

2.2.2、代表性技术

方法内联(重要技术)

  • 去除方法调用成本:如查找方法版本、建立栈帧
  • 为其他优化建立良好的基础,因此通常作为优化序列的前面

逃逸分析(前沿技术)

逃逸:在方法内部定义的局部变量,被外部方法所引用。

  • 方法逃逸:如作为调用参数传递到其他方法
  • 线程逃逸:如赋值给可以在其他线程中访问的实例变量
  • 不逃逸、方法逃逸、线程逃逸,是对象从低祷告的不同逃逸程度。

公共子表达式消除(语言无关)

  • 公共子表达式:如果表达式 E 在之前已经被计算过了,并且 E 中所有变量的值都没有发生变化,称 E 称公共子表达式。

  • 对于公共子表达式,没必要花时间重新计算,直接用前面计算过的表达式结果代替 E

    // 举个例子
    int x = (a * b) + c + (b * a)
    // 消除后
    int x = E + 12 + c * E
    

数组边界检查消除(语言有关)

数组边界检查

Java 中访问数据元素时,系统会自动进行上下界的范围检查。

超出范围则抛出数组下标越界异常。

为了安全,必须进行数组边界检查,但是没有必要在每次访问的时候都进行。

示例

  1. 数组下标是常量:如 foo[3],只要在编译期分析确定 foo.length 并判断下标 3 没有越界,执行的时候就无需判断。
  2. 循环访问数组:如 for 循环,只要在编译期分析确定循环变量的范围永远在 [0, foo.length) 之间,那么在循环中就可以把整个数组数组边界检查消除了。
posted @ 2022-03-06 15:58  Jaywee  阅读(135)  评论(0编辑  收藏  举报

👇