技术文

Java字节码指令的执行

2019-01-30  本文已影响91人  java高并发

虚拟机运行活化的内存数据中的指令:程序的执行

二进制字节码转为内存中方法区里存储的活化对象,那么最重要的程序执行就做好了基础:当方法区里的字段和方法按照虚拟机规定的数据结构排好,常量池中的符号引用数据在加载过程中最大限度地转为了直接引用,那么这个时候虚拟机就可以在加载主类后创建新的线程按步执行主类的main函数中的指令了。

java虚拟机执行程序的基础是特定的二进制指令集和运行时栈帧:

栈帧的进一步划分:

现在我们使用一个综合实例来说明运行的整个过程:

源代码如下,逻辑很简单:

public class TestDemo {
    public static int minus(int x){
        return -x;
    }
    public static void main(String[] args) {
        int x = 5;
        int y = minus(x);
    }
}

我们可以分析它的二进制字节码,当然这里我们借助javap工具进行分析:

jinhaoplus$ javap -verbose TestDemo
Classfile /Users/jinhao/Desktop/TestDemo.class
  Last modified 2015-10-17; size 342 bytes
  MD5 checksum 4f37459aa1b3438b1608de788d43586d
  Compiled from "TestDemo.java"
public class TestDemo
  minor version: 0
  major version: 52
  flags: ACC_PUBLIC, ACC_SUPER
Constant pool:
   #1 = Methodref          #4.#15         // java/lang/Object."<init>":()V
   #2 = Methodref          #3.#16         // TestDemo.minus:(I)I
   #3 = Class              #17            // TestDemo
   #4 = Class              #18            // java/lang/Object
   #5 = Utf8               <init>
   #6 = Utf8               ()V
   #7 = Utf8               Code
   #8 = Utf8               LineNumberTable
   #9 = Utf8               minus
  #10 = Utf8               (I)I
  #11 = Utf8               main
  #12 = Utf8               ([Ljava/lang/String;)V
  #13 = Utf8               SourceFile
  #14 = Utf8               TestDemo.java
  #15 = NameAndType        #5:#6          // "<init>":()V
  #16 = NameAndType        #9:#10         // minus:(I)I
  #17 = Utf8               TestDemo
  #18 = Utf8               java/lang/Object
{
  public TestDemo();
    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 int minus(int);
    descriptor: (I)I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: iload_0
         1: ineg
         2: ireturn
      LineNumberTable:
        line 3: 0

  public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=3, args_size=1
         0: iconst_5
         1: istore_1
         2: iload_1
         3: invokestatic  #2                  // Method minus:(I)I
         6: istore_2
         7: return
      LineNumberTable:
        line 6: 0
        line 7: 2
        line 8: 7
}
SourceFile: "TestDemo.java"

这个过程是从固化在class文件中的二进制字节码开始,经过加载器对当前类的加载,虚拟机对二进制码的验证、准备和一定的解析,进入内存中的方法区,常量池中的符号引用一定程度上转换为直接引用,使得字节码通过结构化的组织让虚拟机了解类的每一块的构成,创建的线程申请到了虚拟机栈中的空间构造出属于这一线程的栈帧空间,执行主类的main方法:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=3, args_size=1
         0: iconst_5
         1: istore_1
         2: iload_1
         3: invokestatic  #2                  // Method minus:(I)I
         6: istore_2
         7: return
      LineNumberTable:
        line 6: 0
        line 7: 2
        line 8: 7
}

首先检查main的访问标志、描述符描述的返回类型和参数列表,确定可以访问后进入Code属性表执行命令,读入栈深度建立符合要求的操作数栈,读入局部变量大小建立符合要求的局部变量表,根据参数数向局部变量表中依序加入参数(第一个参数是引用当前对象的this,所以空参数列表的参数数也是1),然后开始根据命令正式执行:

0: iconst_5

将整数5压入栈顶

1: istore_1

将栈顶整数值存入局部变量表的slot1(slot0是参数this)

2: iload_1

将slot1压入栈顶

3: invokestatic  #2   // Method minus:(I)I

二进制invokestatic方法用于调用静态方法,参数是根据常量池中已经转换为直接引用的常量,意即minus函数在方法区中的地址,找到这个地址调用函数,向其中加入的参数为栈顶的值

6: istore_2

将栈顶整数存入局部变量的slot2

7: return

将返回地址中存储的PC地址返到PC,栈帧恢复到调用前

现在我们分析调用minus函数的时候进入minus函数的过程:

public static int minus(int);
    descriptor: (I)I
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=1, locals=1, args_size=1
         0: iload_0
         1: ineg
         2: ireturn
      LineNumberTable:
        line 3: 0

同样的首先检查minus函数的访问标志、描述符描述的返回类型和参数列表,确定可以访问后进入Code属性表执行命令,读入栈深度建立符合要求的操作数栈,读入局部变量大小建立符合要求的局部变量表,根据参数数向局部变量表中依序加入参数,然后开始根据命令正式执行:

0: iload_0

将slot0压入栈顶,也就是传入的参数

1: ineg

将栈顶的值弹出取负后压回栈顶

2: ireturn

将返回地址中存储的PC地址返到PC,栈帧恢复到调用前

这个过程结束后对象的生命周期结束,因此开始执行GC回收内存中的对象,包括堆中的类对应的java.lang.Class对象,卸载方法区中的类。

方法的解析和分派

上面这个例子中main方法里调用minus方法的时候是没有二义性的,因为从二进制字节码里我们可以看到invokestatic方法调用的是minus方法的直接引用,也就说在编译期这个调用就已经决定了。这个时候我们来说说方法调用,这个部分的内容在前面的类加载时候提过,在能够唯一确定方法的直接引用的时候虚拟机会将常量表里的符号引用转换为直接引用,这样在运行的时候就可以直接根据这个地址找到对应的方法去执行,这种时候的转换才能叫做我们当时提到的在连接过程中的解析。但是如果方法是动态绑定的,也就是说在编译期我们并不知道使用哪个方法(或者叫不知道使用方法的哪个版本),那么这个时候就需要在运行时才能确定哪个版本的方法将被调用,这个时候才能将符号引用转换为直接引用。这个问题提到的多个版本的方法在java中的重载和多态重写问题息息相关。

重载(override)

public class TestDemo {
    static class Human{
    }
    static class Man extends Human{

    }
    static class Woman extends Human{

    }
    public void sayHello(Human human) {
        System.out.println("hello human");
    }
    public void sayHello(Man man) {
        System.out.println("hello man");
    }
    public void sayHello(Woman woman) {
        System.out.println("hello woman");
    }
    public static void main(String[] args) {
        TestDemo demo = new TestDemo();
        Human man = new Man();
        Human woman = new Woman();
        demo.sayHello(man);
        demo.sayHello(woman);
    }
}

javap结果:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=4, args_size=1
         0: new           #7                  // class TestDemo
         3: dup
         4: invokespecial #8                  // Method "<init>":()V
         7: astore_1
         8: new           #9                  // class TestDemo$Man
        11: dup
        12: invokespecial #10                 // Method TestDemo$Man."<init>":()V
        15: astore_2
        16: new           #11                 // class TestDemo$Woman
        19: dup
        20: invokespecial #12                 // Method TestDemo$Woman."<init>":()V
        23: astore_3
        24: aload_1
        25: aload_2
        26: invokevirtual #13                 // Method sayHello:(LTestDemo$Human;)V
        29: aload_1
        30: aload_3
        31: invokevirtual #13                 // Method sayHello:(LTestDemo$Human;)V
        34: return
      LineNumberTable:
        line 21: 0
        line 22: 8
        line 23: 16
        line 24: 24
        line 25: 29
        line 26: 34

重写(overwrite)

public class TestDemo {
    static class Human{
        public void sayHello() {
            System.out.println("hello human");
        }
    }
    static class Man extends Human{
        public void sayHello() {
            System.out.println("hello man");
        }
    }
    static class Woman extends Human{
        public void sayHello() {
            System.out.println("hello woman");
        }
    }

    public static void main(String[] args) {
        Human man = new Man();
        Human woman = new Woman();
        man.sayHello();
        woman.sayHello();
    }
}

javap结果:

public static void main(java.lang.String[]);
    descriptor: ([Ljava/lang/String;)V
    flags: ACC_PUBLIC, ACC_STATIC
    Code:
      stack=2, locals=3, args_size=1
         0: new           #2                  // class TestDemo$Man
         3: dup
         4: invokespecial #3                  // Method TestDemo$Man."<init>":()V
         7: astore_1
         8: new           #4                  // class TestDemo$Woman
        11: dup
        12: invokespecial #5                  // Method TestDemo$Woman."<init>":()V
        15: astore_2
        16: aload_1
        17: invokevirtual #6                  // Method TestDemo$Human.sayHello:()V
        20: aload_2
        21: invokevirtual #6                  // Method TestDemo$Human.sayHello:()V
        24: return
      LineNumberTable:
        line 20: 0
        line 21: 8
        line 22: 16
        line 23: 20
        line 24: 24

我们可以看出来无论是重载还是重写,都是二进制指令invokevirtual调用了sayHello方法来执行的。

对象的创建和堆内存的分配

前面我们提到的都是类在方法区中的内存分配:

在方法区中有类的常量池,常量池中保存着类的很多信息的符号引用,很多符号引用还转换为了直接引用以使在运行的过程能够访问到这些信息的真实地址。

那么创建出的对象是怎么在堆中分配空间的呢?

首先我们要明确对象中存储的大部分的数据就是它对应的非静态字段和每个字段方法对应的方法区中的地址,因为这些东西每个对象都是不一样的,所以必须通过各自的堆空间存储这些不一样的数据,而方法是所有同类对象共用的,因为方法的命令是一样的,每个对象只是在各自的线程栈帧里提供各自的局部变量表和操作数栈就好。

这样看来,堆中存放的是真正“有个性”的属于对象自己的变量,这些变量往往是最占空间的,而这些变量对应的类字段的地址会找到位于方法区中,同样的同类对象如果要执行一个方法只需要在自己的栈帧里面创建局部变量表和操作数栈,然后根据方法对应的方法区中的地址去寻找到方法体执行其中的命令即可,这样一来堆里面只存放有限的真正有意义的数据和地址,方法区里存放共用的字段和方法体,能最大程度地减小内存开销。

上一篇下一篇

猜你喜欢

热点阅读