jvm虚拟机浅谈(二)

一、方法调用

指令名称

描述

invokestatic

用于调用静态方法

invokespecial

用于调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的default方法

invokevirtual

用于调用非私有实例方法

invokeinterface

用于调用接口方法

invokedynamic

用于调用动态方法

1.1 虚方法调用和非虚方法调用

虚方法调用:可以被子类重写的方法调用,需要运行时才能确定具体的调用类型。接口方法调用(invokeinterface 指令)和非私有实例方法调用(invokevirtual 指令)都属于虚方法调用。

非虚方法调用:被invokestatic和invokespecial指令调用的方法,在解析阶段可以确定唯一的调用版本。静态方法、私有方法、实例构造器、父类方法4种,再加上被final修饰的方法(尽管它使用invokevirtual指令调用),这五种方法调用在类加载的时候就可以把符号引用解析为该方法的直接引用。

1.2 静态绑定和动态绑定

静态绑定:包括用于调用静态方法的 invokestatic 指令,和用于调用构造器、私有实例方法以及超类非私有实例方法的 invokespecial 指令。如果虚方法调用指向一个标记为 final 的方法,那么 Java 虚拟机也可以静态绑定该虚方法调用的目标方法。

动态绑定:Java 虚拟机需要根据调用者的动态类型,来确定虚方法调用的目标方法。

1.3 java重写和jvm重写的区别

java的重写:指的是方法名相同并且参数类型也相同的方法之间的关系。

Java虚拟机的重写:除了方法名和参数类型之外,返回类型也必须一致。

对于 Java 语言中重写而 Java 虚拟机中非重写的情况,编译器会通过生成桥接方法来实现 Java 中的重写语义。

eg:当一个声明类型为 Merchant,实际类型为 NaiveMerchant 的对象,调用 actionPrice 方法时,字节码里的符号引用指向的是 Merchant.actionPrice(double,Customer) 方法。Java 虚拟机将动态绑定至 NaiveMerchant 类的桥接方法之中,并且调用其 actionPrice(double,Customer) 方法。

interface Customer {
  boolean isVIP();
}

class Merchant {
  public Number actionPrice(double price, Customer customer) {
     return Double.doubleToLongBits(2.0);
  }
}

public class NaiveMerchant extends Merchant {
  @Override
  public Double actionPrice(double price, Customer customer) {
     return 3.0;
  }

  public static void main(String[] args) {
     Merchant naiveMerchant = new NaiveMerchant();
     Number price =naiveMerchant.actionPrice(1.0d,null);
  }

}

javap -v 反编译如下

public class NaiveMerchant extends Merchant
  minor version: 0
  major version: 55
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #5                          // NaiveMerchant
  super_class: #9                         // Merchant
  interfaces: 0, fields: 0, methods: 4, attributes: 1
Constant pool:
   #1 = Methodref          #9.#21         // Merchant."<init>":()V
   #2 = Double             3.0d
   #4 = Methodref          #22.#23        // java/lang/Double.valueOf:(D)Ljava/lang/Double;
   #5 = Class              #24            // NaiveMerchant
   #6 = Methodref          #5.#21         // NaiveMerchant."<init>":()V
   #7 = Methodref          #9.#25         // Merchant.actionPrice:(DLCustomer;)Ljava/lang/Number;
   #8 = Methodref          #5.#26         // NaiveMerchant.actionPrice:(DLCustomer;)Ljava/lang/Double;
   #9 = Class              #27            // Merchant
  #10 = Utf8               <init>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               actionPrice
  #15 = Utf8               (DLCustomer;)Ljava/lang/Double;
  #16 = Utf8               main
  #17 = Utf8               ([Ljava/lang/String;)V
  #18 = Utf8               (DLCustomer;)Ljava/lang/Number;
  #19 = Utf8               SourceFile
  #20 = Utf8               NaiveMerchant.java
  #21 = NameAndType        #10:#11        // "<init>":()V
  #22 = Class              #28            // java/lang/Double
  #23 = NameAndType        #29:#30        // valueOf:(D)Ljava/lang/Double;
  #24 = Utf8               NaiveMerchant
  #25 = NameAndType        #14:#18        // actionPrice:(DLCustomer;)Ljava/lang/Number;
  #26 = NameAndType        #14:#15        // actionPrice:(DLCustomer;)Ljava/lang/Double;
  #27 = Utf8               Merchant
  #28 = Utf8               java/lang/Double
  #29 = Utf8               valueOf
  #30 = Utf8               (D)Ljava/lang/Double;
{
  public NaiveMerchant();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method Merchant."<init>":()V
         4: return
      LineNumberTable:
        line 12: 0

  public java.lang.Double actionPrice(double, Customer);
    descriptor: (DLCustomer;)Ljava/lang/Double;
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=4, args_size=3
         0: ldc2_w        #2                  // double 3.0d
         3: invokestatic  #4                  // Method java/lang/Double.valueOf:(D)Ljava/lang/Double;
         6: areturn
      LineNumberTable:
        line 15: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=4, locals=3, args_size=1
         0: new           #5                  // class NaiveMerchant
         3: dup
         4: invokespecial #6                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: dconst_1
        10: aconst_null
        11: invokevirtual #7                  // Method Merchant.actionPrice:(DLCustomer;)Ljava/lang/Number;
        14: astore_2
        15: return
      LineNumberTable:
        line 19: 0
        line 20: 8
        line 21: 15

  public java.lang.Number actionPrice(double, Customer);
    descriptor: (DLCustomer;)Ljava/lang/Number;
    flags: (0x1041) ACC_PUBLIC, ACC_BRIDGE, ACC_SYNTHETIC
    Code:
      stack=4, locals=4, args_size=3
         0: aload_0
         1: dload_1
         2: aload_3
         3: invokevirtual #8                  // Method actionPrice:(DLCustomer;)Ljava/lang/Double;
         6: areturn
      LineNumberTable:
        line 12: 0
}

在编译过程中,我们并不知道目标方法的具体内存地址。因此,Java编译器会暂时用符号引用来表示该目标方法。这一符号引用包括目标方法所在的类或接口的名字,以及目标方法的方法名和方法描述符。符号引用存储在 class 文件的常量池之中。根据目标方法是否为接口方法,这些引用可分为接口符号引用和非接口符号引用。在执行使用了符号引用的字节码前,Java 虚拟机需要解析这些符号引用,并替换为实际引用。

1.4 解析符号引用

在C中查找符合名字及描述符的方法。
如果没有找到,在C的父类中继续搜索,直至Object类。
如果没有找到,在C所直接实现或间接实现的接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的

对于非接口符号引用,假定该符号引用所指向的类为 C,则 Java 虚拟机会按照如下步骤进行查找。

在I中查找符合名字及描述符的方法。
如果没有找到,在Object类中的公有实例方法中搜索。
如果没有找到,则在I的超接口中搜索,这一步搜索得到的目标方法必须是非私有、非静态的。

经过上述的解析步骤之后,符号引用会被解析成实际引用。对于可以静态绑定的方法调用而言,实际引用是一个指向方法的指针。对于需要动态绑定的方法调用而言,实际引用则是一个方法表的索引。

1.5 方法表

在类加载的准备阶段,在类的方法区中构造与该类相关联的方法表。方法表分invokevirtual使用的虚方法表(virtual method table,vtable)和invokeinterface使用的接口方法表(interface method table,itable)。方法表本质上是一个数组,每个数组元素指向一个当前类及其祖先类中非私有的实例方法。这些方法可能是具体的、可执行的方法,也可能是没有相应字节码的抽象方法。

方法表满足两个特质:

其一,子类方法表中包含父类方法表中的所有方法。

虚方法表中存放着各个方法的实际入口地址。如果某个方法在子类中没有被重写,那子类的虚方法表中的地址入口和父类相同方法的地址入口是一致的,都指向父类的实现入口。如果子类中重写了这个方法,子类虚方法表中的地址也会被替换为指向子类实现版本的入口地址。

eg:

public class Dispatch {
	static class QQ {}
	static class _360 {}
	public static class Father {
		public void hardChoice(QQ arg) {
        	System.out.println("father choose qq");
        }
        public void hardChoice(_360 arg) {
        	System.out.println("father choose 360");
        }
    }
  	public static class Son extends Father {
        public void hardChoice(QQ arg) {
        	System.out.println("son choose qq");
        }
        public void hardChoice(_360 arg) {
        	System.out.println("son choose 360");
        }
  	}
    public static void main(String[] args) {
      Father father = new Father();
      Father son = new Son();
      father.hardChoice(new _360());
      son.hardChoice(new QQ());
    }
}

其二,子类方法在方法表中的索引值,与它所重写的父类方法的索引值相同。

为了程序实现方便,具有相同签名的方法,在父类、子类的虚方法表中都应当具有一样的索引序号,这样当类型变换时,仅需要变更查找的虚方法表,就可以从不同的虚方法表中按索引转换出所需的入口地址。

在解析虚方法调用时,Java 虚拟机会纪录下所声明的目标方法的索引值,并且在运行过程中根据这个索引值查找具体的目标方法。

eg:


abstract class Passenger {
  abstract void passThroughImmigration();
  @Override
  public String toString() { 
  	 return null;
  }
}
class ForeignerPassenger extends Passenger {
   @Override
   void passThroughImmigration() { 
     /* 外国人通道 */ 
   }
}
class ChinesePassenger extends Passenger {
  @Override
  void passThroughImmigration() { 
    /* 中国人通道 */ 
  }
  void visitDutyFreeShops() { 
    /* 逛免税店 */ 
  }
}

相应的虚方法表:

1.6 内联缓存

内联缓存是一种加快动态绑定的优化技术。它能够缓存虚方法调用中调用者的动态类型,以及该类型所对应的目标方法。在之后的执行过程中,如果碰到已缓存的类型,内联缓存便会直接调用该类型所对应的目标方法。如果没有碰到已缓存的类型,内联缓存则会退化至使用基于方法表的动态绑定。对于内联缓存来说,有对应的单态内联缓存、多态内联缓存和超多态内联缓存。

单态内联缓存,顾名思义,便是只缓存了一种动态类型以及它所对应的目标方法。它的实现非常简单:比较所缓存的动态类型,如果命中,则直接调用对应的目标方法。

多态内联缓存,则缓存了多个动态类型及其目标方法。它需要逐个将所缓存的动态类型与当前动态类型进行比较,如果命中,则调用对应的目标方法。

为了节省内存空间,Java 虚拟机只采用单态内联缓存。对于内联缓存中的内容,我们有两种思路。

一是替换单态内联缓存中的纪录

在最坏情况下,我们用两种不同类型的调用者,轮流执行该方法调用,那么每次进行方法调用都将替换内联缓存。也就是说,只有写缓存的额外开销,而没有用缓存的性能提升。

另外一种选择则是劣化为超多态状态

这也是 Java 虚拟机的具体实现方式。处于这种状态下的内联缓存,实际上放弃了优化的机会。它将直接访问方法表,来动态绑定目标方法。与替换内联缓存纪录的做法相比,它牺牲了优化的机会,但是节省了写缓存的额外开销。

eg:


public abstract class Passenger {
   abstract void passThroughImmigration();
   public static void main(String[] args) {
      Passenger a = new ChinesePassenger();
      Passenger b = new ForeignerPassenger();
      long current = System.currentTimeMillis();
      for (int i = 1; i <= 2_000_000_000; i++) {
        if (i % 100_000_000 == 0) {
          long temp = System.currentTimeMillis();
          System.out.println(temp - current);
          current = temp;
        }
        Passenger c = (i < 1_000_000_000) ? a : b;
  //      Passenger c = (i % 2)==0? a : b;
        c.passThroughImmigration();
      }
  }
}
class ChinesePassenger extends Passenger {
  @Override void passThroughImmigration() {} 
}
class ForeignerPassenger extends Passenger {
  @Override void passThroughImmigration() {}
}

为了消除方法内联的影响,使用以下命令。

java -XX:CompileCommand='dontinline,*.passThroughImmigration' Passenger

java -XX:CompileCommand='dontinline,*.passThroughImmigration' Passenger
CompileCommand: dontinline *.passThroughImmigration
258                   //缓存动态类型
262
255
284
274
259
257
254
257
257         
371                  //缓存失效,劣化为超多态
366
370
367
368
376
419
433
385
426

1.7 方法内联

在编译过程中遇到方法调用时,将目标方法的方法体纳入编译范围之中,并取代原方法调用的优化手段。

以 getter/setter 为例,如果没有方法内联,在调用 getter/setter 时,程序需要保存当前方法的执行位置,创建并压入用于 getter/setter 的栈帧、访问字段、弹出栈帧,最后再恢复当前方法的执行。而当内联了对 getter/setter 的方法调用后,上述操作仅剩字段访问。

在 C2 中,方法内联是在解析字节码的过程中完成的。每当碰到方法调用字节码时,C2 将决定是否需要内联该方法调用。如果需要内联,则开始解析目标方法的字节码。

静态方法调用

eg:

public static boolean flag = true;
public static int value0 = 0;
public static int value1 = 1;

public static int foo(int value) {
    int result = bar(flag);
    if (result != 0) {
        return result;
    } else {
        return value;
    }
}

public static int bar(boolean flag) {
    return flag ? value0 : value1;
}

foo方法的IR图(代码中间表示)(内联前)

在编译 foo 方法时,其对应的 IR 图中将出现对 bar 方法的调用,即上图中的 5 号 Invoke 节点。如果内联算法判定应当内联对 bar 方法的调用时,那么即时编译器将开始解析 bar 方法的字节码,并生成对应的 IR 图,如下图所示。

接下来,即时编译器便可以进行方法内联,把 bar 方法所对应的 IR 图纳入到对 foo 方法的编译中。具体的操作便是将 foo 方法的 IR 图中 5 号 Invoke 节点替换为 bar 方法的 IR 图。

将flag、value0、value1定义成final类型


public final static boolean flag = true;
public final static int value0 = 0;
public final static int value1 = 1;

public static int foo(int value) {
    int result = bar(flag);
    if (result != 0) {
        return result;
    } else {
        return value;
    }
}

public static int bar(boolean flag) {
    return flag ? value0 : value1;
}

foo的IR图(内联后)

进一步优化(死代码消除)

foo的IR图(优化后)

方法内联性能影响

强制进行方法内联,如下命令:

java -XX:CompileCommand='inline,*.passThroughImmigration' Passenger


CompileCommand: inline *.passThroughImmigration
86
152
152
159
165
179
145
146
150
144
331
246
235
235
232
234
235
235
233
234

参考链接:深入拆解Java虚拟机_JVM_Java底层-极客时间

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值