JVM中的栈和局部变量[转载]

当启动一个Java程序时,一个虚拟机实例也就诞生了。当该程序关闭退出,这个虚拟机实例也就随之消亡。若是在同一台计算机上同时运行三个Java程序,将获得三个Java虚拟机实例。每一个Java程序都运行于它本身的Java虚拟机实例中。java

    Java虚拟机在执行Java程序的过程当中会把它所管理的内存划分为若干个不一样的数据区域。根据《Java虚拟机规范(第2版)》的规定,Java虚拟机所管理的内存将会包括如下几个运行时数据区域数组

1.class文件:虚拟机并不关心Class的来源是什么语言,只要它符合Java class文件格式就能够在Java虚拟机中运行。使用Java编译器能够把Java代码编译为存储字节码的Class文件,使用JRuby等其余语言的编译器同样能够把程序代码编译成Class文件。安全

2.类装载器子系统:负责查找并装载Class 文件到内存,最终造成能够被虚拟机直接使用的Java类型。多线程

3.方法区:在类装载器加载class文件到内存的过程当中,虚拟机会提取其中的类型信息,并将这些信息存储到方法区。方法区用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。因为全部线程都共享方法区,所以它们对方法区数据的访问必须被设计为是线程安全的。this

4.堆:存储Java程序建立的类实例。全部线程共享,所以设计程序时也要考虑到多线程访问对象(堆数据)的同步问题。spa

5.Java栈:Java栈是线程私有的。每当启动一个新线程时,Java虚拟机都会为它分配一个Java栈。Java栈以帧为单位保存线程的运行状态。虚拟机只会直接对Java栈执行两种操做:以帧为单位的压栈或出栈。当线程调用java方法时,虚拟机压入一个新的栈帧到该线程的java栈中。当方法返回时,这个栈帧被从java栈中弹出并抛弃。一个栈帧包含一个java方法的调用状态,它存储有局部变量表、操做栈、动态连接、方法出口等信息。线程

 6.程序计数器:一个运行中的Java程序,每当启动一个新线程时,都会为这个新线程建立一个本身的PC(程序计数器)寄存器。程序计数器的做用能够看作是当前线程所执行的字节码的行号指示器。字节码解释器工做时就是经过改变这个计数器的值来选取下一条须要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都须要依赖这个计数器来完成。若是线程正在执行的是一个Java方法,这个计数器记录的是正在执行的虚拟机字节码指令的地址;若是正在执行的是Natvie方法,这个计数器值则为空(Undefined)。设计

7.本地方法栈:本地方法栈与虚拟机栈所发挥的做用是很是类似的,其区别不过是虚拟机栈为虚拟机执行Java方法(也就是字节码)服务,而本地方法栈则是为虚拟机使用到的Native方法服务。任何本地方法接口都会使用某种本地方法栈。当线程调用Java方法时,虚拟机会建立一个新的栈帧并压入Java栈。然而当它调用的是本地方法时,虚拟机会保持Java栈不变,再也不在线程的Java栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。若是某个虚拟机实现的本地方法接口是使用C链接模型的话,那么它的本地方法栈就是C栈。指针

8.执行引擎:负责执行字节码。方法的字节码是由Java虚拟机的指令序列构成的。每一条指令包含一个单字节的操做码,后面跟随0个或多个操做数。执行引擎执行字节码时,首先取得一个操做码,若是操做码有操做数,取得它的操做数。它执行操做码和跟随的操做数规定的动做,而后再取得下一个操做码。这个执行字节码的过程在线程完成前将一直持续。对象

 

Java中的栈

每当启用一个线程时,JVM就为他分配一个Java栈,栈是以帧为单位保存当前线程的运行状态。某个线程正在执行的方法称为当前方法,当前方法使用的栈帧称为当前帧,当前方法所属的类称为当前类,当前类的常量池称为当前常量池。当线程执行一个方法时,它会跟踪当前常量池。

每当线程调用一个Java方法时,JVM就会在该线程对应的栈中压入一个帧,这个帧天然就成了当前帧。当执行这个方法时,它使用这个帧来存储参数、局部变量、中间运算结果等等。

Java栈上的全部数据都是私有的。任何线程都不能访问另外一个线程的栈数据。因此咱们不用考虑多线程状况下栈数据访问同步的状况。

像方法区和堆同样,Java栈和帧在内存中也没必要是连续的,帧能够分布在连续的栈里,也能够分布在堆里

Java栈的组成元素——栈帧

栈帧由三部分组成:局部变量区、操做数栈、帧数据区。局部变量区和操做数栈的大小要视对应的方法而定,他们是按字长计算的。但调用一个方法时,它从类型信息中获得此方法局部变量区和操做数栈大小,并据此分配栈内存,而后压入Java栈。

局部变量区 局部变量区被组织为以一个字长为单位、从0开始计数的数组,类型为short、byte和char的值在存入数组前要被转换成int值,而long和 double在数组中占据连续的两项,在访问局部变量中的long或double时,只需取出连续两项的第一项的索引值便可,如某个long值在局部变量区中占据的索引时三、4项,取值时,指令只需取索引为3的long值便可。

下面就看个例子,好让你们对局部变量区有更深入的认识。这个图来自《深刻JVM》:

  1. public static int runClassMethod(int i,long l,float f,double d,Object o,byte b) {     
  2.         return 0;     
  3.     }     
  4.          
  5.     public int runInstanceMethod(char c,double d,short s,boolean b) {     
  6.         return 0;     
  7.     }    

上面代码片的方法参数和局部变量在局部变量区中的存储结构以下图:

上面这个图没什么好说的,你们看看就会懂。可是,在这个图里,有一点须要注意:

runInstanceMethod的局部变量区第一项是个reference(引用),它指定的就是对象自己的引用,也就是咱们经常使用的this,可是在runClassMethod方法中,没这个引用,那是由于runClassMethod是个静态方法。

 

操做数栈和局部变量区同样,操做数栈也被组织成一个以字长为单位的数组。但和前者不一样的是,它不是经过索引来访问的,而是经过入栈和出栈来访问的。可把操做数栈理解为存储计算时,临时数据的存储区域。下面咱们经过一段简短的程序片断外加一幅图片来了解下操做数栈的做用。

int a = 100;

int b = 98;

int c = a+b;

从图中能够得出:操做数栈其实就是个临时数据存储区域,它是经过入栈和出栈来进行操做的。

帧数据区除了局部变量区和操做数栈外,Java栈帧还须要一些数据来支持常量池解析、正常方法返回以及异常派发机制。这些数据都保存在Java栈帧的帧数据区中。
当JVM执行到须要常量池数据的指令时,它都会经过帧数据区中指向常量池的指针来访问它。

除了处理常量池解析外,帧里的数据还要处理Java方法的正常结束和异常终止。若是是经过return正常结束,则当前栈帧从Java栈中弹出,恢复玻璃钢液下泵发起调用的方法的栈。若是方法又返回值,JVM会把返回值压入到发起调用方法的操做数栈。

为了处理Java方法中的异常状况,帧数据区还必须保存一个对此方法异常引用表的引用。当异常抛出时,JVM给catch块中的代码。若是没发现,方法当即终止,而后JVM用帧区数据的信息恢复发起调用的方法的帧。而后再发起调用方法的上下文从新抛出一样的异常。

栈的整个结构

在前面就描述过:栈是由栈帧组成,每当线程调用一个Java方法时,JVM就会在该线程对应的栈中压入一个帧,而帧是由局部变量区、操做数栈和帧数据区组成。那在一个代码块中,栈究竟是什么形式呢?下面是我从《深刻JVM》中摘抄的一个例子,你们能够看看:

代码片断:

执行过程当中的三个快照:

 

上面所给的图,只想说明两件事情,咱们也可用此来理解Java中的栈:

一、只有在调用一个方法时,才为当前栈分配一个帧,而后将该帧压入栈。

二、帧中存储了对应方法的局部数据,方法执行完,对应的帧则从栈中弹出,并把返回结果存储在调用方法的帧的操做数栈中。

相关文章
相关标签/搜索