深入理解JVM(四)一一运行时数据区(程序计数器+本地方法栈)

992 阅读10分钟

运行时数据区(程序计数器+本地方法栈)

本文介绍运行时数据区中的两个部分:程序计数器和本地方法栈,它们都是线程私有。

运行时数据区结构图2.png

程序计数器

什么是程序计数器

JVM中的程序计数寄存器(Program Counter Register)中,也叫程序计数器(pc寄存器),Register的命名源于CPU的寄存器,寄存器存储指令相关的现场信息。CPU只有把数据装载到寄存器才能够运行。

这里,并非是广义上所指的物理寄存器,或许将其翻译为PC计数器(或指令计数器)会更加贴切(也称为程序钩子),并且也不容易引起一些不必要的误会。JVM中的PC寄存器是对物理PC寄存器的一种抽象模拟。

为什么需要程序计数器

  • 因为CPU需要不停的切换各个线程,这时候切换回来以后,就得知道接着从哪开始继续(并发)

  • JVM的字节码解释器就需要通过改变PC寄存器的值来明确下一-条应该执行什么样的字节

image.png

并发和并行区别

  • 并发: 两个或两个以上的事件在同一时间段发生
  • 并行: 两个或两个以上的事件在同一时刻发生

作用

PC寄存器是用来存储指向下一条指令的地址,也是即将要执行的指令代码。由执行引擎读取下一条指令。

image.png

特点

  • 它是一块很小的内存空间,几乎可以忽略不计。也是运行速度最快的存储区域
  • 在jvm规范中,每个线程都有它自己的程序计数器,是线程私有的,生命周期与线程的生命周期保持一致
  • 任何时间一个线程都只有一个方法在执行,也就是所谓的当前方法。程序计数器会存储当前线程正在执行的java方法的JVM指令地址;或者,如果实在执行native方法,则是未指定值(undefined)。
  • 它是程序控制流的指示器,分支、循环、跳转、异常处理、线程恢复等基础功能都需要依赖这个计数器来完成
  • 字节码解释器工作时就是通过改变这个计数器的值来选取吓一跳需要执行的字节码指令
  • 它是唯一一个在java虚拟机规范中没有规定任何OOM情况的区域

为什么设计为线程私有

我们都知道所谓的多线程在一个特定的时间段内只会执行其中某一个线程的方法,CPU会不停地做任务切换,这样必然导致经常中断或恢复,如何保证分毫无差呢?

  • 为了能够准确地记录各个线程正在执行的当前字节码指令地址,最好的办法自然是为每一个线程都分配一个PC寄存器,这样一来各个线程之间便可以进行独立计算,从而不会出现相互干扰的情况。

  • 由于CPU时间片轮限制,众多线程在并发执行过程中,任何一个确定的时刻,一个处理器或者多核处理器中的一个内核,只会执行某个线程中的一条指令。

  • 这样必然导致经常中断或恢复,如何保证分毫无差呢?每个线程在创建后,都会产生自己的程序计数器和栈帧,程序计数器在各个线程之间互不影响。

程序计数器运行流程

程序计数器是用来存储指向下一条指令的地址,也是即将要执行的指令代码。由执行引擎读取下一条指令进行操作。伴随着栈帧的入栈,程序计数器中地址会保存一份到下个栈帧的方法返回地址出库中。当栈帧出栈,程序根据栈帧中的方法返回地址正确的运行到指定的出口。

程序计数器运行流程.png

本地方法栈

什么是本地方法栈

简单地讲,一个Native Method就是一个Java调用非Java代码的接口。一个Native Method是这样一个Java方法: 该方法的实现由非Java语言实现,比如C。 在定义一个native method时,并不提供实现体(有些像定义一个Javainterface),因为其实现体是由非java语言在外面实现的。

本地接口的作用是融合不同的编程语言为Java所用,它的初衷是融合C/C++程序。

HotSpot JVM Architecture.png

为什么使用本地方法

  • Java使用起来非常方便,然而有些层次的任务用Java实现起来不容易,或者我们对程序的效率很在意时,就需要更加底层的语言和更快运行效率。

  • 有时Java应用需要与Java外面的环境交互,这是本地方法存在的主要原因。本地方法正是这样一种交流机制:它为我们提供了一个非常简洁的接口,而且我们无需去了解Java应用之外的繁琐的细节。

  • 虚拟机本来就是由C++写的,一些操作系统特性java语言是没有封装得,我们很难自己再去实现。

  • 为我所用,最求更快更方便的运行效率

特点

  • Java虚拟机栈用于管理Java方法的调用,而本地方法栈用于管理本地方法的调用。

  • 本地方法栈,也是线程私有的。允许被实现成固定或者是可动态扩展的内存大小。(在内存溢出方面是相同的)

    • 如果线程请求分配的栈容量超过本地方法栈允许的最大容量,Java 虚拟机将会抛出一个StackOverflowError 异常。
    • 如果本地方法栈可以动态扩展,并且在尝试扩展的时候无法申请到足够的内存,或者在创建新的线程时没有足够的内存去创建对应的本地方法栈,那么Java虚拟机将会抛出一个OutOfMemoryError异常。
  • 本地方法是使用c语言实现的。

  • 它的具体做法是Native Method Stack中登记native方法,在Execution Engine 执行时加载本地方法库。

  • 当某个线程调用一个本地方法时,它就进入了一个全新的并且不再受虚拟机限制的世界。它和虚拟机拥有同样的权限。

    • 本地方法可以通过本地方法接口来访问虚拟机内部的运行时数据区。
    • 它甚至可以直接使用本地处理器中的寄存器
    • 直接从本地内存的堆中分配任意数量的内存。
  • 并不是所有的JVM都支持本地方法。因为Java虚拟机规范并没有明确要求本地方法栈的使用语言、具体实现方式、数据结构等。如果JVM产品不打算支持native方法,也可以无需实现本地方法栈。

hotSpot中native方法

image.png

虚拟机栈和本地方法栈运行流程

任何本地方法接口都会使用某种本地方法栈。当线程调用Java方法时,虚拟机会创建一个新的栈帧并压入Java栈(虚拟机栈)。然而当它调用的是本地方法时,虚拟机会保持Java栈不变,不再在线程的Java栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。

如果某个虚拟机实现的本地方法接口是使用C连接模型的话,那么它的本地方法栈就是C栈。当C程序调用一个C函数时,其栈操作都是确定的。传递给该函数的参数以某个确定的顺序压入栈,它的返回值也以确定的方式传回调用者。这就是虚拟机实现本地方法栈的行为。

当本地方法接口需要回调Java虚拟机中的Java方法,在这种情况下,该线程会保存本地方法栈的状态并进入到另一个Java栈。

当一个线程调用一个本地方法时,本地方法又回调虚拟机中的另一个Java方法。 这幅图展示了JAVA虚拟机内部线程运行的全景图。一个线程可能在整个生命周期中都执行Java方法,操作它的Java栈;或者它可能毫无障碍地在Java栈和本地方法栈之间跳转。

image.png

该线程首先调用了两个Java方法,而第二个Java方法又调用了一个本地方法,这样导致虚拟机使用了一个本地方法栈。

假设这是一个C语言栈,其间有两个C函数,第一个C函数被第二个Java方法当做本地方法调用,而这个C函数又调用了第二个C函数。

之后第二个C函数又通过本地方法接口回调了一个Java方法(第三个Java方法),最终这个Java方法又调用了一个Java方法(它成为图中的当前方法)。

现状

目前该方法使用的越来越少了,除非是与硬件有关的应用,比如通过Java程序驱动打印机或者Java系统管理生产设备,在企业级应用中已经比较少见。因为现在的异构领域间的通信很发达,比如可以使用Socket通信,也可以使用Web Service等等, 这里不多做介绍。

深入理解JVM系列