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

一、程序计数器

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

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

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

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


二、Java 虚拟机栈(具体应该叫做方法调用栈)

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

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

     举例来说, 如果方法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会自动释放掉为该变量所分配的内存空间,该内存空间可以立即被另作他用。

三、本地方法栈

与虚拟机一样,本地方法栈区域也会抛出 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调用。

四、Java 堆

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

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

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

四、方法区

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

      方法区是堆的一个逻辑部分,但是它与java堆又是不同的,所以它有了一个别名——非堆(Non-Heap)。

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

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

 

 

 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值