JVM内存模型系列(虚拟机栈、本地方法栈、程序计数器)

JVM作为运行Java程序的平台,我们Java程序员必须要去了解它。JVM 能涉及非常庞大的一块知识体系, 比如内存结构、 垃圾回收、 类加载、 性能调优、 JVM 自身优化技术、 执行引擎、 类文件结构、 监控工具等。但是在所有的知识体系中, 都或多或少跟内存结构有一定的关系:比如垃圾回收回收的就是内存、 类加载加载到的地方也是内存、 性能优化也涉及到内存优化、 执行引擎与内存密不可分、 类文件结构与内存的设计有关系, 监控工具也会监控内存。 所以内存结构处于 JVM 中核心位置。 也是属于我们入门 JVM 学习的最好的选择。同时 JVM 是一个虚拟化的操作系统, 所以除了要虚拟指令之外, 最重要的一个事情就是需要虚拟化内存, 这个虚拟化内存就是我们马上要讲到的 JVM 的内存区域。

在这里插入图片描述
JVM内存结构可以分为五个模块加上一个直接内存。其中这些模块又可以分为两个大类,线程共享区域和线程私有区域。
线程共享区域:

  • 堆内存:JVM 上最大的内存区域, 我们申请的几乎所有的对象, 都是在这里存储的。
  • 方法区:JVM的逻辑划分,不同版本有不同实现。主要是用来存放已被虚拟机加载的类相关信息, 包括类信息、 静态变量、 常量、 运行时常量池、 字符串常量池等。

线程私有区域:

  • 虚拟机栈:JVM 运行过程中存储当前线程运行方法所需的数据, 指令、 返回地址。
  • 本地方法栈:Java程序调用底层C/C++函数库。
  • 程序计数器:当前线程执行的字节码的行号指示器。

直接内存:又叫堆外内存,它不是虚拟机运行时数据区的一部分,但是虚拟机部分逻辑会用到直接内存。

我们就从线程私有的区域开始讲起吧!

虚拟机栈

它的数据结构如其名,栈是一种FILO(先进后出)的数据结构,它的声明周期和线程息息相关,它的作用就是存储当前线程运行java方法所需的数据、指令、返回地址。(虚拟机栈在Java程序员口中简称,为了方便下文就简称栈)
JDK1.8官方指定栈的默认大小为1M,程序运行时可以用 -Xss命令指定大小
栈的结构:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200718173948320.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQyMjkwNTYx,size_16,color_FFFFFF,t_70

栈帧:在一个线程里,每当调用一个方法就会创建一个栈帧,并入栈,当方法执行完以后进行出栈

例如:在java代码中A()方法中调用了B()方法,B()方法中又调用了C()方法。那么线程执行A()时,创建一个栈帧入栈,执行B()方法时又创建一个栈帧入栈…,当C()方法执行完毕出栈,紧跟着B()结束也跟着出栈,直至栈底的栈帧出栈宣告完毕,此时线程也跟着消亡。
栈的组成元素时栈帧,那么栈帧里面长什么样子呢?我们刚说到,栈是拿来存数据, 指令、 返回地址的,那么这些东西肯定是存在栈帧中了,我们来看看栈帧的内部结构:

  • 局部变量表:顾名思义它是一张表来存数据的,而且是局部变量的数据。局部变量表中存储的数据是一个32位长度的数据,比如我们常见的8大基本类型变量,如果是double和long则使用32位高低位来标识,如果是对象,那么就存储对象的堆内存地址。
  • 操作数栈:顾名思义它的内存结构也是一个栈结构(先进后出),它的作用就是存储方法运行时执行引擎需要计算的数据。
  • 动态链接:解决符号引用相关问题(后续类加载机制时解析)。
  • 返回地址:方法执行完毕需要将程序计数器中的地址作为返回,便于后续栈帧执行。

光说概念是不是很枯燥,我们写段代码,通过反汇编来瞅瞅

/**
 * @author Minor
 */
public class Demo1 {

    public int test() {
        int a = 10;
        int b = 20;
        int c = (a+b)*2;
        return c;
    }

}

首先我们定义了一个非常普通的java方法test(),内部定义两个变量a,b然后计算他们的和再乘以2,赋值给c然后返回。我们先用javac命令编译一下得到Demo1.class文件,然后用javap -c命令查看这个class文件的反汇编指令代码,
为了方便,我直接在反汇编指令里面写注释:

 wangzhi@wangzhideMacBook-Pro   ~/Desktop/JavaBase/src/com/company/base  javap -c Demo1   
警告: 二进制文件Demo1包含com.company.base.Demo1
Compiled from "Demo1.java"
public class com.company.base.Demo1 {
  public com.company.base.Demo1();
    Code:
       0: aload_0
       1: invokespecial #1                  // Method java/lang/Object."<init>":()V
       4: return

  public int test();					// 我们java代码里定义的test()方法
    Code:								// 字节码指令
       0: bipush        10				// 将常量10压入操作数栈
       2: istore_1						// 将操作数栈的值10存储到局部变量表下标为1的位置
       3: bipush        20				// 将常量20压入操作数栈
       5: istore_2						// 将操作数栈的值10存储到局部变量表下标为2的位置
       6: iload_1						// 将局部变量表下标为1的变量压入操作数栈		
       7: iload_2						// 将局部变量表下标为2的变量压入操作数栈	
       8: iadd							// 加法运算,将操作数栈里的值进行求和
       9: iconst_2						// 将值为2的常量压入操作数栈
      10: imul							// 乘法运算
      11: istore_3						// 将操作数栈的值存储到局部变量表下标为3的位置
      12: iload_3						// 将局部变量表下标为3的变量压入操作数栈
      13: ireturn						// 方法返回
}

class指令集参考表:[https://cloud.tencent.com/developer/article/1333540]
(https://cloud.tencent.com/developer/article/1333540)
我们可以看到,一个简单的方法,解释成jvm指令时变得很复杂,但是每一步缺逻辑清晰。注意一点istore_n指令表示将操作数栈里的值存入局部变量表的下标n的位置。iconst_n表示将n常量压入操作数栈,常量是几,n就是几。
细心的小伙伴注意到了,当操作数栈存储常量10的时候,为什么存储的是局部变量表下标为1的位置。其实java代码底层对方法的调用有一个this,代表当前对象,所以局部变量表index[0]号位置是当前对象this的引用。

有一点需要注意的是,栈这个数据结构是先入后出,如果一个线程不断地入栈而没有出栈,就会造成栈溢出错误StackOverflowException,比如递归操作控制不当就会发生异常。

程序计数器

程序计数器是用来记录线程执行字节码的行号地址,因为现代计算机的工作模式基于CPU的时间片轮转机制,线程在执行程序的时候难免会遇到CPU调度问题,此时就需要一个地方来存储线程当前执行的位置。显然,每个线程各自独立,都有属于自己一份的程序计数器。由于结构简单,功能单一,程序计数器也是JVM内存模型中唯一不会发生内存溢出的地方。需要值得注意的点是当java线程在执行本地方法(native修饰的方法)时,程序计数器并不会记录执行位置,因为操作系统层面也有一个程序计数器,本地方法依靠它去记录。

本地方法栈

本地方发栈顾名思义也是一个栈结构,和虚拟机栈类似。虚拟机栈用于控制java方法的调用,而本地方法栈控制本地方法的调用。

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Minor王智

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值