方法调用-解析

/ Jvm / 没有评论 / 350浏览

在这一篇中我们详细分析一下虚拟机在进行方法调用时所执行的具体逻辑。在这里要特别说明一下,方法调用并不代表执行方法中的具体代码。这里的方法调用的目的是确定被调用方法的版本(也就是到底调用哪个方法),所以此时是不会执行方法里的代码的。下面我们看一下在方法调用时所要执行的必须操作。

通常在方法调用时目标方法在Class文件里存储的都是常量池中的符号引用,在类的加载时,才会把一部份的符号引用转化成直接引用。这在类加载的时候已经介绍过了。但实际的加载过程中是比较复杂的,因为在转化成直接引用时有一个前提条件。就是上面提到的,在方法调用之前必须有一个可确定的版本,并且这个方法的调用版本在运行期是不可改变的。在说的简单点就是调用的方法在编译时就已经确定下来的,这类方法的调用叫做解析。

在上述解析的过程中,主要包括静态方法和私有方法两种。静态方法与类型直接关联,私有方法只能在本类中访问。这两种方法各自的特点决定了它们都不可能通过继承或者重写等方式重写其它版本,所以这两种方法都会在类加载阶段进行解析。

在虚拟机中提供了5种方法调用的指令,它们分别是:

只要能被invokestatic和invokespecial指令调用的方法,都可以在解析阶段中确定唯一的调用版本,符合这个条件的有静态方法、 私有方法、 实例构造器、 父类方法等。它们在类加载的时候就会把符号引用解析为该方法的直接引用。 这些方法就叫做非虚方法。除了上述方法外的方法就叫做虚方法(除了final方法)。下面我们用Java中自带的命令工具来验证invokestatic指令的调用。

源码

public class Test {

public static void helloWorld() {
System.out.println("hello world");
}

public static void main(String[] args) {
Test.helloWorld();
}

}

调用

javap -verbose Test

结果

Classfile /C:/Java/jdk1.8.0_91/bin/Test.class
  Last modified 2017-4-17; size 482 bytes
  MD5 checksum 71c7eabbf6f7ca9a2ff493a3a8390b5b
  Compiled from "Test.java"
public class Test
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #7.#17         // java/lang/Object."<init>":()V
   #2 = Fieldref           #18.#19        // java/lang/System.out:Ljava/io/PrintStream;
   #3 = String             #20            // hello world
   #4 = Methodref          #21.#22        // java/io/PrintStream.println:(Ljava/lang/String;)V
   #5 = Methodref          #6.#23         // Test.helloWorld:()V
   #6 = Class              #24            // Test
   #7 = Class              #25            // java/lang/Object
   #8 = Utf8               <init>
   #9 = Utf8               ()V
  #10 = Utf8               Code
  #11 = Utf8               LineNumberTable
  #12 = Utf8               helloWorld
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               SourceFile
  #16 = Utf8               Test.java
  #17 = NameAndType        #8:#9          // "<init>":()V
  #18 = Class              #26            // java/lang/System
  #19 = NameAndType        #27:#28        // out:Ljava/io/PrintStream;
  #20 = Utf8               hello world
  #21 = Class              #29            // java/io/PrintStream
  #22 = NameAndType        #30:#31        // println:(Ljava/lang/String;)V
  #23 = NameAndType        #12:#9         // helloWorld:()V
  #24 = Utf8               Test
  #25 = Utf8               java/lang/Object
  #26 = Utf8               java/lang/System
  #27 = Utf8               out
  #28 = Utf8               Ljava/io/PrintStream;
  #29 = Utf8               java/io/PrintStream
  #30 = Utf8               println
  #31 = Utf8               (Ljava/lang/String;)V
{
  public Test();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: return
      LineNumberTable:
        line 1: 0

  public static void helloWorld();     descriptor: ()V     flags: ACC_PUBLIC, ACC_STATIC     Code:       stack=2, locals=0, args_size=0          0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;          3: ldc           #3                  // String hello world          5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V          8: return       LineNumberTable:         line 4: 0         line 5: 8

  public static void main(java.lang.String[]);     descriptor: ([Ljava/lang/String;)V     flags: ACC_PUBLIC, ACC_STATIC     Code:       stack=0, locals=1, args_size=1          0: invokestatic  #5                  // Method helloWorld:()V          3: return       LineNumberTable:         line 8: 0         line 9: 3 } SourceFile: "Test.java"

我们看虚拟机确实调用了invokestatic指令。在Java中的非虚方法除了使用invokestatic、 invokespecial调用的方法之外还有一种,就是被final修饰的方法。 虽然final方法是使用invokevirtual指令来调用的,但由于final方法不能被覆盖,不可能有其他版本,所以也无须对方法调用者进行多态选择,或者说多态选择的结果也肯定只有唯一的一个。 所以final方法也是一种非虚方法。