JVM内存模型——虚拟机栈详细讲解.md

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/u014453515/article/details/85052986

0.JVM运行时数据模型

在这里插入图片描述

Java 虚拟机的内存模型分为两部分:一部分是线程共享的,包括 Java 堆和方法区;另一部分是线程私有的,包括虚拟机栈和本地方法栈,以及程序计数器这一小部分内存。

1.程序计数器和本地方法栈

程序计数器和程序计数器比较简单,放在一块讲。

1.1 程序计数器是一块小的内存空间,线程私有的。

可以看做是当前线程所执行的字节码的行号指示器。每一个线程都有自己程序计数器。

如果线程正在执行的是一个Java方法,程序计数器的值就是正在执行的虚拟机字节码指令的地址;如果线程正在执行的是Native方法,这个程序计数器的值为空(undefined)。此内存区域是虚拟机规范中没有规定任何OutOfMemoryError情况的区域。

1.2 本地方法栈

本地方法栈与虚拟机栈所发挥的作用是非常相似的,它们之间的区别不过是:

虚拟机栈为虚拟机执行java方法,而本地栈则为虚拟机使用到的Native方法服务。Native方法是用C++实现的,在Java中以接口的方式存在,并以native修饰。

2.虚拟机栈的工作原理

虚拟机栈在Java方法被调用时起作用,虚拟机栈的栈元素是栈帧。每当一个Java方法执行时,方法对应的栈帧入栈;执行完毕后,对应栈帧出栈。栈帧有4个部分组成,局部变量表,操作数栈,动态链接和返回地址。
方法的参数和方法中定义的局部变量以及就存放在局部变量表中;方法内语句的操作数存放在操作数栈。Java 程序编译之后就变成了一条条字节码指令。当执行到一条语句有n个操作数时,就用操作数栈顶中取出n个操作数,指令完成对应的计算,然后把对应的结果入栈(如果结果被赋值给了变量)。虚拟机栈的进出栈顺序是FILO原则,即先入后出,后入先出原则。比如A方法中调用B方法,那么进出栈的过程是:A进栈,B进栈,B出栈,A出栈。

下面我们写一个简单的方法。通过反编译得到字节码:
执行命令: javap -v xxx.class

public int hello(int i){
    int j =10;
    int k = i+j;
    long l = 110L;
    System.out.println(l);
    return k;
}
对应的字节码:
public int hello(int);
    descriptor: (I)I
    flags: ACC_PUBLIC
    Code:
      stack=3, locals=6, args_size=2
         0: bipush 10
         2: istore_2
         3: iload_1
         4: iload_2
         5: iadd
         6: istore_3
         7: ldc2_w #2 // long 110l
        10: lstore 4
        12: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;
        15: lload 4
        17: invokevirtual #5 // Method java/io/PrintStream.println:(J)V
        20: iload_3
        21: ireturn

解读下Java指令的运行过程(注意下面的“栈”,都是指“操作数栈”):

0: bipush 10 将一个byte型常量值10推送至栈顶,供下一条指令使用
2: istore_2 将栈顶int型数值(10)存入局部变量表的第三个局部变量
以上条指令对应语句:int j =10;
3: iload_1 从局部变量表中获取第二个int型局部变量进栈,第二个局部变量是hello方法的int参数i
4: iload_2 从局部变量表中获取第三个int型局部变量进栈,即j
5: iadd 栈顶两int型数值相加,并且结果进栈
6: istore_3 取栈顶(iadd 的结果)int型数值存入局部变量表的第4个局部变量,即k
7: ldc2_w 将long或double型常量值从常量池中推送至栈顶(宽索引)
10: lstore 4 将栈顶long型数值存入局部变量5,即l
12: getstatic 获取指定类的静态域(java/lang/System.out:Ljava/io/PrintStream),并将其值压入栈顶,对应语句System.out.println(l);
15: lload 4 将long型局部变量5进栈
17: invokevirtual 调用实例方法 java/io/PrintStream.println:(J)V
20: iload_3 从局部变量表中获取第4个int型局部变量进栈,即k
21: ireturn 当前方法返回int

这是hello方法在JVM中的执行过程。
下面问几个问题

1.局部变量表的第一个局部变量是什么?

是this,即当前对象,注意这个方法是成员方法才有this,如果是静态方法就没有this了
我们看下字节码文件hello方法中有这样的信息,LocalVariableTable就是局部变量表:

 LocalVariableTable:
        Start Length Slot Name Signature
            0 22 0 this Lcom/wy/jvm/StackDemo;
            0 22 1 i I
            3 19 2 j I
            7 15 3 k I
           12 10 4 l J

2.我们说一个方法对应一个栈帧,那么递归调用会有一个栈帧,还是多个栈帧?

多个,一个方法运行时,便产生一个栈帧,多次执行就创建多个栈帧。我们可以验证下,当一个方法递归调用死循环时,会抛出StackOverflowError

3.动态链接的作用是什么

支撑运行时的动态特性。举个例子:

class A{
private IServer serv;
public void hello(){
    serv.work();
}
}

我们A有一个成员对象serv,它的类型是一个接口。那么在执行hello方法时,它执行IServer 的work接口,那么接口是不能执行的,serv只是一个引用,就得去找IServer的实例对象。那么实例对象的地址就存放在动态链接过程,可以把这个场景类比于Spring的依赖注入执行过程。

4.局部变量引用了成员对象,那么它在局部变量表中怎么存的?

class A{
private B b = new B();
public void hello(){
    Object c = b;
}
}

例如上面情况,局部变量表怎么存储c?
我们知道局部变量表中,用32位空间存储变量,包括了8中基本类型和引用类型。基本类型变量直接存储,引用类型存的是一个指针。因为引用类型实例我们称为对象,对象是存在堆里面的,局部变量表中就存一个对象的指针,指向堆。这是一个栈指向堆的例子。

猜你喜欢

转载自blog.csdn.net/u014453515/article/details/85052986