Loading

Java虚拟机(1)——内存结构&栈

废话

最近把《深入理解JVM原理》看完了,这种书理论性太强了,我怕不写点什么我自己过两天就忘了,写来加深印象。

概述

100%JVM教程或者是书上都会有这张图。。。

啊,,,,这是JVM运行时的内存结构。很多Java基础教程都会提到Java中的堆和栈,但实际上Java的运行时内存结构要更为复杂一些。这里我只介绍栈,剩下的后面再说。

  • 虚拟机栈: 就是我们平时说的栈,也是一会主要介绍的东西。Java程序员常说方法调用本质上就是栈,可以看出栈主要管的就是Java中的方法调用。
  • 本地方法栈: 用于调用Native方法。有些底层方法或者平台相关的方法需要用本地代码实现,成为Native方法。

虚拟机栈

你应该先知道栈是啥,我想都nm学JVM了,不会有人不知道栈这种最基本的数据结构吧。

不知道请点这里

虚拟机栈里都有啥

主要就三个东西:局部变量表操作数栈帧数据区

倒也不是没有别的,后面也会介绍。

虚拟机栈长啥样??

栈是以栈帧(Stack Frame)为单位的,一个栈帧就代表一次方法调用。下图就是使用Idea自带的Debugger中查看的栈帧。

对应代码如下

public static void main(String[] args) {
    method1();
}
public static void method1(){
    method2();
}

public static void method2(){
}

那栈帧里都有啥?用一个简单的Java代码说明

public static void main(String[] args) {
    int a = 10;
    int b = 20;
    int c = a + b;
}

编译,使用javap -v <类名>查看字节码,其中包含如下信息:

public static void main(java.lang.String[]);
descriptor: ([Ljava/lang/String;)V
flags: ACC_PUBLIC, ACC_STATIC
Code:
    stack=3, locals=4, args_size=1
        0: bipush    10
        //...省略其他字节码
    LineNumberTable:
        line 5: 0
        //...省略其他行号映射

    LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      11     0  args   [Ljava/lang/String;
            3       8     1     a   I
            6       5     2     b   I
           10       1     3     c   I

首先前三行就是一些关于方法的描述信息。
第一行是该方法的签名。
第二行是方法的参数和返回值描述符,是(参数)返回值形式,[代表是数组,L代表是引用类型,[Ljava/lang/String就是一个java.lang包下的引用类型String组成的数组,也就是字符串数组。V就代表返回类型是void
第三行就是方法的修饰符信息,代表该方法被public static两个修饰符修饰。

然后第五行我们先略过,直接看下面,下面是根据方法中的Java代码生成的JVM可识别的Java字节码,JVM会使用上文提到的操作数栈来执行这些字节码代表的运算,最后返回结果。

所以第五行stack=3就是操作数栈的大小为3,编译器会自动判断这个大小,确保能完成方法中所有操作。locals=4是方法中有四个局部变量(分别是方法参数和abc三个)。arg_size=1就是参数有一个。

操作数栈

比如我们要执行上面a+b的操作,翻译成字节码就是这样的(我们只关注下面代码中没有注释的):

0: bipush        10
//2: istore_1
3: bipush        20
//5: istore_2
//6: iload_1
//7: iload_2
8: iadd
//9: istore_3
//10: return

首先是两个bipush指令分别把整形的1020压入操作数栈,然后iadd操作把栈顶的两个操作数取出,相加,再入操作数栈。用图片来说就是这样的:

不过实际过程要比这个复杂,上述只是为了便于理解,实际则是这样的:

0: bipush        10  // 把10压入操作数栈
2: istore_1          // 把10弹出,存给局部变量表第一个变量槽
3: bipush        20  // 把20压入操作数栈
5: istore_2          // 把20弹出,存给局部变量表第二个变量槽
6: iload_1           // 从局部变量表槽1加载数据
7: iload_2           // 从局部变量表槽2加载数据
8: iadd              // 将前面两个操作数相加,将结果压入操作数栈
9: istore_3          // 存给局部变量表第三个变量槽
10: return           // 方法结束

上面的代码注释很全了,就是JVM在运行时做的事,上面提到了局部变量表,康康它是啥玩意儿。

局部变量表

回顾上面使用javap命令查看到的字节码,里面有这样一段,就是局部变量表

LocalVariableTable:
    Start  Length  Slot  Name   Signature
        0      11     0  args   [Ljava/lang/String;
        3       8     1     a   I
        6       5     2     b   I
       10       1     3     c   I

局部变量表中保存的就是方法中的局部变量,包括方法参数,局部变量和实例方法中的隐含参数this

局部变量表中有一些槽(Slot),用于存放变量值,对于基本类型是值,对于对象则是存放的引用。

把基本类型直接存放在虚拟机栈里有一个好处,我们知道一个栈帧代表一个方法调用,方法结束,栈帧销毁,存在栈帧中的变量也随之销毁,而不需要GC垃圾回收器干预。Java编译器在面对一些可展开的对象时也会把对象直接展开成若干个基本类型,从而降低垃圾回收器的工作量。如果此系列还有续的话(如果我不懒的话)应该会在后面说到。

我们看上面的变量槽,正好能和字节码命令中istoreiload指令下划线后面的数字对应,这就证明虚拟机会为方法中每一个局部变量开辟一个单独的槽。

槽复用

如果方法中有些作用域更小的变量,它在方法执行完成前就被释放,在它后面定义的变量就可以复用它的槽。比如:

public static void main(String[] args) {
    int a = 10;
    int b = 20;
    {
        int d = 10; // 下面的c可以复用d的槽
    }
    int c = a + b;
}

帧数据区

帧数据区中存储栈帧中一些方法参数和返回值的东西。

动态链接

先来说常量池,Java的Class文件中就有常量池的描述,JVM加载类的时候会根据这个描述在方法区中建立运行时常量池。常量池里存的就是Java程序运行时产生的一些常量,比如方法名、变量名、类名等等。而虚拟机栈需要在运行时动态链接到当前栈帧所属方法的引用。

我们看看下面这段程序和它对应的字节码。

public static void main(String[] args) {
    method1();
    method2();
}
public static void method1(){

}
public static void method2(){

}
Code:
    stack=0, locals=1, args_size=1
        0: invokestatic  #2                  // Method method1:()V
        3: invokestatic  #3                  // Method method2:()V
        6: return

可以看到第一二条指令是invokestatic,字面意思就是调用静态方法,java有五个方法调用相关的字节码,分别是invokestaticinvokespecialinvokevirtualinvokeinterfaceinvokedynamic,会在后面说到。

这两个invokestatic分别调用了#2#3,这两个东西在常量池里,我们看看这个类字节码中的常量池信息。

Constant pool:
   #1 = Methodref          #5.#21         // java/lang/Object."<init>":()V
   #2 = Methodref          #4.#22         // io/lilpig/jvm_learn/stack/StackLearn_01.method1:()V
   #3 = Methodref          #4.#23         // io/lilpig/jvm_learn/stack/StackLearn_01.method2:()V
   #4 = Class              #24            // io/lilpig/jvm_learn/stack/StackLearn_01
   #5 = Class              #25            // java/lang/Object
   #6 = Utf8               <init>
   #7 = Utf8               ()V
   #8 = Utf8               Code
   #9 = Utf8               LineNumberTable
  #10 = Utf8               LocalVariableTable
  #11 = Utf8               this
  #12 = Utf8               Lio/lilpig/jvm_learn/stack/StackLearn_01;
  #13 = Utf8               main
  #14 = Utf8               ([Ljava/lang/String;)V
  #15 = Utf8               args
  #16 = Utf8               [Ljava/lang/String;
  #17 = Utf8               method1
  #18 = Utf8               method2
  #19 = Utf8               SourceFile
  #20 = Utf8               StackLearn_01.java
  #21 = NameAndType        #6:#7          // "<init>":()V
  #22 = NameAndType        #17:#7         // method1:()V
  #23 = NameAndType        #18:#7         // method2:()V
  #24 = Utf8               io/lilpig/jvm_learn/stack/StackLearn_01
  #25 = Utf8               java/lang/Object

我们看到#2#3分别就是我们的method1method2的方法引用,这个方法具体在哪是后话,就知道我们的虚拟机栈需要动态链接到常量池找它就好了。然后可以看到#2#3又分别引用了#4.#22#4.#23,然后那些东西又引用了其它的。

所以常量池的优点就是能把一些公共的东西抽取出来,以引用的形式访问。比如上图的#7,是一个代表void返回值的Utf8字面量,程序中很多地方要用到,以这种常量池的方式只需要在这里声明一次就好了。

方法链接

前面说,JVM字节码中有五个调用方法相关的字节码命令,分别是invokestaticinvokespecialinvokevirtualinvokeinterfaceinvokedynamic。前四个分别用于调用静态方法,调用实例方法,调用重载的方法和调用接口方法。第五个是Java为支持动态类型的一些特性新增的字节码指令,在JDK8中使用Lambda表达式就会被编译成一个invokedynamic字节码。

前四个被分为两大类,分别是静态链接和动态链接,怎么区别呢?就是你这个方法,编译器能直接确定调用哪个版本,那就是静态链接,静态链接的两条指令是invokestaticinvokespecial,反之,如果这个方法编译器无法确定直接调用哪个版本,而是要留到JVM在运行期间确定,那就是动态链接,两条指令是invokeinterfaceinvokevirtual。(我认为invokedynamic也算动态链接)。

那么哪些方法在编译期间可以确定版本呢?肯定是那些不能支持多态的方法,它们就没有版本这一说,要调用就只有一个。比如static方法、final方法、private方法都不能被重写。而普通的实例方法,实现的接口方法则都得等到运行期间才能确定调用哪个版本,这种方法也称为虚方法(virtual method)。

或者你在代码中写明了要调用哪个版本,如使用thissuper限定,这样编译器也能在编译期确定方法版本。

值得一提的是final方法虽然也算静态链接,但是使用的字节码仍然是invokevirtual

动态链接对应的绑定方法称为晚期绑定,静态链接称为早期绑定。

看下面的代码,又是一个因为讲解而拼凑的无意义代码。

public class StackLearn_02 {
    public static void main(String[] args) {
        Creature creature = getCreature();
        creature.born();
        creature.die();
    }

    public static Creature getCreature(){
        return new Human();
    }
}

interface Creature{
    void born();
    void die();
}

class Human implements Creature{

    @Override
    public void born() {
        System.out.println("Human born...");
    }

    @Override
    public void die() {
        System.out.println("Human die...");
    }
}

这个getCreature方法是静态的,所以编译期能确定,肯定使用的是invokestatic。而creature.borncreature.die编译器无法确定版本,所以使用invokeinterface。字节码如下:

 0 invokestatic #2 <io/lilpig/jvm_learn/stack/StackLearn_02.getCreature>
 3 astore_1
 4 aload_1
 5 invokeinterface #3 <io/lilpig/jvm_learn/stack/Creature.born> count 1
10 aload_1
11 invokeinterface #4 <io/lilpig/jvm_learn/stack/Creature.die> count 1
16 return

第五个,我已经累了,不想写了,直接看代码。。。

public class StackLearn_03 {
    interface IntFunc{
        int apply(int x);
    }
    public static void main(String[] args) {
        IntFunc func = x -> x*x;
        func.apply(10);
    }
}
 0 invokedynamic #2 <apply, BootstrapMethods #0>
 5 astore_1
 6 aload_1
 7 bipush 10
 9 invokeinterface #3 <io/lilpig/jvm_learn/stack/StackLearn_03$IntFunc.apply> count 2
14 pop
15 return
posted @ 2020-09-21 20:20  yudoge  阅读(226)  评论(0编辑  收藏  举报