剑指JVM:虚拟机实践与性能调优
上QQ阅读APP看书,第一时间看更新

4.1 虚拟机栈概述

在第1章中我们已经提到,Java语言具有跨平台性,由于不同平台的CPU架构不同,所以Java的指令不能设计为基于寄存器的,而是设计为基于栈架构的。基于栈架构的优点是可以跨平台,指令集小,编译器容易实现。缺点是性能较低,实现同样的功能需要更多的指令。

Java虚拟机栈(Java Virtual Machine Stack)早期也叫Java栈。每个线程在创建时都会创建一个虚拟机栈,其内部由许多栈帧(Stack Frame)构成,每个栈帧对应着一个Java方法的调用,如代码清单4-1所示。与数据结构上的栈有着类似的含义,它是一块先进后出的数据结构,只支持出栈和入栈两种操作。栈是线程私有的,虚拟机栈的生命周期和线程一致,下面举例说明。

代码清单4-1 一个栈帧对应一个Java方法的调用

每个方法被执行的时候,JVM都会同步创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到出栈的过程。如图4-1所示,methodB()方法处于栈顶,把处于栈顶的方法称为当前方法,当methodB()方法执行完之后(图4-1中上面的框)就出栈了,methodA()方法又成了当前方法。

图4-1 栈中的栈帧

虚拟机栈的作用是主管Java程序的运行,栈解决程序的运行问题,即程序如何执行或者说如何处理数据。

以菜品佛跳墙为例,如图4-2所示,在做菜之前,需要准备相应的食材,比如鳐鱼翅、小鲍鱼、瑶柱、广肚等主料,这些食材就相当于Java中的变量。图4-2中关于佛跳墙的做法列出了9个步骤,每一步负责把对应的食材放入到图4-2中右侧的瓦罐中,比如第一步把姜片铺在罐底,第二步负责铺上冬笋片,后面的步骤不再赘述,这个流程步骤就相当于虚拟机栈,负责处理Java中的相关变量。图4-2中右侧的瓦罐就相当于堆空间了,关于堆的内容我们将在本书第7章讲解。

图4-2 佛跳墙

虚拟机栈保存方法的局部变量(8种基本数据类型、对象的引用地址)和部分结果,并参与方法的调用和返回。虚拟机栈有如下几个特点。

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

(2)对于栈来说不存在垃圾回收问题,但存在内存溢出。

(3)栈是先进后出的,每个方法执行,伴随着压栈操作;方法执行结束后,伴随着出栈操作,如图4-3所示。

Java虚拟机规范允许虚拟机栈的大小是可动态扩展的或者是固定不变的(注意:目前HotSpot虚拟机中不支持栈大小动态扩展)。关于虚拟机栈的大小可能出现的异常有以下两种。

(1)如果采用固定大小的Java虚拟机栈,那每一个线程的Java虚拟机栈容量在线程创建的时候按照固定大小来设置。如果线程请求分配的栈容量超过Java虚拟机栈允许的最大容量,JVM将会抛出一个StackOverflowError异常。

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

图4-3 栈的压栈和出栈

代码清单4-2的目的是抛出StackOverflowError异常。

代码清单4-2 StackOverflowError异常

我们可以使用参数-Xss选项来设置线程的最大栈空间,栈的大小直接决定了函数调用的最大可达深度。修改方法以IntelliJ IDEA为例。

(1)单击IntelliJ IDEA开发工具的“Run”,再单击“Edit Configurations”,如图4-4所示。

(2)进入“Edit Configurations”界面,修改“VM options”,然后单击“OK”按钮即可,如图4-5所示。

在没有设置栈大小的时候输出结果,栈大小默认为1M,如图4-6所示。

图4-4 设置栈内存大小(1)

设置栈大小为256K之后的结果,如图4-7所示。

可以看到,当栈大小从默认的1M减小为256K之后,栈调用深度从22558变为了3561。这直接证明了栈的大小决定了函数调用的最大可达深度,即栈空间越大,函数调用深度越深,反之亦然。

图4-5 设置栈内存大小(2)

图4-6 设置栈内存输出结果(1)

图4-7 设置栈内存输出结果(2)

代码清单4-3的目的是抛出OOM异常。

代码清单4-3 OOM异常

运行结果如下:

需要注意的是案例2运行环境要求32位操作系统。