(一)运行时数据区域之程序计数器、虚拟机栈、本地方法栈、堆、方法区

1、程序计数器java

程序计数器(Program Counter Register)是一块较小的内存空间,它能够看做是当前线程所执行的字节码行号指示器。 程序员

    通俗地讲,线程执行的任务在计算机语言中,被当作是一条条的指令。线程须要一个计数器来帮助它标记执行了什么指令,以及选取下一条指令。字节码解释器工做时就是经过改变这个计数器的值来选取下一条须要执行的字节码指令,分支、循环、跳转、异常处理、线程恢复等基础功能都须要依赖这个计数器来完成。多线程

  每条线程被CPU执行以后(由于java虚拟机的多线程是经过线程轮流切换并分配处理器执行时间来实现的--时间片轮转),须要切换下一条,为了使线程能恢复到正确的执行位置,每条线程都须要有一个独立的程序计数器,各线程之间的计数器互不影响,独立存储。编辑器

  这一块内存区域为“线程私有”的内存。ide


2、Java 虚拟机栈(具体应该叫作方法调用栈)函数

     Java 虚拟机栈(Java Virtual Machine Stacks)也是线程私有的,生命周期与线程相同,由于它描述的是Java方法执行的内存模型。优化

      Java栈是一个线程的执行区域, 它保存着一个线程中的方法的调用状态, 也能够说, 一个Java线程的运行状态, 都由一个Java栈来保存。 在这个栈中, 每一方法对应一个栈帧, 请注意区分栈帧和栈这两个概念。 指的是整个线程的执行栈, 栈帧是栈中的一个单位, 每一个方法对应一个栈帧(栈帧随着一个方法的调用开始而建立,这个方法调用完成而销毁)。 JVM会对Java栈执行两种操做: 压栈和出栈。 这两种操做在执行时都是以帧(栈帧)为单位的。 当调用了一个新的方法, 就会压入一个栈帧, 当一个方法调用完成, 就会弹出这个方法的栈帧, 回到调用者的栈帧。 spa

     举例来讲, 若是方法a调用了方法b, 而方法b中调用了方法c。 这个过程当中的方法调用和返回的装状态是这样的(其中图中两条虚线之间表示Java栈,每一个方块表示一个特定方法的栈帧)操作系统

一个线程中方法的调用链可能会很长,不少方法都同时处于执行状态。对于JVM执行引擎来讲,在在活动线程中,只有位于JVM虚拟机栈栈顶的元素才是有效的,即称为当前栈帧,与这个栈帧相关连的方法称为当前方法,定义这个方法的类叫作当前类。 线程

 

由上图能够看出,Java栈中存放的是一个个的栈帧,每一个栈帧对应一个被调用的方法,在栈帧中包括局部变量表(Local Variables)、操做数栈(Operand Stack)、指向当前方法所属的类的运行时常量池(运行时常量池的概念在方法区部分会谈到)的引用(Reference to runtime constant pool)、方法返回地址(Return Address)和一些额外的附加信息。当线程执行一个方法时,就会随之建立一个对应的栈帧,并将创建的栈帧压栈。当方法执行完毕以后,便会将栈帧出栈。所以可知,线程当前执行的方法所对应的栈帧一定位于Java栈的顶部。对于全部的程序设计语言来讲,栈这部分空间对程序员来讲是不透明的。 

  每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

  在Java虚拟机的规范中,对这个区域规定了两种异常状况:

①局部变量表(Local Variable Table) 

  • StackOverflowError
      若是线程请求的栈深度大于虚拟机所容许的深度,将抛出 StackOverflowError异常
  • OutOfMemoryError
      若是虚拟机栈在扩展的时没法申请到足够的内存,就会抛出 OutOfMemoryError异常

局部变量表(Local Variable Table)是一组变量值存储空间,用于存放方法参数和方法内定义的局部变量。局部变量表的容量以变量槽(Variable Slot)为最小单位,Java虚拟机规范并无定义一个槽所应该占用内存空间的大小,可是规定了一个槽应该能够存放一个32位之内的数据类型。

在Java程序编译为Class文件时,就在方法的Code属性中的max_locals数据项中肯定了该方法所需分配的局部变量表的最大容量。(最大Slot数量)

一个局部变量能够保存一个类型为boolean、byte、char、short、int、float、reference和returnAddress类型的数据。reference类型表示对一个对象实例的引用。returnAddress类型是为jsr、jsr_w和ret指令服务的,目前已经不多使用了。

虚拟机经过索引定位的方法查找相应的局部变量,索引的范围是从0~局部变量表最大容量。若是Slot是32位的,则遇到一个64位数据类型的变量(如long或double型),则会连续使用两个连续的Slot来存储。

②操做数

操做数就是操做数据

     Java 程序编译以后就变成了一条条字节码指令,其形式相似汇编,但和汇编有不一样之处:汇编指令的操做数存放在数据段和寄存器中,可经过存储器或寄存器寻址找到须要的操做数;而 Java 字节码指令的操做数存放在操做数栈中,当执行某条带 n 个操做数的指令时,就从栈顶取 n 个操做数,而后把指令的计算结果(若是有的话)入栈。所以,当咱们说 JVM 执行引擎是基于栈的时候,其中的“栈”指的就是操做数栈。举个简单的例子对比下汇编指令和 Java 字节码指令的执行过程,好比计算 1 + 2,在汇编指令是这样的:

1

2

mov ax, 1 ;把 1 放入寄存器 ax

add ax, 2 ;用 ax 的内容和 2 相加后存入 ax

而 JVM 的字节码指令是这样的:

1

2

3

iconst_1 //把整数 1 压入操做数栈

iconst_2 //把整数 2 压入操做数栈

iadd //栈顶的两个数相加后出栈,结果入栈

因为操做数栈是内存空间,因此字节码指令没必要担忧不一样机器上寄存器以及机器指令的差异,从而作到了平台无关。

注意,局部变量表中的变量不可直接使用,如需使用必须经过相关指令将其加载至操做数栈中做为操做数使用。好比有一个方法 void foo(),其中的代码为:int a = 1 + 2; int b = a + 3;,编译为字节码指令就是这样的:

1

2

3

4

5

6

7

8

9

iconst_1 //把整数 1 压入操做数栈

iconst_2 //把整数 2 压入操做数栈

iadd //栈顶的两个数出栈后相加,结果入栈;实际上前三步会被编译器优化为:iconst_3

istore_1 //把栈顶的内容放入局部变量表中索引为 1 的 slot 中,也就是 a 对应的空间中

iload_1 // 把局部变量表索引为 1 的 slot 中存放的变量值(3)加载至操做数栈

iconst_3

iadd //栈顶的两个数出栈后相加,结果入栈

istore_2 // 把栈顶的内容放入局部变量表中索引为 2 的 slot 中,也就是 b 对应的空间中

return // 方法返回指令,回到调用点

须要说明的是,局部变量表以及操做数栈的容量的最大值在编译时就已经肯定了,运行时不会改变。而且局部变量表的空间是能够复用的,例如,当指令的位置超出了局部变量表中某个变量 a 的做用域时,若是有新的局部变量 b 要被定义,b 就会覆盖 a 在局部变量表的空间。

③动态链接

在一个class文件中,一个方法要调用其余方法,须要将这些方法的符号引用转化为其在内存地址中的直接引用,而符号引用存在于方法区中的运行时常量池。

Java虚拟机栈中,每一个栈帧都包含一个指向运行时常量池中该栈所属方法的符号引用,持有这个引用的目的是为了支持方法调用过程当中的动态链接(Dynamic Linking)

这些符号引用一部分会在类加载阶段或者第一次使用时就直接转化为直接引用,这类转化称为静态解析。另外一部分将在每次运行期间转化为直接引用,这类转化称为动态链接。

④方法返回

当一个方法开始执行时,可能有两种方式退出该方法:

  • 正常完成出口
  • 异常完成出口

正常完成出口是指方法正常完成并退出,没有抛出任何异常(包括Java虚拟机异常以及执行时经过throw语句显示抛出的异常)。若是当前方法正常完成,则根据当前方法返回的字节码指令,这时有可能会有返回值传递给方法调用者(调用它的方法),或者无返回值。具体是否有返回值以及返回值的数据类型将根据该方法返回的字节码指令肯定。

异常完成出口是指方法执行过程当中遇到异常,而且这个异常在方法体内部没有获得处理,致使方法退出。

不管是Java虚拟机抛出的异常仍是代码中使用athrow指令产生的异常,只要在本方法的异常表中没有搜索到相应的异常处理器,就会致使方法退出。

不管方法采用何种方式退出,在方法退出后都须要返回到方法被调用的位置,程序才能继续执行,方法返回时可能须要在当前栈帧中保存一些信息,用来帮他恢复它的上层方法执行状态。

方法退出过程实际上就等同于把当前栈帧出栈,所以退出能够执行的操做有:恢复上层方法的局部变量表和操做数栈,把返回值(若是有的话)压如调用者的操做数栈中,调整PC计数器的值以指向方法调用指令后的下一条指令。

通常来讲,方法正常退出时,调用者的PC计数值能够做为返回地址,栈帧中可能保存此计数值。而方法异常退出时,返回地址是经过异常处理器表肯定的,栈帧中通常不会保存此部分信息。

总结 
1. 每一个线程包含一个栈区,栈中局部变量表保存基础数据类型的对象和自定义对象的引用(不是对象)。对象都存放在堆区中。 
2. 每一个栈中的数据(基础数据类型和对象引用)都是私有的,其余栈不能访问。 
3. 栈分为3个部分:基本类型变量,执行环境上下文,操做指令区(存放操做指令). 
4. 在函数中定义的一些基本类型的变量数据和对象的引用变量都在函数的栈内存中分配。 
5. 当在一段代码块定义一个变量时,Java就在栈中为这个变量分配内存空间,当该变量退出该做用域后,Java会自动释放掉为该变量所分配的内存空间,该内存空间能够当即被另做他用。

3、本地方法栈

与虚拟机同样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemeoryError 异常。 

  • 虚拟机栈为虚拟机执行java方法(也就是字节码)服务
  • 本地方法栈则为虚拟机使用到的Native方法服务

 与虚拟机同样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemeoryError 异常。

扩展:Native Method:"A native method is a Java method whose implementation is provided by non-java code."

         为何有Native Method:有些层次的任务用java实现起来不容易,或者咱们对程序的效率很在乎时

         ①与java环境外交互
   有时java应用须要与java外面的环境交互。这是本地方法存在的主要缘由,你能够想一想java须要与一些底层系统如操做系统或某些硬件交换信息时的状况。本地方法正是这样一种交流机制:它为咱们提供了一个很是简洁的接口,并且咱们无需去了解java应用以外的繁琐的细节。
        ②与操做系统交互
   JVM支持着java语言自己和运行时库,它是java程序赖以生存的平台,它由一个解释器(解释字节码)和一些链接到本地代码的库组成。然而无论怎 样,它毕竟不是一个完整的系统,它常常依赖于一些底层(underneath在下面的)系统的支持。这些底层系统经常是强大的操做系统。经过使用本地方法,咱们得以用java实现了jre的与底层系统的交互,甚至JVM的一些部分就是用C写的,还有,若是咱们要使用一些java语言自己没有提供封装的操做系统的特性时,咱们也须要使用本地方法。
       ③Sun's Java
    Sun的解释器是用C实现的,这使得它能像一些普通的C同样与外部交互。jre大部分是用java实现的,它也经过一些本地方法与外界交互。例如:类java.lang.Thread 的 setPriority()方法是用java实现的,可是它实现调用的是该类里的本地方法setPriority0()。这个本地方法是用C实现的,并被植入JVM内部,在Windows 95的平台上,这个本地方法最终将调用Win32 SetPriority() API。这是一个本地方法的具体实现由JVM直接提供,更多的状况是本地方法由外部的动态连接库(external dynamic link library)提供,而后被JVM调用。

4、Java 堆

      对大部分应用来讲,java堆是java虚拟机所管理的内存中最大的一块。java堆是被全部线程共享的一块内存区域(即一个JVM只有一个堆),在虚拟机启动时建立。此内存区域的惟一目的是存放对象实例,几乎全部的对象实例都在这里分配内存。

Java 堆能够处于物理上不连续的内存空间中,只要逻辑上是连续便可。在实现时,既能够实现成固定大小的,也能够是可扩展的(经过-Xmx 和 -Xms 控制)。若是在堆中没有内存完成实例分配,而且堆也没法再扩展时,将会抛出 OutOfMemoryError 异常。

  此外,Java 堆是垃圾收集器器管理的主要区域。大体能够分红:新生代和老生代,还可再细分。可是进行细分的目的是为了更好地回收内存或更快的分配内存。

4、方法区

      方法区(Method Area)与 Java 堆 同样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编辑器编译后的代码等数据。

      方法区是堆的一个逻辑部分,可是它与java堆又是不一样的,因此它有了一个别名——非堆(Non-Heap)。

  方法区中的内存通常不会被 GC 回收,GC 也难回收,因此被取名为“永久代”,意思是永久存在。这区域的内存回收目标主要是针对常量池的回收和对类的卸载。可是“永久代”中的数据并不是真的永久存在,只是回收比较麻烦。

  根据 Java 虚拟机规范的规定,当方法区没法知足内存分配需求时,将抛出 OutOfMemoryError 异常。

相关文章
相关标签/搜索