JVM运行时内存模型之一【栈】

虚拟机栈【Java Virtual Machine Stacks】线程私有

作用:存储线程执行方法方法时的局部变量表,操作数栈,动态链接,方法出口等信息。

基础含义:虚拟机栈是一种基于栈的数据结构,遵循“先进后出”原则。每当一个方法被调用时,JVM就会为该方法创建一个新的栈帧,并压入当前线程的虚拟机栈中;当方法执行完毕后,相应的栈帧会被弹出并丢弃。栈帧中保存了方法的局部变量表,操作数栈,动态链接信息以及方法返回地址等重要信息。

规则一:虚拟机栈在遵循栈数据结构的原则的,“先进后出”原则。

其实非常好理解这个原则,大家都玩过游戏对吧,知道枪对吧,枪里面有弹夹,那么手枪弹夹的设计原理与栈的先进后出的原则是一模一样的。我们第一颗放入的子弹,那肯定是最后一颗被射出,最后一颗放入到弹夹中的子弹,是最先被射出的。如下图。

规则二:每当一个方法被调用时,JVM就会为该方法创建一个新的栈帧,并压入当前线程的虚拟机栈中。

一、什么是栈帧?

回答:栈帧是Java虚拟机中用于支持方法调用和执行的数据结构,它在每个线程的虚拟机栈中占据一段内存空间。每当一个java方法被调用时,JVM就会为该方法创建一个新的栈帧,并将其压入调用该方法的线程的虚拟机栈顶部。

二:什么时候栈帧会被创建?

1、方法调用时:当程序执行到一个方法调用语句时,为了执行该方法,会为其创建一个新的栈帧。

2、异常处理时:当异常被抛出并需要处理时,也可能创建一个新的栈帧来处理异常情况。

3、递归调用时:在递归方法中,每次方法自我调用时,都会为新的调用层级创建一个新的栈帧。

三、那么栈帧中包含了那些东西呢?
1、局部变量表

作用:存储方法参数和局部变量。参数从索引0开始存放,紧随其后的是其他局部变量。基础数据类型直接存储值,引用类型存储的是对象的地址。

接下来我们一一举例:

  public void demo(int a,int b){
        int c = 10;
        int d = 20;
    }

方法参数:

上述代码中,方法参数就是,int a 与 int b啦。

局部变量:

上述代码中,局部变量就是:int c 与 int d 啦。

2、操作数栈

作用:用于执行方法内的算术运算,逻辑比较操作。操作数栈遵循后进先出原则,操作时现将操作数压入栈顶,运算完成后将结果从栈顶弹出。

操作数栈的具体用途有如下几个:

1、运算操作:当执行诸如加减乘除,逻辑运算等操作时,操作数会被从局部变量表加载到操作数栈中,运算完成之后,结果再存回操作数栈或局部变量表。

2、方法调用与返回:方法调用时,参数从操作数栈或局部变量中取得并压入调用方法的操作数栈;方法返回时,返回值会被压入调用者的方法的操作数栈中。

3、数据转换:不同类型的数据在运算前,可能需要再操作数栈上进行类型转换。【这里就是隐式转换的地方

举例说明:

public String int add(int a, int b) {
    int sum = a + b;
    return sum;
}

第一步:在add方法被调用时,会为该方法新建一个栈帧,并压入到虚拟机栈的栈顶。我们可以看见add方法有两个入参,那么这两个入参a与b的赋值是直接从调用者方法的栈帧中获取,然后在add方法的栈帧初始化时,将a与b会存入到新的栈帧的局部变量表中,以便后续计算使用。

第二步:执行a+b=sum,a与b会从局部变量表中被加载到操作数栈中。首先是a被压入操作数栈,紧接着就是b;然后由虚拟机来执行加法指令,该指令会直接将a与b这两个数从操作数栈中弹出(弹出之后的a与b将不会再存在操作数栈中),并进行运算,得到的结果会压回到操作数栈中。

第三步:我们知道等号是赋值作用,那么在计算完成之后,虚拟机就会将a+b的结果从操作数栈中弹出,通过赋值指令将结果赋值给sum,sum就会被保存到局部变量表中,以供后续使用。

拓展:请问,sum是在a+b计算之前就已经在局部变量表中创建好了,等待a+b的赋值,还是说a+b的计算完成之后才创建的sum?

回答:在Java中,变量sum在a+b执行之前就已经在局部变量表中创建好了,那么此时的sum是处于未赋值状态,那么int类型的sum,默认值就是0,等到a+b计算完成之后,再次进行赋值操作。

3、动态链接

作用:每个栈帧都包含对运行时常量池中该方法所属类的符号引用,以支持方法调用过程中的动态绑定。它允许程序再运行时解析和绑定对其他代码或库函数的调用,而不是在编译或加载时完成这种绑定。这种机制使得程序可以在不修改原有代码的情况下,灵活地使用或替换库函数,增强了软件的灵活性和可维护性。JVM的动态链接主要体现在方法调用,尤其是虚方法调用上。

代码举例说明:

// 基类:动物
abstract class Animal {
    abstract void makeSound(); 
}

// 子类:狗
class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("汪汪汪");
    }
}

// 子类:猫
class Cat extends Animal {
    @Override
    void makeSound() {
        System.out.println("喵喵喵");
    }
}

public class Game {
    public static void main(String[] args) {
        Animal myPet = new Dog(); 
        myPet.makeSound(); 
    }
}

分析案例:我们来看Animal类,这是一个抽象类,其中有一个抽象方法makeSound(),该方法表示的含义是,动物会发出叫声,Dog与Cat分别继承了Animal,并重写了makeSound方法,他们分贝提供了具体的实现,再来看Gmae类的main方法,案例中创建了一个Dog对象,并使用了Animal类型的引用myPet来指向Dog对象。当程序运行时,myPet.makeSound()时,JVM会根据myPet实际指向的对象类型(Dog),动态地调用Dog类中的makeSound方法,打印出“汪汪汪”。这就是动态链接的一个典型应用:在运行时,根据对象的实际类型来决定调用那个具体方法。

4、返回地址

作用:存储了方法执行完毕之后需要返回的程序计数器的值,即方法调用结束后控制权应传递到下一条指令的地址。简而言之,它告诉程序当前方法执行完毕后应该去哪里继续执行代码。

代码案例如下:

public class ReturnAddressExample {
    public static void main(String[] args) {
        int a = 10;
        int b = 20;
        int sum = add(a, b);
        System.out.println("Sum: " + sum); 
    }

    public static int add(int x, int y) {
        int result = x + y;
        return result; 
    }
}

在上述代码中,main方法在调用add方法时,会为add方法新建一个栈帧,其中包含局部变量与操作数栈等内容。同时,栈帧中还会记录一个返回地址,该地址指向add方法调用后main方法中下一条指令,即System.out.println("Sum: " + sum);这行代码。当add方法执行完毕并执行到return result;时,虚拟机将根据栈帧中记录的返回地址,将控制权交还给main方法,继续执行下一条指令。

那么请问返回地址记录的内容是什么呢?

回答:按照上述代码案例来说,此处的返回地址记录的就是System.out.println("Sum: " + sum);这一行指令在内存中的地址。【也可以认为是程序计数器在方法调用之后应设置的值,但是这个我们现在先不涉及,后续会在程序计数器的文章中再详细说明】

5、附加信息

作用:附加信息是栈帧中除了局部变量表,操作数栈,动态链接和返回地址之外的额外数据,它们通常是为支持更复杂的程序执行和管理需要而设计的。这些信息可能包含但不限于异常处理信息,调试信息等。

异常处理信息

在Java中,异常处理是通过try-catch-finally结构来实现的。为了支持这一机制,栈帧中会包含异常处理表。异常处理表记录了方法中各个异常处理块的范围以及对应的处理代码入口。当方法执行过程中抛出异常时,JVM会根据异常处理表来查找合适的catch块来处理该异常。

try {
    // 可能抛出异常的代码
} catch (IOException e) {
    // 处理IOException的代码
} finally {
    // 无论是否发生异常都会执行的代码
}

在上述代码中,栈帧会记录try块的开始和结束位置,以及对应的catch块的入口地址,以便在抛出异常时准确跳转。

调试信息

为了便于程序调试,栈帧中可能包含额外的调试信息,如源代码行号、变量名、方法名等。这些信息有助于调试工具(如IDE的调试器)提供详细的调用栈跟踪、断点设置、变量观察等功能。

规则三:为什么说虚拟机栈是线程私有的

1、线程隔离性:每个线程都有自己独立的执行路径和上下文环境。虚拟线程存储了方法的调用信息,如过栈是线程共享的,那么一个线程执行方法调用时可能会干扰到其他线程的调用栈,导致数据混乱或安全问题。例如:一个线程在执行某个方法时,如果能访问到另一个线程的栈帧,可能会错误地修改另一个线程的数据或方法返回值。

2、安全性:线程私有栈可以有效避免数据竞争和同步问题。由于每个线程只影响自己的栈,因此不需要在访问栈数据时引入复杂的锁机制,降低了死锁和竞态条件的风险。

3、设计简洁性:线程私有的栈使得设计和实现更加简单清晰。每个线程可以独立地管理自己的栈空间,无需担心线程间的数据共享和协调问题,减少了编程和调试的复杂度。

4、内存管理效率:线程私有的栈可以独立分配和回收,避免了线程间内存访问的冲突,提高了内存分配和回收的效率。当一个线程结束时,其对应的栈可以立即被回收,而不会影响到其他线程。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值