JVM基础知识

深入探讨JVM运行时数据区的结构与作用,包括程序计数器、虚拟机栈的功能及其在多线程环境下的重要性。通过具体代码示例,解析栈帧的工作原理及如何处理方法调用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

JVM运行时数据区是什么?是干什么的?如果你有兴趣了解,请看本章内容。

下面这张图给我们展示出了JVM运行时数据区的结构,我把它分为两个部分:数据+指令,我们先来看看各个子模块的作用

 

  • 程序计数器:指向当前线程正在执行的字节码指令的地址(行号)。它是线程私有的,独享的(为什么?)

那么为什么要有程序计数器?

原因:因为java的最小执行单位是线程,而线程执行指令最终还是要落在操作系统层面,操作系统层面即要在CPU上运行。在CPU上运行指令有一个不得不考虑的不稳定的因素-调度策略

调度策略是基于时间片的,所谓调度策略,举个例子说明:我开启了一个线程,占用了CPU的资源在看电影,突然有人要和我开视频聊天,那么我开启了视频,即开启了另外一个线程,此时看电影这个线程被挂起。当聊天结束后,我又接着看我的电影,那么此时就要有一个能记录之前看电影这个线程的执行地址(行号),这样我才能接着之前继续看。这个可以帮助线程记录执行位置的东东就是程序计数器。(线程不具备记忆功能,不能记住自己的执行位置,因此“记忆”这个工作就得程序计数器去做)。

现在你也就能明白为什么它是线程私有的,独享的了吧。每个线程都有一个自己的程序计数器,用于记录本线程的执行地址。

 

  • 虚拟机栈:存储当前线程运行方法时所需要的数据、指令、返回地址。

线程是一个执行者,它只负责做,不负责存储,因此线程在运行方法时所需要的数据、指令、返回地址就必须被存储起来,而存储这些数据的就是虚拟机栈。

虚拟机栈的结构:

 

虚拟机栈是一个栈,因此是后进先出的,即FILO。图中黄色部分表示的是一个虚拟机栈,绿色部分是一个栈帧,mehodOne()是一个方法,这里所表示的意思是一个方法就是一个栈帧。java程序中每一个方法都会在虚拟机栈中开辟一个栈帧,栈帧包括局部变量表、操作数栈、动态链接、出口,等等。下面以一个java方法来具体说明虚拟机栈的运行原理。

java代码:

public class JVMDemo {

	private Object obj = new Object();
	
	//局部变量
	public void methodOne(int i) {
		
		int j = 0;
		int sum = i + j;
		Object abc = obj;
		long start = System.currentTimeMillis();
		methodTwo();
		return;
	}

	private void methodTwo() {
		
		File file = new File("");
	}
	
	public static void main(String[] args) {
		
		JVMDemo demo = new JVMDemo();
		demo.methodOne(1);
	}
}

想要弄清楚程序到底是怎么执行的,我们反编译一下,用命令行,执行javap指令,下面就是反编译后的文件

 

找到执行methodOne方法的代码:

 

 public void methodOne(int);
    descriptor: (I)V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=7, args_size=2
         0: iconst_0
         1: istore_2
         2: iload_1
         3: iload_2
         4: iadd
         5: istore_3
         6: aload_0
         7: getfield      #12                 // Field obj:Ljava/lang/Object;
        10: astore        4
        12: invokestatic  #20                 // Method java/lang/System.currentTimeMillis:()J
        15: lstore        5
        17: aload_0
        18: invokespecial #26                 // Method methodTwo:()V
        21: return
      LineNumberTable:
        line 12: 0
        line 13: 2
        line 14: 6
        line 15: 12
        line 16: 17
        line 17: 21
      LocalVariableTable:
        Start  Length  Slot  Name   Signature
            0      22     0  this   Lcom/tongtong/jvm/HelloWorldDemo;
            0      22     1     i   I
            2      20     2     j   I
            6      16     3   sum   I
           12      10     4   abc   Ljava/lang/Object;
           17       5     5 start   J

 

着重看这一部分

 Code:
      stack=2, locals=7, args_size=2
         0: iconst_0
         1: istore_2
         2: iload_1
         3: iload_2
         4: iadd
         5: istore_3
         6: aload_0
         7: getfield      #12                 // Field obj:Ljava/lang/Object;
        10: astore        4
        12: invokestatic  #20                 // Method java/lang/System.currentTimeMillis:()J
        15: lstore        5
        17: aload_0
        18: invokespecial #26                 // Method methodTwo:()V
        21: return

关于javap的指令集,已准备好,点开https://mp.youkuaiyun.com/postedit/82820634,里面有详细的指令集说明,可以对照着看上面的代码。

下面进行分析:

  1. iconst_0  将int类型常量0压入栈
  2. istore_2   将int类型值存入局部变量2

 

上面的两个步骤就对应这两个指令。那为什么一定要存在局部变量2中呢?为什么不是0或者1中呢?

以为局部变量表1中存储的是程序中的参数i,验证方法如下:

int sum = i + j;

这是methodOne方法中的一句代码,对应反编译中的这三个指令:

 2: iload_1
 3: iload_2
 4: iadd
 5: istore_3

我们来看看指令的具体含义:

  1. iload_1 :从局部变量1中装载int类型值
  2. iload_2 :从局部变量2中装载int类型值
  3. iadd  : 执行int类型的加法
  4. istore_3 : 将int类型值存入局部变量3

很明显,iload_2装载的是j的值,那么iload_1装载的肯定是i的值,因此可以说明i是保存在局部变量1中的。那么问题来了,局部变量0中保存的是什么呢?答案是:this,具体为什么会是this,暂时还不清楚,有待后续学习。但是有一个地方,可以间接的说明这个问题,在反编译代码中有这么一句:args_size=2,表示的是方法中传入的参数的个数,在方法中明明只传了一个i,为什么会有两个?事实上,另一个参数就是this。

经过上述分析,我们可以得到下面这张图:

 

好了,证明结束。现在回过头来继续解析下面这四个指令:

  1. iload_1 :从局部变量1中装载int类型值
  2. iload_2 :从局部变量2中装载int类型值
  3. iadd  : 执行int类型的加法
  4. istore_3 : 将int类型值存入局部变量3

前两个指令是分别从局部变量1和2中装载int类型值到操作数栈中,然后执行iadd,得到sum的值,接着执行istore_3,把sum保存到局部变量3中,下图即展示了这几个过程:

从以上分析中大概可以得出结论,操作数栈是用来保存操作数或者中间结果的。而局部变量表,顾名思义,就是用来保存局部变量的,它是32位的。最开始的时候,里面是空的,在真正执行程序时才会填充数据。

 

出了局部变量,程序中还定义了成员变量,即Object obj = new Object(),并且在methodOne()中还把obj付给了局部变量abc。吗,额这个过程又改怎样表示呢?

 

看上图的红线部分,obj保存在堆(heap)中,而abc是保存在局部变量表中,是obj的一个引用,它指向堆中的obj,保存的是obj的地址。

 

  • 动态链接

动态链接突出的是java具有动态性,即java的运行时多态。Java的运行时多态就是用动态链接实现的。举个例子:

//定义接口
public interface UserService{

}

//定义类
public class UserController{

    @Autowired
    private UserService userService;

    public void addUser(User user){

        userService.save();
        .......
   }

}

 

以上面代码为例,我们定义了一个接口UserService,它有很多实现类(此处没给出具体实现),当我们注入UserService的实例时并执行save()方法时,事实上,jvm需要通过动态链接来动态的获取这个实例对应的具体实现类,因为这个接口可能有不止一个实现类,当执行方法时,我们要具体到某个实现类才可以。这也体现了java的运行时多态的特性。

 

  • 出口

这个好理解,程序不可能一直执行,所以必须要有一个出口。出口大致分为两种:正常出口和异常出口。正常出口,就是return;程序执行到这里就会正常退出。另外一个就是程序发生异常,导致异常退出。

 

解决了上述的诸多疑问之后,现在提出几个问题。

1.如果我们在methodOne()方法中调用methodTwo()方法,那么此时虚拟机栈中的栈帧是怎样排列的?

2.如果我们在methodThree()方法中执行递归调用,虚拟机栈中会开辟几个栈帧?(1 or n)执行时又会出现什么问题?

对于问题1,很简单也很容易理解。当调用methodTwo()方法后,很显然在虚拟机栈中会开辟一个新的栈帧,此时这个栈帧会放在methosOne()栈帧的上面,理由:FILO(后进先出)。

而对于问题2,我们可以写个程序运行一下。

 

 

这个异常是不是很熟悉,这个结果即是问题2 的答案。如果只开辟一个栈帧,那么怎么可能会有StackOverflowError异常发生。很明显,是开辟了n个栈帧。另外,针对栈帧有个概念,叫做栈深度。表明栈的深度是有限的,如果栈的空间占用达到了极限,那么就会报这个错误。

本人不擅长写文章,所写的都是根据个人的理解来阐述的,如有错误地方,请大家批评指正,大家一起进步。今天就先写到这,后面的会尽快补上,谢谢大家。

 

 

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值