<JVM上篇:内存与垃圾回收篇>03-程序计数器 | 虚拟机栈(二)

简介: <JVM上篇:内存与垃圾回收篇>03-程序计数器 | 虚拟机栈

4. 虚拟机栈

4.1. 虚拟机栈概述

4.1.1. 虚拟机栈出现的背景

由于跨平台性的设计,Java 的指令都是根据栈来设计的。不同平台 CPU 架构不同,所以不能设计为基于寄存器的。


优点是跨平台,指令集小,编译器容易实现,缺点是性能下降,实现同样的功能需要更多的指令。


4.1.2. 初步印象


有不少 Java 开发人员一提到 Java 内存结构,就会非常粗粒度地将 JVM 中的内存区理解为仅有 Java 堆(heap)和 Java 栈(stack)?为什么?


4.1.3. 内存中的栈与堆


栈是运行时的单位,而堆是存储的单位


栈解决程序的运行问题,即程序如何执行,或者说如何处理数据。

堆解决的是数据存储的问题,即数据怎么放,放哪里


9dab669873c1c70d258539f7b46be106.png


4.1.4. 虚拟机栈基本内容


Java 虚拟机栈是什么?

Java 虚拟机栈(Java Virtual Machine Stack),早期也叫 Java 栈。每个线程在创建时都会创建一个虚拟机栈,其内部保存一个个的栈帧(Stack Frame),对应着一次次的 Java 方法调用,是线程私有的。


202203231706992.png


生命周期

生命周期和线程一致


作用

主管 Java 程序的运行,它保存方法的局部变量(8种基本数据类型、对象的引用类型)、部分结果,并参与方法的调用和返回。


补充:


局部变量 VS 成员变量(或属性)

基本数据变量 VS 引用类型变量(类、数组、接口)

栈的特点

栈是一种快速有效的分配存储方式,访问速度仅次于PC计数器。


JVM 直接对 Java 栈的操作只有两个:


每个方法执行,伴随着进栈(入栈、压栈)

执行结束后的出栈工作

对于栈来说不存在垃圾回收问题(栈存在溢出的情况)


5ecd4490ae34246c4a6a709a04a00ed2.png


面试题:开发中遇到哪些异常?

栈中可能出现的异常


Java 虚拟机规范允许Java 栈的大小是动态的或者是固定不变的。


如果采用固定大小的 Java 虚拟机栈,那每一个线程的 Java 虚拟机栈容量可以在线程创建的时候独立选定。如果线程请求分配的栈容量超过 Java 虚拟机栈允许的最大容量,Java 虚拟机将会抛出一个StackOverflowError 异常。


如果 Java 虚拟机栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的虚拟机栈,那 Java 虚拟机将会抛出一个 OutOfMemoryError 异常。

public static void main(String[] args) {
    main(args);
}
//抛出异常:Exception in thread "main" java.lang.StackOverflowError
//程序不断的进行递归调用,而且没有退出条件,就会导致不断地进行压栈。

设置栈内存大小

我们可以使用参数 -Xss 选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度

/**
 * 演示栈中的异常:StackOverflowError
 * @author shkstart
 * @create 2020 下午 9:08
 *
 *  默认情况下:count : 11417
 *  设置栈的大小: -Xss256k : count : 2460
 */
public class StackErrorTest {
    private static int count = 1;
    public static void main(String[] args) {
        System.out.println(count);
        count++;
        main(args);
    }
}


4.2. 栈的存储单位

4.2.1. 栈中存储什么?

每个线程都有自己的栈,栈中的数据都是以栈帧(Stack Frame)的格式存在

在这个线程上正在执行的每个方法都各自对应一个栈帧(Stack Frame)。

栈帧是一个内存区块,是一个数据集,维系着方法执行过程中的各种数据信息。

4.2.2. 栈运行原理


JVM 直接对 Java 栈的操作只有两个,就是对栈帧的压栈和出栈,遵循“先进后出”/“后进先出”原则。


在一条活动线程中,一个时间点上,只会有一个活动的栈帧。即只有当前正在执行的方法的栈帧(栈顶栈帧)是有效的,这个栈帧被称为当前栈帧(Current Frame),与当前栈帧相对应的方法就是当前方法(Current Method),定义这个方法的类就是当前类(Current Class)。


执行引擎运行的所有字节码指令只针对当前栈帧进行操作。


如果在该方法中调用了其他方法,对应的新的栈帧会被创建出来,放在栈的顶端,成为新的当前帧。


4a6e50b80dd045d690cff272de22a82d.jpg


不同线程中所包含的栈帧是不允许存在相互引用的,即不可能在一个栈帧之中引用另外一个线程的栈帧。


如果当前方法调用了其他方法,方法返回之际,当前栈帧会传回此方法的执行结果给前一个栈帧,接着,虚拟机会丢弃当前栈帧,使得前一个栈帧重新成为当前栈帧。


Java 方法有两种返回函数的方式,一种是正常的函数返回,使用 return 指令;另外一种是抛出异常。不管使用哪种方式,都会导致栈帧被弹出。


/**
 * @author shkstart
 * @create 2020 下午 4:11
 *
 * 方法的结束方式分为两种:① 正常结束,以return为代表  ② 方法执行中出现未捕获处理的异常,以抛出异常的方式结束
 *
 */
public class StackFrameTest {
    public static void main(String[] args) {
        try {
            StackFrameTest test = new StackFrameTest();
            test.method1();
        } catch (Exception e) {
            e.printStackTrace();
        }
        System.out.println("main()正常结束");
    }
    public void method1(){
        System.out.println("method1()开始执行...");
        method2();
        System.out.println("method1()执行结束...");
}
    public int method2() {
        System.out.println("method2()开始执行...");
        int i = 10;
        int m = (int) method3();
        System.out.println("method2()即将结束...");
        return i + m;
    }
    public double method3() {
        System.out.println("method3()开始执行...");
        double j = 20.0;
        System.out.println("method3()即将结束...");
        return j;
    }
}

92f4d299acc950237a64981d05b64db9.png

4.2.3. 栈帧的内部结构


每个栈帧中存储着:


局部变量表(Local Variables)

操作数栈(operand Stack)(或表达式栈)

动态链接(DynamicLinking)(或指向运行时常量池的方法引用)

方法返回地址(Return Address)(或方法正常退出或者异常退出的定义)

一些附加信息


af7c0eee9af0899f5952fe5631b752a0.jpg

并行每个线程下的栈都是私有的,因此每个线程都有自己各自的栈,并且每个栈里面都有很多栈帧,栈帧的大小主要由局部变量表 和 操作数栈决定的


08910e0e40650532f541596f6d13c003.png


4.3. 局部变量表(Local Variables)


局部变量表也被称之为局部变量数组或本地变量表


定义为一个数字数组,主要用于存储方法参数和定义在方法体内的局部变量,这些数据类型包括各类基本数据类型、对象引用(reference),以及 returnAddress 类型。

由于局部变量表是建立在线程的栈上,是线程的私有数据,因此不存在数据安全问题

局部变量表所需的容量大小是在编译期确定下来的,并保存在方法的 Code 属性的 maximum local variables 数据项中。在方法运行期间是不会改变局部变量表的大小的。

方法嵌套调用的次数由栈的大小决定。一般来说,栈越大,方法嵌套调用次数越多。对一个函数而言,它的参数和局部变量越多,使得局部变量表膨胀,它的栈帧就越大,以满足方法调用所需传递的信息增大的需求。进而函数调用就会占用更多的栈空间,导致其嵌套调用次数就会减少。

局部变量表中的变量只在当前方法调用中有效。在方法执行时,虚拟机通过使用局部变量表完成参数值到参数变量列表的传递过程。当方法调用结束后,随着方法栈帧的销毁,局部变量表也会随之销毁。

补充: 栈的大小决定方法嵌套的次数,也就是栈帧的多少,栈帧的大小由局部变量表决定.


代码验证观察局部变量表:

/**
 * @author shkstart
 * @create 2020 下午 6:13
 */
public class LocalVariablesTest {
    private int count = 0;
    public static void main(String[] args) {
        LocalVariablesTest test = new LocalVariablesTest();
        int num = 10;
        test.test1();
    }
    public void test1() {
        Date date = new Date();
        String name1 = "atguigu.com";
        test2(date, name1);
        System.out.println(date + name1);
    }
}
  • 利用javap反编译观察main方法结果:

48b6a0913f39050adbf33f0870317d8e.png

通过jclasslib进行观察:

c28103c52e2d35802963c212f0740c52.png

根据jclasslib详细分析代码执行结构:

f9069481a9b7deb6a8b03bb2b529e49a.png

补充:字节码中方法内部结构的剖析图(结合Jclasslib)

202203241759973.png

相关文章
|
1月前
|
NoSQL 测试技术
内存程序崩溃
【10月更文挑战第13天】
140 62
|
14天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。
|
12天前
|
Java Linux Windows
JVM内存
首先JVM内存限制于实际的最大物理内存,假设物理内存无限大的话,JVM内存的最大值跟操作系统有很大的关系。简单的说就32位处理器虽然可控内存空间有4GB,但是具体的操作系统会给一个限制,这个限制一般是2GB-3GB(一般来说Windows系统下为1.5G-2G,Linux系统下为2G-3G),而64bit以上的处理器就不会有限制。
12 1
|
19天前
|
存储 缓存 Java
结构体和类在内存管理方面的差异对程序性能有何影响?
【10月更文挑战第30天】结构体和类在内存管理方面的差异对程序性能有着重要的影响。在实际编程中,需要根据具体的应用场景和性能要求,合理地选择使用结构体或类,以优化程序的性能和内存使用效率。
|
1月前
|
存储 算法 Java
聊聊jvm的内存结构, 以及各种结构的作用
【10月更文挑战第27天】JVM(Java虚拟机)的内存结构主要包括程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和运行时常量池。各部分协同工作,为Java程序提供高效稳定的内存管理和运行环境,确保程序的正常执行、数据存储和资源利用。
50 10
|
1月前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
1月前
|
存储 Kubernetes 架构师
阿里面试:JVM 锁内存 是怎么变化的? JVM 锁的膨胀过程 ?
尼恩,一位经验丰富的40岁老架构师,通过其读者交流群分享了一系列关于JVM锁的深度解析,包括偏向锁、轻量级锁、自旋锁和重量级锁的概念、内存结构变化及锁膨胀流程。这些内容不仅帮助群内的小伙伴们顺利通过了多家一线互联网企业的面试,还整理成了《尼恩Java面试宝典》等技术资料,助力更多开发者提升技术水平,实现职业逆袭。尼恩强调,掌握这些核心知识点不仅能提高面试成功率,还能在实际工作中更好地应对高并发场景下的性能优化问题。
|
1月前
|
存储 安全 Java
jvm 锁的 膨胀过程?锁内存怎么变化的
【10月更文挑战第3天】在Java虚拟机(JVM)中,`synchronized`关键字用于实现同步,确保多个线程在访问共享资源时的一致性和线程安全。JVM对`synchronized`进行了优化,以适应不同的竞争场景,这种优化主要体现在锁的膨胀过程,即从偏向锁到轻量级锁,再到重量级锁的转变。下面我们将详细介绍这一过程以及锁在内存中的变化。
37 4
|
3月前
|
Java Docker 索引
记录一次索引未建立、继而引发一系列的问题、包含索引创建失败、虚拟机中JVM虚拟机内存满的情况
这篇文章记录了作者在分布式微服务项目中遇到的一系列问题,起因是商品服务检索接口测试失败,原因是Elasticsearch索引未找到。文章详细描述了解决过程中遇到的几个关键问题:分词器的安装、Elasticsearch内存溢出的处理,以及最终成功创建`gulimall_product`索引的步骤。作者还分享了使用Postman测试接口的经历,并强调了问题解决过程中遇到的挑战和所花费的时间。
|
1月前
|
缓存 算法 Java
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS
这篇文章详细介绍了Java虚拟机(JVM)中的垃圾回收机制,包括垃圾的定义、垃圾回收算法、堆内存的逻辑分区、对象的内存分配和回收过程,以及不同垃圾回收器的工作原理和参数设置。
69 4
JVM知识体系学习六:JVM垃圾是什么、GC常用垃圾清除算法、堆内存逻辑分区、栈上分配、对象何时进入老年代、有关老年代新生代的两个问题、常见的垃圾回收器、CMS