专注于 JetBrains IDEA 全家桶,永久激活,教程
持续更新 PyCharm,IDEA,WebStorm,PhpStorm,DataGrip,RubyMine,CLion,AppCode 永久激活教程

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

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

103_1.pngJVM内存结构可以分为五个模块加上一个直接内存。其中这些模块又可以分为两个大类,线程共享区域和线程私有区域。 线程共享区域:

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

线程私有区域:

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

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

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

虚拟机栈

它的数据结构如其名,栈是一种FILO(先进后出)的数据结构,它的声明周期和线程息息相关,它的作用就是存储当前线程运行java方法所需的数据、指令、返回地址。(虚拟机栈在Java程序员口中简称,为了方便下文就简称栈) JDK1.8官方指定栈的默认大小为1M,程序运行时可以用 -Xss命令指定大小 栈的结构:

103_2.png

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

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

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

程序计数器

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

本地方法栈

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

未经允许不得转载:搜云库技术团队 » JVM内存模型系列(虚拟机栈、本地方法栈、程序计数器)

JetBrains 全家桶,激活、破解、教程

提供 JetBrains 全家桶激活码、注册码、破解补丁下载及详细激活教程,支持 IntelliJ IDEA、PyCharm、WebStorm 等工具的永久激活。无论是破解教程,还是最新激活码,均可免费获得,帮助开发者解决常见激活问题,确保轻松破解并快速使用 JetBrains 软件。获取免费的破解补丁和激活码,快速解决激活难题,全面覆盖 2024/2025 版本!

联系我们联系我们