概述:
对于从事C、C++开发的程序员来说,在内存管理领域,他们既是拥有最高权力的“皇帝”,又是从事最基础工作的劳动人民——既拥有每个对象的“所有权”,
又担负着每一个对象从开始到终结的维护职责。
对于java程序员来说,在虚拟机自动内存管理机制的帮助下,不再需要为没一个new操作去配对的free/delete(C、C++语言对对象的删除和内存释放操作),
不容易出现内存泄漏和内存溢出问题,看起来由虚拟机管理内存一切看起来很美好。不过,也正是java把控制内存的权力交给了java虚拟机,一旦出现内存泄漏
和内存溢出方面的问题,如果不了解虚拟机是怎么使用内存的,那排查错误、修正问题将会成为一项异常艰难的工作。
运行时数据区:
java虚拟机在执行java程序的过程中会把它所管理的内存划分为若干个不同的数据区域。这些区域有各自的用途,以及创建和销毁的时间,有些区域会随着
虚拟机进程的启动而一直存在,有些区域则是依赖用户线程的启动和结束而建立和销毁。如下图所示:
我们知道JVM也属于一种特殊的操作系统,那这些数据区域跟我们最常用的windows哪些部分相对应呢。我们可以把windows的CPU+缓存+主内存和JVM的执行引擎+
操作数栈+(栈、堆)对应起来,这样更加利于我们去理解JVM。
虚拟机栈:
从上图可见,java虚拟机栈是线程私有的,它的生命周期和线程相同。虚拟机栈描述的是java方法执行的线程内存模型:每个方法被执行的时候,java虚拟机都会
同步创建一个栈帧用于存储局部变量表、操作数栈、动态连接、返回地址等信息。每一个方法被调用直至执行完毕的过程,就对应着一个栈帧在虚拟机栈中从入栈到
出栈的过程。我们来通过一段非常简短的代码来演示虚拟机栈的作用:
当我们运行main方法,虚拟机会开启一个线程,同时为当前线程划分一块内存区域作为当前线程的虚拟机栈。同时在执行每个方法的时候都会打包成一个栈帧。
比如main开始运行,打包一个栈帧送入到虚拟机栈。C方法运行完了,C方法出栈,接着B方法运行完了,B方法出栈、接着A方法运行完了,A方法出栈,
最后main方法运行完了,main方法这个栈帧就出栈了。这个就是Java方法运行对虚拟机栈的一个影响。虚拟机栈就是用来存储线程运行方法中的数据的。而
每一个方法对应一个栈帧。入栈过程如下图所示:
上图描述了整个main方法调用的入栈和出栈的过程,需要注意的是栈帧出栈之后就没了,栈帧没得GC的说法。
栈帧详解:
栈帧大体都包含四个区域:(局部变量表、操作数栈、动态连接、返回地址)
局部变量表:顾名思义就是局部变量的表,用于存放我们的局部变量的(方法中的变量)。首先它是一个32位的长度,主要存放我们的Java的八大基础数据
类型,一般32位就可以存放下,如果是64位的就使用高低位占用两个也可以存放下,如果是局部的一些对象,比如我们的Object对象,我们只需要存放它的一个引用地址即可。
操作数栈:存放java方法执行的操作数的,它就是一个栈,先进后出的栈结构,操作数栈,就是用来操作的,操作的的元素可以是任意的java数据类型,所
以我们知道一个方法刚刚开始的时候,这个方法的操作数栈就是空的。操作数栈本质上是JVM执行引擎的一个工作区,也就是方法在执行,才会对操作数栈进行操作,如果代码不不执行,操作数栈其实就是空的。
动态连接:Java语言特性多态(后续章节细讲,需要结合class与执行引擎一起来讲)。
方法出口:正常返回(调用程序计数器中的地址作为返回)、异常的话(通过异常处理器表非栈帧中的来确定)。
我们来通过分析一个简单的方法来理解栈帧中各个区域是如何运作的,代码如下:
当该程序运行的时候,JVM会为其分配虚拟机栈,并生成对应的栈帧,如下图所示:
我们通过反汇编命令查看work方法的字节码如下:
我们看到work方法一共由10条字节码组成,我们来逐步分析。
现来看iconst_2对应的含义,如图
所以第1个字节码是将一个值为2的数字加载到操作数栈。再来看istore_0的含义,如图
所以第2个字节码的含义就是将第一步中放入到操作数栈的数字放到局部变量表中,位置为0。所以前面两个字节码对应的java代码就是inta=2;那么显而易见3和4两个字节码对应的
就是intb=3;到这里,大家心里肯定会有疑问,为什么不直接将值放到局部变量表呢?我们接着分析,你就明白了。
继续来看第5和第6两个字节码:iload_0和iload_1,它们的含义是将局部变量表中位置0和1的两个数加载到操作数栈中,接着我们来看关键的第7个字节码:imul,它代表的意思
是相乘,就是将操作数栈中的数字进行乘法运算,我们知道相乘是需要运算的,所以此时要交给执行引擎运算,运算完成之后再将运算的结果返回到操作数栈。所以操作数栈的作用
就是为jvm高速的计算提供缓冲区。
接着来看第8个字节码:istore_2,它的含义就是将计算的结果放入局部变量表,到这里intc=a*b;就执行完了。然后再来看第9和第10个字节码,它们的含义是将局部变量表的值再
压入操作数栈,最后返回。至此,整个方法执行结束,以上就是栈帧中各个区域在方法执行中的运作流程。
虚拟机栈大小的设置:
虚拟机栈的大小缺省为1M,可用参数–Xss调整大小,例如-Xssk。
我们可以看到linux的建议配置为1M,至于windows为啥没有,博主大胆猜想可能跟微软和Oracel两家公司竞争有关吧,毕竟微软开发.net就是和java竞争的。
虚拟机栈相关的程序异常:
StackOverflowError异常:如果线程请求的栈深入大于虚拟机所允许的深度,将抛出StackOverflowError异常,通常是由无线递归导致的,如下面的代码
OutOfMemoryError:如果java虚拟机的容量可以动态扩展,当栈扩展时无法申请到足够的内存会抛出OutOfMemoryError异常。这种情况基本很少出现,也很难模拟,这里就不演示了。
程序计数器:
与虚拟机栈一样,程序计数器也是线程私有的。程序计数器是一块很小的内存空间,它可以看作是当前线程所执行的字节码的行号指示器,就如上面反汇编User.class看到的一样。每一个字节码都有自己的序号:
如上图所示,虽然这些序号是由顺序的,但是并不一定是依次递增,如果某给字节码占用的空间很大,那么它的序号相较于前一个序号就差距更大。
在java虚拟机的概念模型里,字节码解释器工作时就是通过改变这个计数器的值来选取下一条需要执行的字节码指令,它是程序控制流的执行器,分支、
循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器完成。
它还有另外一个作用,我们知道在java中可以开启成百上千个线程,但是我们一般的电脑CPU也就8个左右。java虚拟机的多线程是通过线程轮流切换、
分配处理器执行时间方式来实现的,那么切换后虚拟机是怎么知道以前运行的位置,继续运行的呢?这个时候,程序计数器就起到了决定性的作用,因为
程序计数器是线程独有的,所以不会相互影响,当切回到当前线程,根据程序计数器记录的序号,继续执行对应的字节码即可。
在JVM中,只有执行java方法的时候,程序计数器才会记录正在执行的虚拟机字节码指令的地址,如果正在执行的是本地(Native)方法,这个计数器
则应为空(Undefined)。但是这里会产生一个疑问,如果刚好在执行Native方法的时候线程切换了,那切回来之后该怎么找到对应的位置呢?这里,我猜测
JVM可能规定了在执行Native本地方法的时候,禁止切换当前线程(如不正确,请指正)。xianc
本地方法栈:
本地方法栈与虚拟机栈的作用非常相似,其区别只是虚拟机栈为java方法服务,而本地方法栈专门为Native本地方法服务。需要注意的是,HotSpot直接把
本地方法栈和虚拟机栈合并了。
总结:
本篇文章介绍了JVM的内存区域之线程私有区域,主要介绍了虚拟机栈的各个组成部分以及java方法是怎么通过虚拟机栈来实现执行的,接着介绍了程序计数器的作用
最后简述了本地方法栈。下一章,我们将要分析JVM内存区域的线程共享数据区,主要包括堆、方法区、运行时常量池以及直接内存等内容。