Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

类相关的指令 #10

Open
bitfishxyz opened this issue Jul 9, 2019 · 1 comment
Open

类相关的指令 #10

bitfishxyz opened this issue Jul 9, 2019 · 1 comment
Labels
字节码 JVM执行的字节码

Comments

@bitfishxyz
Copy link
Member

bitfishxyz commented Jul 9, 2019

接下里的指令涉及到jvm的类加载机制,如果你对类加载机制不熟悉,请务必阅读我之前的文章

#3

一个类会有 cinit 方法,和init方法,类初始化的时候会执行cinit方法,对象初始化的时候会执行init方法。

public class Monkey {
    public static int count = 1024;

    int age = 100;

    public static void main(String[] args) {
        Monkey monkey = new Monkey();
        monkey.age = 22;
    }
}

上面的类编译后的字节码是这样的:

$ javac Monkey.java
$ javap -v Monkey.class
Classfile /Users/apple/Downloads/x1hnd1rk/TemplateJava/src/Monkey.class
  Last modified Jul 6, 2019; size 404 bytes
  MD5 checksum 5126d2b933fff12a7a23312c26d22d7e
  Compiled from "Monkey.java"
public class Monkey
  minor version: 0
  major version: 54
  flags: (0x0021) ACC_PUBLIC, ACC_SUPER
  this_class: #3                          // Monkey
  super_class: #6                         // java/lang/Object
  interfaces: 0, fields: 2, methods: 3, attributes: 1
Constant pool:
   #1 = Methodref          #6.#19         // java/lang/Object."<init>":()V
   #2 = Fieldref           #3.#20         // Monkey.age:I
   #3 = Class              #21            // Monkey
   #4 = Methodref          #3.#19         // Monkey."<init>":()V
   #5 = Fieldref           #3.#22         // Monkey.count:I
   #6 = Class              #23            // java/lang/Object
   #7 = Utf8               count
   #8 = Utf8               I
   #9 = Utf8               age
  #10 = Utf8               <init>
  #11 = Utf8               ()V
  #12 = Utf8               Code
  #13 = Utf8               LineNumberTable
  #14 = Utf8               main
  #15 = Utf8               ([Ljava/lang/String;)V
  #16 = Utf8               <clinit>
  #17 = Utf8               SourceFile
  #18 = Utf8               Monkey.java
  #19 = NameAndType        #10:#11        // "<init>":()V
  #20 = NameAndType        #9:#8          // age:I
  #21 = Utf8               Monkey
  #22 = NameAndType        #7:#8          // count:I
  #23 = Utf8               java/lang/Object
{
  public static int count;
    descriptor: I
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC

  int age;
    descriptor: I
    flags: (0x0000)

  public Monkey();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: bipush        100
         7: putfield      #2                  // Field age:I
        10: return
      LineNumberTable:
        line 1: 0
        line 4: 4

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=2, args_size=1
         0: new           #3                  // class Monkey
         3: dup
         4: invokespecial #4                  // Method "<init>":()V
         7: astore_1
         8: aload_1
         9: bipush        22
        11: putfield      #2                  // Field age:I
        14: return
      LineNumberTable:
        line 7: 0
        line 8: 8
        line 9: 14

  static {};
    descriptor: ()V
    flags: (0x0008) ACC_STATIC
    Code:
      stack=1, locals=0, args_size=0
         0: sipush        1024
         3: putstatic     #5                  // Field count:I
         6: return
      LineNumberTable:
        line 2: 0
}
SourceFile: "Monkey.java"

这次我们来看看一个对象的构造方法里面的指令究竟是什么。

  public Monkey();
    descriptor: ()V
    flags: (0x0001) ACC_PUBLIC
    Code:
      stack=2, locals=1, args_size=1
         0: aload_0
         1: invokespecial #1                  // Method java/lang/Object."<init>":()V
         4: aload_0
         5: bipush        100
         7: putfield      #2                  // Field age:I
        10: return
      LineNumberTable:
        line 1: 0
        line 4: 4

这里有几个新的指令,我来介绍下

  • aload_0将本地变量表中第一个元素添加对操作栈中,和iload_0不同,这个操作针对的是jVM的reference类型,而不是int类型。

  • invokespectal 用于调用私有实例方法、构造器,以及使用 super 关键字调用父类的实例方法或构造器,和所实现接口的默认方法, 我们我们还会

  • invokestatic:用于调用静态方法

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

  • invokevirtual:用于调用非私有实例方法

  • invokeinterface:用于调用接口方法

  • new: 创建一个对象的实例

  • dup: 复制栈顶的值,并且将复制的值压入栈中。

0: aload_0
1: invokespecial #1                  // Method java/lang/Object."<init>":()V
4: aload_0
5: bipush        100
7: putfield      #2                  // Field age:I
10: return

20190706180011

一开始创建对象,此时他所有的地段的值都是为空。

就是想将这对象压入栈中,然后调用它父类的初始化方法,然后把100入栈,赋值给它的age字段,然后就结束了。过程其实很简单。

然后我们看下我们创建一个对象时候的

Monkey monkey = new Monkey()
         0: new           #3                  // class Monkey
         3: dup
         4: invokespecial #4                  // Method "<init>":()V
         7: astore_1

就是创建 =》 初始化方法 》 保存下

为什么创建一个对象需要三条指令呢? 首先,我们需要清楚类的构造器函数是以函数名出现的,被称为实例的初始化方法。调用 new 指令时,只是创建了一个类的实例,但是还没有调用构造器函数,使用 invokespecial 调用了 后才真正调用了构造器函数,正是因为需要调用这个函数才导致中间必须要有一个 dup 指令,不然调用完函数以后,操作数栈为空,就再也找不回刚刚创建的对象了。

@bitfishxyz bitfishxyz added the 字节码 JVM执行的字节码 label Jul 9, 2019
@bitfishxyz
Copy link
Member Author

Invoke method

接下说说调用一个方法相关的指令

invokestatic

调用一个静态方法。

public class Hello {
    public static void func(){
        hi();
    }
    public static void hi(){};
}
  public static void func();
    descriptor: ()V
    flags: (0x0009) ACC_PUBLIC, ACC_STATIC
    Code:
      stack=0, locals=0, args_size=0
         0: invokestatic  #2                  // Method hi:()V
         3: return
      LineNumberTable:
        line 3: 0
        line 4: 3

invokevirtual

调用一个成员方法

public class Hello {
    public void func(){
        hi();
    }
    public void hi(){};
}
         0: aload_0
         1: invokevirtual #2                  // Method hi:()V
         4: return

invokespecial

调用类初始化方法或者私有方法

public class Hello {
    public void func(){
        hi();
    }
    private void hi(){};
}
      stack=1, locals=1, args_size=1
         0: aload_0
         1: invokespecial #2                  // Method hi:()V
         4: return

然后这里你可能就有问题了,为什么调用一个方法会有这么多的指令,合并成一个invokemethod不行嘛?下面就来分析下为什么会有四个调用方法的指令。

invokevirtual vs invokespecial

这两个都是调用类的实例方法的,但是jvm为什么要设计两个指令呢?为什么不把它们合并在一起?

接下来我解释一下。

首先我们来看一个简单的示例:

public class Hello {
    public static void main(String[] args) {
        Animal animal;

        // 这里应该有一个给animal赋值的语句
        // 但是这里我把赋值语句省略了

        animal.run();
    }
}
class Animal{
    void run(){
        System.out.println("Animal is running.");
    }
}

我在代码注释的地方省略了一个赋值语句,然后现在我希望你在这个地方添加一个赋值语句,让程序正常执行。

你会填写什么呢?

同学们肯定会这样写:

public class Hello {
    public static void main(String[] args) {
        Animal animal;
        animal = new Animal();
        animal.run();
    }
}
class Animal{
    void run(){
        System.out.println("Animal is running.");
    }
}

运行上面的程序会打印出Animal is running.

如果你这样写,没有任何错误,但是现在我更新了一下代码。

public class Hello {
    public static void main(String[] args) {
        Animal animal;

        // insert a statement

        animal.run();
    }
}
class Animal{
    void run(){
        System.out.println("Animal is running.");
    }
}
class Cat extends Animal{
    @Override
    void run() {
        System.out.println("Cat is running.");
    }
}

这种时候,你会怎么填写呢?

如果你填写了

animal = new Cat()

然后再运行程序,结果会不同吗?

或者这样写:

public class Hello {
    public static void main(String[] args) {
    }
    static void run(Animal animal){
        animal.run();
    }
}

在编译阶段,我们能够确定Hello.run(Animal animal)这个函数里面的animal.run()的运行结果吗?

所以我们可以非常明确的发现:在编译阶段,当我们调用一个对象的public、protected、default方法时,我们如法确认在运行时期到底是执行了那个方法。因为这个方法有可能被子类覆盖了。

而对象的私有方法,super方法,只能在类的成员函数内部调用,不会出现被子类覆盖的情况。

我们的jvm区分了这两种情况。那些在编译阶段就可以确认的方法会被invokespecial指令调用,它的性能比invokevirtual更加的高。

示例

应为对象的私有方法,调用的super方法以及init方法在编译阶段可以完全确定,不会有任何疑问。

public class Hello {
    public static void main(String[] args) {
        Animal animal;

        // 给animal赋值
        // 但是这里我把赋值语句省略了

        animal.run();
    }
}
class Animal{
    void run(){
        System.out.println("Animal is running.");
    }
}

中间我们省略了一个复制语句,现在运行你添加一下代码,请问

当我执行animal.run的时候,控制台一定会答应这句话吗?

请你思考几分钟。

很多同学可能会这样想

public class Hello {
    public static void main(String[] args) {
        Animal animal;

        animal = new Animal();
        animal.run();
    }
}
class Animal{
    void run(){
        System.out.println("Animal is running.");
    }
}

然后打印出这句话。如果你这样想,没有任何错误,但是你可能胡萝了下面的这个情况。

public class Hello {
    public static void main(String[] args) {
        Animal animal;

        animal = new Cat();

        animal.run();
    }
}
class Animal{
    void run(){
        System.out.println("Animal is running.");
    }
}
class Cat extends Animal{
    @Override
    void run() {
        System.out.println("Cat is running.");
    }
}

发现了没有,虽然我们的animal是Animal类型,但是他的实例其实是一个Cat,这就导致了,在运行阶段,我们的animal 可能是一个animal,也可能是一个cat,我们没办法确定。

而对象的私有方法,super方法,只能在类的成员函数内部调用,不会出现被子类覆盖的情况,而init方法是new时候调用的,

我们的jvm区分了这两种情况,应为invokespecial在编译阶段就可以确定,所以它的性能比invokevirtual更加的高。

但是接下来我们在看一下这样的指令

public final class Hello {
    public static void main(String[] args) {
    }
    static void run(Runnable runnable){
        runnable.run();
    }
}

interface Runnable{
    void run();
}

class Animal implements Runnable{
    @Override
    public void run(){
        System.out.println("Animal is running.");
    }
}
         0: aload_0
         1: invokeinterface #2,  1            // InterfaceMethod Runnable.run:()V
         6: return

哎,好像多可以了一个指令。
这次调用一个方法的时候,没有使用invokevirtual或者invokespecial,而是invokeinterface,为甚有多了一个,?

invokeinterface 用于调用接口方法,在运行时再确定一个实现此接口的对象。 那它跟 invokevirtual 有什么区别呢?为什么不用 invokevirtual 来实现接口方法的调用?其实也不是不可以,只是为了效率上的考量。

invokestatic 指令需要调用的方法只属于某个特定的类,在编译期唯一确定,不会运行时动态变化,是最快的 invokespecial 指令可能调用的方法也在编译期确定,且只有少数几个需要处理的方法,查找也非常快

invokevirtual 和 invokeinterface 的关系就比较微妙了,区别没有那么明显,我们用一个实际的例子来说明,可以这么认为,每个类文件都关联着一个「虚方法表」(virtual method table),这个表中包含了父类的方法和自己扩展的方法。比如

为了便于理解,我换了一个示例。

class A {
    public void method1() { }
    public void method2() { }
    public void method3() { }
}

class B extends A {
    public void method2() { } // overridden from BaseClass
    public void method4() { }
}

对应的虚方法表:

20190706195447

现在 B 类的虚方法表保留了父类 A 中方法的顺序,只是覆盖了 method2() 指向的函数链接和新增了method4()。 假设这时需要调用 method2 方法,不管是A的method2还是B的method2,我们不关心,我们的invokevirtual 只需要直接去找虚方法表位置为 2 的地方的函数引用就可以了。不管这个对象是A的实例还是B的实例,它的method2方法都在虚方法表中索引为2的地方。

如果是用如果我们是接口的话,情况就不一样了。

interface X {
    void methodX()
}
class B extends A implements X {
    public void method2() { } // overridden from BaseClass
    public void method4() { }
    public void methodX() { }
}
Class C implements X {
    public void methodC() { }
    public void methodX() { }
}

20190706195840

可以看到,B和C都实现了X接口,但是二者有不同的方法,它们的虚方法表在内存排不上没有任何规律。当我们调用X的methodX方法时,我们没办法优化他,只能遍历一遍虚方法表来。

invokeinterface 不得不搜索整个虚方法表来找到对应方法,效率上远不如 invokevirtual。这就是为什么jvm需要两个不用的指令,主要是考虑性能优化的问题。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
字节码 JVM执行的字节码
Projects
None yet
Development

No branches or pull requests

1 participant