Java经过20多年的发展,据统计,国内Java程序员达到百万人口,是程序员群体里最庞大的人群,在百度招聘信息上显示,在北京每天有3万+的招聘岗位。Java依然一如既往的火爆。火爆,就意味着带来竞争,现阶段对Java程序员的要求越来越高了,从面试问题就不难看出,所以今天的文章内容是面向有Java基础的初中级程序员,需要在性能优化上解决真正项目问题的童鞋,否则你会听不懂我在说什么。深入理解 JVM 的运行时数据区,是做优化性能前必须掌握的内容,这里讲解的 JVM 指的是Sun HotSpot VM。好了,进入主题。
我们都知道Java虚拟机在执行Java程序的过程中会把它所管理的内存划分为若干个不同的数据区域,这些区域都各司其职,以及创建和销毁的时间,在《Java虚拟机规范(Java SE 8版)》的规定中,Java虚拟机所管理的内存包括五大部分区域:分别是程序计数器、虚拟机栈、本地方法栈、堆和方法区。
注意:本文先介绍JVM内存五大部分的程序计数器和虚拟机栈,其它三部分下篇详细介绍。
我们把程序代码抽象一下,可以理解为由三个部分组成,分别是数据、指令、控制流,所谓数据,可以理解为定义的成员变量,静态变量,常量;指令理解为在方法中执行的语句,控制流理解为分支、循环、跳转、异常处理、线程恢复等。我们在编写代码的过程中,可以理解为都是围绕着这三部分来展开组织代码的。好了,理解完这个前提,我们接下来一一介绍 JVM 中的五大组成部分的程序计数器和虚拟机栈,剩下三个部分看大家兴趣大不大哈。
程序计数器
也叫PC寄存器,是一块较小的内存空间,它可以看做是当前线程所执行的字节码的行号指示器。
在虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令、分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成。
(1)区别于计算机硬件的pc寄存器,两者不略有不同。计算机用pc寄存器来存放“伪指令”或地址,而相对于虚拟机,pc寄存器它表现为一块内存(一个字长,虚拟机要求字长最小为32位),虚拟机的pc寄存器的功能也是存放伪指令,更确切的说存放的是将要执行指令的地址。
(2)当虚拟机正在执行的方法是一个本地(native)方法的时候,jvm的pc寄存器存储的值是undefined。
(3)程序计数器是线程私有的,它的生命周期与线程相同,每个线程都有一个。
(4)此内存区域是唯一一个在Java虚拟机规范中没有规定任何OutOfMemoryError情况的区域。
程序计数器是一块较小的内存空间,指的是当前线所执行的字节码的行号指示器。这是比较官方的解释,通俗一点来说,首先程序计数器是与线程绑定的,也就是说每个线程在运行期都有独立的程序计数器,在上图中也可以看出,程序计数器是属于线程隔离的数据区。
我们在学习Java多线程的时候讲过,多线程是通过线程间切换抢夺 CPU分配的时间片来竞争执行的(多核CPU来说指的是一个内核),一个 CPU 处理器只会在同一时间执行一条线程指令。举个例子,有两个线程A和B,当A线程获取到运行时间并执行到一半时,CPU分配的时间片用完了,此时 A 线程就要被挂起,然后两个线程再次竞争下一次的 CPU 时间片,因此 A 线程就需要一个计数器来记录上次执行的位置,好让下次再获取到CPU时间片时可以恢复到正确位置继续执行下去。各个线程之间的计数器是独立的,互不影响,独立存储,如果线程在执行一个方法时,此线程记录的是正在执行的字节码指令的地址,如果执行的是本地(Native)方法,则计数器的值为空(undefined),由于程序计数器的内存空间非常小,所以 JVM 规范中没有规定此区域的内存溢出的情况。
Java虚拟机栈
Java虚拟机栈也是线程独立的,多个线程有独立的Java虚拟机栈,栈的生命周期与线程相同,在线程启动时被创建,线程结束时被销毁,栈是用来存储Java方法运行时数据的,那栈中存储的数据是什么方式来组织的呢?其实在栈中存储的数据结构是一个数据单位来体现的,这个数据单位称为栈帧(Stack Frame),当程序执行一个方法时,会创建一个栈帧,我们称为入栈,当方法执行结束后,栈帧就会被销毁,我们称为出栈。在一个栈帧里,用于存储局部变量表、操作数栈、动态链接、方法出口和一些额外的附加信息。
也许你会跟我一样,栈帧里的这些东东是什么鬼?不要捉急,下面我们将详细介绍运行时栈帧的结构。
前面我们说过,栈帧是虚拟机在方法调用执行时存储在虚拟机栈的数据结构,也可以称为栈元素,一个方法对应一个栈帧,一个栈帧概念结构如下图所示。
栈数据结构是先进后出,当前正在运行的线程所在的位置称为栈顶,多个线程拥有各自独立的虚拟机栈,在一个栈帧里面,接下来详细讲解一下栈帧中的局部变量表、操作数栈、动态连接、返回地址等部分的作用和数据结构。
局部变量表可以理解为是一组变量值的存储空间,目的是为了存放方法参数和方法内部定义的局部变量。当程序被编译成Class文件时,该方法会有一个Code属性的max_locals数据项来确定该方法所需要分配的局部变量表的最大容量,所以,栈帧中需要多大的局部变量表,在编译后就已经确定了,并且在程序运行期变量表的容量不用改变,所以我们在基础入门时讲的,栈中存储的数据是确定大小的,就是这个原因了。
操作数栈是一个先入后出,同局部变量表一样,操作数栈的最大深度也在编译的时候写到方法的Code属性的max_stacks数据项中,操作数栈可以理解为正在操作中需要处理的数据和结果,看个例子哈,很简单的两数相加操作,加法的字节码指令是iadd,在运行的时候操作数栈中最接近栈顶的两个元素例如已经存入了两个int型的数值,执行iadd指令时,会将这两个int值出栈并相加,然后将相加的结果入栈。
动态连接,每个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,有这个引用是为了支持方法调用过程中的动态连接,因为Class文件的常量池有很多符号引用,这些符号有一部分将在每一次运行期转化为直接引用,称为动态连接,还有一部分是在类加载或第一次使用时转化为直接引用,称为静态解析。说白了,动态连接,就是通过符号的方式来引用常量池。
方法返回地址,记录着该方法要返回到被调用的位置(通过地址来记录),我们知道方法结束有两种方式,一种是方法内部执行时遇到任意一个返回的字节码指令,这时候可能有返回值要传递给上层方法的调用者,就是调用当前方法的方法;另一种结束方式是在执行的过程中遇到了异常,并且没有在方法体内进行处理,也就是没有使用try...catch语句,此时在本方法中维护的异常表没有搜索到匹配的异常处理器,就会导致方法退出,而这种退出为异常完成出口,就不会给上层调用者返回任何值。所以方法返回地址就是用于记录调用者在哪里,以便于可以正常回到调用者的位置上。