java虚拟机学习

时间:2021-11-16 20:25:13
 

JVM(Java Virtual Machine):Java虚拟机,就是构建在操作系统之上的又一个运行平台而已,java程序运行于此平台之上。类似:计算机系统中,硬件-->操作系统-->应用软件。跟做云计算的同学了解到,他们要在一个硬件和操作系统之间再加一层,通用操作系统平台,貌似是用xen实现的。计算机系统的发展史可以从分层的意义上去看,分层的概念对于软件设计也是非常重要的。

 

Java虚拟机是什么

 

          要理解java虚拟机,你首先必须意识到,当你说“Java虚拟机”时,可能指的是如下三个不同的东西:

1. 抽象规范

2. 一个具体的实现

3. 一个运行中的虚拟机实例

 

           Java虚拟机抽象规范仅仅是一个概念,在Tim Lindholm和Frank Yellin编著的《The Java Virtual Machine Specification》一书中详细地描述了它。而该规范的具体实现,可能来自多个提供商,并存在于多个平台。他或者完全用软件实现,或者以硬件和软件结合的方式来实现。当运行一个Java程序的同时,也就是运行了一个Java虚拟机实例。每个Java程序都运行于某个具体的Java虚拟机实现的实例上。

 

Java虚拟机的生命周期

 

  一个运行时的Java虚拟机实例的天职就是:负责运行一个Java程序。当启动一个Java程序时,一个虚拟机实例也就诞生了。当该程序关闭退出是,这个虚拟机实例也就随之消亡。如果在同一个计算机上同时运行三个程序,将得到三个Java虚拟机实例。每个Java程序都运行于它自己的Java虚拟机实例中。

  Java虚拟机实例通过调用某个初始类的main()方法来运行一个Java程序,此方法将作为该程序初始线程的启动,任何其他的线程都是由这个初始线程启动的。

  在Java虚拟机内部有两种线程:守护线程与非守护线程。守护线程通常是由虚拟机自己使用的,比如执行垃圾收集任务的线程。但是,Java程序也可以把他创建的任何线程标记为守护线程。而Java程序中的初始线程---------就是开始于main()的那个,是非守护进程。只要还有非守护线程在运行,那么这个Java程序也在继续运行(虚拟机仍然存活)。当该程序中所有的非守护线程都终止时,虚拟机实例将自动退出。假若安全管理器允许,程序本身也能够通过调用Runtime类或者System类的exit()方法来退出。

 


Java虚拟机体系结构

java虚拟机学习

从上图可以看得出来,JVM中包含:

 

 1. Class Loader 类加载器

 

   所有的class文件必须被加载后才能在jvm中运行,关于jvm的类加载器,在其他章节中重点描述。

 

2.  Runtime Data Areas 运行数据区

 

     运行时数据区分为:method area(本地方法区)、heap(堆)、java stacks(Java 栈)、 pc registers(pc寄存器)、 native method stacks(本地方法栈)

 

     下面来依次解释下上述内容:

 

   Method area(方法区)

 

在Java虚拟机中,关于被装载类型的信息存储在一个逻辑上被称为方法区的内存中。当虚拟机状态某个类型时,它使用类装载器定位相应的class文件,然后读入这个class文件------一个线性二进制数据流。然后将它传输到虚拟机中。紧接着虚拟机提取其中的类型信息,并将这些信息存储到方法区。该类型中的类(静态)变量同样也是存储在方法区中。

 

Java虚拟机在内部如何存储类型信息,这是由具体实现的设计者来决定的。比如,在class文件中,多字节总是以高位在前(即代表较大数的字节在前)的顺序储存。但是这些数据被引入到方法区后,虚拟机可以以任何方式存储它。假设某个实现是运行在低位优先的处理器上,那么它很可能会把多字节以低位优先的顺序存储到方法区中。

 

当虚拟机运行Java程序时,它会查找使用存储在方法区中的类型信息。设计者应当为类型信息的内部设计适当的数据结构,以尽可能在保持虚拟机小巧紧凑的同事加快程序的运行效率。如果正在设计一个需要在少量内存的限制中操作的实现,设计则可能会决定以牺牲某些运行速度来换取紧凑性。另外一方面,如果设计一个将在虚拟内存系统中运行的实现,设计者可能会决定在方法去中保存一些冗余的信息,以此来加快执行 速度。(如果底层主机没有提供虚拟内存,但是提供了一个硬盘,设计者可能会在实现中穿件一个虚拟内存系统。)Java虚拟机的设计者可以根据目标平台的资源限制和需求,在空间和时间上作出权衡,选择实现什么样的数据结构和数据组织。

 

由于所有线程都共享方法区,因此它们对方法区数据的访问必须被设计为是线程安全的。比如,假设同时有连个线程都企图访问一个名为Lava的类,而这个类还没有被装入虚拟机,那么这是只应该有一个线程去装载它,而另外一个线程则只能等待。

 

方法区的大小不必是固定的,虚拟机可以根据应用的需要动态调整。同样,方法区也不必是连续的,方法区可以在一个堆(甚至是虚拟机自己的堆)中*分配。另外,虚拟机也可以允许用户或者程序员指定方法区的初始化大小以及最小和最大尺寸等。

 

方法区也可以被垃圾收集,因为虚拟机允许通过用户定义的类装载器来动态扩展java程序,因此一些类也会成为程序"不再引用"的类。当某个类变为不再被引用的类时,Java虚拟机可以卸载这个类(垃圾收集),从而使方法区占据的内存保持最小。

 

方法区针对具体的语言特性有几种信息是存储在方法区内的:

【类型信息】 --对每个装载的类型,虚拟机都会在方法区中存储以下类型信息

    这个类型的完全限定名(java.lang.String格式)

    这个类型的直接超类的全限定名(除非uzhege类型时java.lang.Object,它没有超类)

    这个类型是类类型还是接口类型

    这个类型的访问修饰符(public、abstract或final的某个子集)

    任何直接超接口的全限定名的有序列表

 

 在Java class文件和虚拟机中,类型名总是以全限定名出现。在java源代码中,全限定名由类所属包的名称加上一个“.”,再加上类名组成。例如,类Object 的所属包为java.lang,那她的全限定名应该是 java.lang.Object,但是在class文件里,所有的“.”都被斜杠“/”替换,这样就成为java/lang/Object。至于全限定名在方法区中的表示,则因不同的设计者有不同的选择而不同,可以用任何形式和数据结构来表示。

 

除了上面列出的基本类型信息外,虚拟机还得为每个被装载的类型存储以下信息:

【该类型的常量池】

【字段信息】

【方法信息】

【除了常量意外的所有类(静态)变量】

【一个到ClassLoader的引用】

【一个到Class类的引用】

 

 【常量池

虚拟机必须为每一个被装载的类型维护一个常量池。常量池就是该类型所用常量的一个有序集合,包括直接常量(string,integer和floating point常量)和对其他类型字段方法符号引用。池中的数据项就像数组一样是通过索引访问的。因为常量池存储了相应类型所用到的所有类型、字段和方法的符号引用,所以它在java程序的动态连接中起着核心的作用。

 

字段信息

对于类型中声明的没一个字段,方法区中必须保存下面的信息。除此之外,这些字段在类或者接口中的声明顺序也必须保存。下面是字段信息的清单:

字段名

字段的类型

字段的修饰符(public、private、protected、static、final、volatile、transient的某个子集)

 

 

方法信息

对于类型中声明的每一个方法,方法区中必须保存下面的信息。和字段一样,这些方法法在类或者接口中的声明顺序也必须保存。下面试方法信息的清单:

方法名

方法的返回类型(或void)

方法参数的数量和类型(按按声明顺序)

方法的修饰符(public、private、protected、static、final、synchronized、native、abstract的某个子集)

 

除了上面的清单中列出的条目之外,如果某个方法不是抽象的和本地的,它还必须保存下面的信息:

方法的字节码(bytecodes)

操作数栈和该方法的栈帧中的局部变量区的大小

异常表

 

类(静态)变量

类变量是由所有类实例共享的,即使没有任何类实例,它也可以被访问。这些变量只与类有关---而非类的实例,因此他们总是作为类型信息的一部分二存储在方法区。除了在类中声明的编译时常量外,虚拟机在使用某个类之前,必须在方法区中为这些类变量分配空间。

 

而编译时常量(就是那些用final声明以及用编译时已知的值初始化的类变量)则和一般的类变量的处理方式不同,每个使用编译时常量的类型都会复制它的所有常量到自己的常量池中,或嵌入到它的字节码流中。作为 常量池或字节码流的一部分,编译时常量保存在方法区中,就和一般的类变量一样。但是当一般的类变量做为声明他们的类型的一部分数据面保存的时候,编译时常量作为使用他们的类型的一部分而保存。

 

指向ClassLoader类的应用

每个类型被加载的时候,虚拟机必须跟踪它是由启动类装载器还是由用户自定义类装载器装载的。如果是用户自定义类装载器装载的,那么虚拟机必须在类型信息中存储对该装载器的引用。这是作为方法表中的类型数据的一部分保存的。虚拟机会在动态连接期间使用这个信息。当某个类型引用另外一个类型的时候,虚拟机会请求装载发起引用类型的类装载器类装载来装载被引用的类型。这个动态连接的过程,对于虚拟机分离命名空间的范式也是至关重要的。为了能够正确地执行动态连接以及维护多个命名空间,虚拟机需要在方法表中得知每个类都是由哪个类装载器装载的,

 

指向Class类的引用

对于每个被装载的类型(不管是类还是接口),虚拟机都会相应地位它创建一个java.lang.Class类的实例,而且虚拟机还必须以某种方式吧这个实例和存储在方法区中的数据关联起来。

 

 

方法表

为了尽可能提高访问效率,设计者必须仔细设计存储在方法区中的类型信息的数据结构,因此,除了以上讨论的原始类型信息,实现中还可能包括其他数据结构加快访问原始数据的速度,比如方法表。虚拟机对每个装载的非抽象类,都生成一个方法表,吧它作为类信息的一部分保存在方法区。方法表是一个数组,他的元素是所有他的实例可能被调用的实例方法的直接饮用,包括哪些从超类继承过来的实例方法。(对于抽象类和接口,方法表没有什么帮组,因为程序绝不会生成他们的实例)运行时可以通过方法表快速搜索在对象中调用的实例方法。

 

方法区使用示例

为了展示虚拟机如何使用方法区中的信息,我们举个例子,看下面这个类

 

  

Java代码 java虚拟机学习 java虚拟机学习java虚拟机学习
  1. class Lava{   
  2.     private int speed = 5;   
  3.   
  4.     void flow(){   
  5.      }   
  6. }   
  7.   
  8.   
  9. class Volcano{   
  10.     public void main(String[] args) {   
  11.         Lava lava = new Lava();   
  12.         lava.flow();   
  13.     }   
  14. }  

 

            下面的段落描述了某个实现是如何执行Volcano程序中main()方法的字节码中第一条指令的。不同的虚拟机实现可能会用完全不同的方法来操作,下面描述的只是其中一种可能,但是并不是仅有的一种,下面看一下Java虚拟机是如何执行Volcano程序中main()方法的第一条指令的。

 

 

    要运行Volcano程序,首先得以某种“依赖于实现的”方式告诉虚拟机“Volcano”这个名字。之后虚拟机将找到并读入相应的class文件“Volcano.class”,然后他会从导入的class文件里的而精致数据中提取类型信息并放到方法区中。通过执行保存在方法区中额字节码,虚拟机开始执行main()方法,在执行时,他会一直持有指向当前类(Volcano类)的常量池(方法区中的一个数据结构)的指针。

 

    注意,虚拟机开始执行Volcano类中main()方法的字节码的时候,尽管Lava类还没被装载,但是和大多数(也许是所有)    虚拟机实现一样,他不会等到把程序中用到的所有类都装载后才开始运行程序。恰好相反,他只需在需要是才装载相应的类

 

     main()的第一条指令告知虚拟机为列在常量池第一项的类分配足够的内存。所以虚拟机使用指向Volcano常量池的指针找到第一项,发现他是一个堆Lava类的符号引用,然后他就检查方法区,看Lava类是否已经被装载了。

 

     这个符号引用仅仅是一个给出了类Lava的全限定名“Lava”的字符串。为了嫩让虚拟机尽可能快地从一个名称找到类,设计者应当选择最佳的数据结构和算法。这里可以采用各种方法,如散列表、搜索树等等。同样的算法可以以用于实现Class类的forName()方法,这个方法根据给定的全限定名返回Class引用。

 

    当虚拟机发现还没有装载过名为“Lava”的类时,他就开始查找并装载文件“Lava.class”,并把从读入的二进制数据中提取的类型信息放在方法区中。

 

    紧接着,虚拟机以一个直接指向方法区Lava类数据的指针类替换常量池第一项(就是那个字符串“Lava”)----以后就可以用这个指针来快熟访问Lava类了。这个替换过程称为常量池解析,即把常量池中的符号引用替换为直接引用。这是通过在方法区中搜索被引用的元素实现的,在这期间可能又需要装载其他类。在这里,我们替换掉符号引用的“直接引用”是一个本地指针。

 

     终于,虚拟机转变为一个新的Lava对象分配内存。此时,它又需要方法区中的信息。还记得刚刚放到Volcano类常量池第一项的指针吗?现在虚拟机用它来访问Lava类型信息(此前刚放到方法区中的),找到其中记录的这样一个信息:一个Lava对象需要分配多少堆空间。

 

     Java虚拟机总能够通过存储于方法区的类型信息来实现一个对象需要的内存,但是,某一个特定对象事实上需要多少内存,是跟特定实现相关的。对象在虚拟机内部的表示由实现的设计者来决定的。

 

     当java虚拟机确定了一个Lava对象的大小后,它就在堆上分配这么大的空间,并吧这个对象实例的变量speed初始化为默认初始值0.假如Lava类的超类Object也有实例变量,这也会在此时被初始化为相应的默认值。

 

     当把新生成的Lava对象的引用压到栈中,main()方法的第一条指令也完成了。接下来的指令通过这个引用调用Java代码(改代码吧speed变量初始化为争取的初始值5)。另外一条指令将用这个引用调用Lava对 象引用的flow()方法。

 

Heap 堆

      java程序在运行时创建的所有类实例或数组都放在同一个堆中。而一个Java虚拟机实例只存在一个堆内存空间,因此所有的线程均共享这个堆。又由于一个java程序独占一个Java虚拟机实例,因而每个java程序都有他自己的堆内存----他们之间不会相互影响。但是同一个Java程序的多线程却共享着同一个堆空间,在这种情况下,就得考虑多线程访问对象(堆数据)的同步问题了。

java虚拟机规范中并没有规定具体的实现需要准备多少内存,也没有说他必须怎么去管理他的堆空间,它仅仅告诉设计者

java程序需要在堆内存中为对象分配空间,并且程序本身并不去释放他。因此堆空间的管理(包括垃圾收集)问题得由设计者自己自行去考虑处理方式。对象的引用在很多地方都存在,如java栈、堆、方法区、本地方法栈,所以垃圾收集技术的设计和使用在很大程度上会影响到运行时数据区的设计,对于垃圾收集技术的深入描述,在后续的文章中会单独拿出来讨论。

     和方法区一样,堆空间也不必是连续的内存区。在程序运行时,他可以动态的扩展或收缩。事实上,一个实现的方法区可以在堆顶实现。换句话说就是当虚拟机需要为某个新装载的类分配内存是,类型信息和实际对象可以都在同一个堆上。因此,负责回收无用对象的垃圾收集器可能也要负责无用类的释放。另外,某些实现允许程序员或用户指定堆的初始化内存大小以及最大、最小值等。

 

对象的内部表示

     Java虚拟机规范并没有规定Java对象在堆中是如何表示的。对象的内部表示也影响着整个堆以及垃圾收集器的设计,他由虚拟机的实现者决定。

     java对象中包含的基本数据类型由他所属的类及其所有超类声明的实例变量组成。只要有一个对象引用虚拟机就必须能够快速定位对象实例的数据。另外,他也必须能够通过该对象引用访问相应的类数据(存储于方法区的类型信息)。因此在对象中通常会有一个指向方法区的指针

     一种可能的堆空间设计就是,把堆分为两个部分:一个句柄池,一个对象池如下图所示,java虚拟机学习

      一个对象引用就是一个指向句柄池的指针。句柄池的每个条目有两个部分:一个指向对象实例变量的指针一个指向方法区中类型数据的指针。这种设计的好处是有利于碎片的整理,当移动对象池中的对象时,句柄部分只需要更改一下指针指向对象的新地址即可----就是句柄池中的那个指针。缺点是每次访问对象的实例变量时都要经过两次指针传递。

     

      另一种设计方式是使对象指针直接指向一组数据,而该数据包括对象实例数据以及指向方法区中类数据的指针

 
java虚拟机学习

        这样设计的优缺点正好和前面的相反,他只需要一个指针就可以访问对象的实例数据,但是移动对象就变得更加复杂。当使用这种堆的虚拟机为了减少内存碎片而移动对象的时候,他必须在整个运行时数据区中更新指向被移动对象的引用。

 

      有如下几个理由要求虚拟机必须能够通过对象引用得到类数据:当程序在运行时需要转换某个对象引用为另一种数据类型时,虚拟机必须要检查这种转换是否被允许,被转换的对象是否的确是被引用的对象或者他的超类,当程序在执行instanceof操作时,虚拟机也进行了同样的检查。在这两种情况下,虚拟机都需要查看被引用对象的类数据。最后,当程序中调用某个实例方法时,虚拟机必须进行动态绑定,换句话说,他不能按照引用类型来决定将要调用的方法,而必须根据对象的实际类,为此,虚拟机必须再次通过对象的引用去访问类数据。

      不管虚拟机的实现使用什么样的对象表示法,很可能每个对象都有一个方法表,因为对象表加快了调用实例方法时的效率,从而对java虚拟机实现的整体性能起着非常重要的作用。但是Java虚拟机规范中并未要求必须是使用方法表,所以并不是所有的实现中都会使用它。比如那些具有严格内存资源限制的实现,或者他们根本不可能有足够的额外内存资源来存储方法表。如果一个实现中使用方法表,那么仅仅使用一个指向对象的引用,就可以很快访问到对象的方法表。

      

 java虚拟机学习

 

     上图展示了一种把方法表和对象引用联系起来的实现方式。每个对象的数据都包含一个指向特定数据结构的指针,这个数据结构位于方法区,他包括两部分:

      一个指向方法区对于类数据的指针

      此对象的方法表

      此方法的操作数栈和局部变量区的大小

      此方法的字节码

      异常表

 

       图5-7的堆内存设计会占用更多的内存,但是可以轻微提高一些效率,一般来讲,该方案只适用于内存足够充裕的系统。

 

         图5-5和图5-6中显示的还有另外一种数据,堆上的对象数据中还有一个逻辑部分,那就是对象锁,这是一个互斥对象。虚拟机中的每个对象都有一个对象锁,他被用于协调多个线程访问同一个对象时的同步。在任何时刻,只能有一个线程“拥有”这个对象锁,因此只有这个线程才能访问该对象的数据。此时其他希望访问这个对象的线程只能等待,直到拥有对象锁得线程释放锁,当某个线程拥有一个对象锁后,可以继续对这个所追加请求。但请求几次,必须对应地释放几次,之后才能轮到其他线程。比如一个线程请求了三次锁,在他释放三次锁之前,他一直保持“拥有”这个锁。

 

         很多对象在其整个生命周期内都没有被任何线程枷锁。在线程实际请求某个对象的锁之前,实现对象锁所需要的数据是不必要的。这样正如图5-5和图5-6所示,很多实现不在对象自身内部保持一个指向锁数据的指针。而只有当第一次需要枷锁的时候才分配对应的锁数据,但这是虚拟机需要某种间接方法来联系对象数据和对应的锁数据。例如把锁数据放在一个以对象地址为索引的搜索树中。

 

         除了实现锁所必须的数据外,每个Java对象逻辑上还与实现等待集合(wait set)的数据相关联。锁是用来实现多线程对共享数据的互斥访问的,而等待集合是用来让多个线程为完成一个共同目标而协调工作的。

 

         等待集合由等待方法和通知方法联系使用。每个类都从Object哪里集成了三个等待方法(三个名为wait()的重载方法)和两个通知方法(notify()以及notifyAll())。当某个线程在一个对象上调用等待方法时,虚拟机就阻塞这个线程,并把他放在了这个对象的等待集合中。知道另外一个线程在同一个对象上调用通知方法,虚拟机才会在之后的某个时刻唤醒一个或多个在等待集合中被阻塞的线程。正像锁数据一样,在实际调用对象的等待方法或通知方法之前,实现对象的等待集合的数据并不是必须的。二次,许多虚拟机实现都吧等待集合数据与实际对象数据分开,只有在需要时菜为此对象创建同步数据(通常是在第一次调用等待方法或通知方法时)。关于锁和等待集合的内容,在特定章节再具体阐述

 

        最后一种数据类型--------可以作为堆中某个对象映像的一部分,是与垃圾收集器有关的数据。垃圾收集器必须(以某种方式)跟踪程序引用的每个对象,这个任务不可避免地要附加一些数据给这些对象,数据的类型要视垃圾收集使用的算法而定。此外,对于不再被引用的对象,还需要指明他的终结方法(finalizer)是否已经运行过了。像线程锁一样,这些数据也可以放在对象数据外。有一些垃圾收集技术只在垃圾收集器运行时需要额外的数据。例如“标记并清除”算法就使用一个独立的位图来标记对象的引用情况。

 

        除了标记对象的引用外,垃圾收集器还要区分对象是否调用了终结方法。对于在其类中声明了终结方法的对象,在回收他之前,垃圾收集器必须调用它的终结方法。java语言对反之处,垃圾收集器对每个对象只能调用一次终结方法,但是允许终结方法复活(resurrect)这个对象,即允许改对象被再次引用。这样当这个对象再次被回收时,就不用再调用终结方法了。需要终结方法的对象不多,而需要复活的更少,所以对一个对象回收两次的情况很少见。这种用来标识终结方法的逻辑上时对象的一部分,但通常实现上不随对象保存在堆中。大部分情况下,垃圾收集器会在一个单独的空间保存这个信息。

 

数组的内部表示

在Java中,数组是真正的对象。和其他对象一样,数组总是存储在堆中。同样,和普通对象一样,实现的设计者将决定数组在堆中的表示形式。

和其他所有对象一样,数组也拥有一个与他们的类相关联的Class实例,所有具有相同维度和类型的数组都是同一个类的实例,而不管数组的长度(多维数组每一维的长度)是多少。例如一个包含3个int整数的数组和一个包含300个int整数的数组拥有同一个类。数组的长度只与实例数据有关。

数组类的名称由两部分组成:每一维用一个方括号“[”表示,用字符或字符串表示元素类型。比如,元素类型为int整数的、以为数组的类名为“[]”,元素类型为byte的三维数组为“[[[B”,元素类型为Object的二位数组为“[[Ljava/lang/Object”。

多为数组被表示为数组的数组。比如,int类型的二维数组,将表示为一个一维数组,其中的每个元素是一个一维int数组的引用,入下图所示:  

java虚拟机学习

 

在堆中的每个数组对象还必须保存的数据是数组的长度、数组数据,以及某些指向数组的类数据的引用。虚拟机必须能够通过一个数组对象的引用得到此数组的长度,通过索引访问其元素(其间要检查数组边界是否越界)、调用所有数组的直接超类Object声明的方法等等。

 

Java Stacks

 

      每当启动一个新线程时,Java虚拟机都会为它分配一个Java栈。前面我们曾经提到,Java栈出帧为单位保存线程的运行状态。虚拟机只会直接对Java栈执行两种操作:以帧为单位的压栈或出栈。

      某个线程正在执行的方法被称为该线程的当前方法,当前方法使用的栈帧称为当前帧,当前方法所属的类称为当前类,当前类的常量池称为当前常量池。在线程执行一个方法时,他会跟踪当前类和当前常量池。此外当虚拟机遇到栈内操作指令时,他对当前帧内数据执行操作。

      每当线程调用一个Java方法时,虚拟机都会在该线程的Java栈中压入一个新帧。而这个新帧自然就成为了当前帧。在执行这个方法时,他使用这个帧来存数参数、局部变量、中间运算结果等等数据。

      Java方法可以以两种方式完成。一种通过return返回的,称为正常返回;一种是通过抛出异常而异常终止的。不管哪种方式返回,虚拟机都会将当前帧弹出Java栈然后释放掉,这样上一个方法的真就成为当前帧了。

      Java栈上的所有数据都是此线程私有的。任何线程都不能访问另外一个线程的栈数据,因此我们不需要考虑多线程情况下栈数据的访问同步问题。当一个线程调用一个方法时,方法的局部变量保存在调用线程Java栈的帧中。只有一个线程能总是访问那些局部变量,即调用方法的线程。

      像方法区和堆一样,Java栈和帧在内存中也不是连续的。帧可以分布在连续的栈里,也可以分步在堆里,或者二者兼而有之。表示Java栈和栈帧的实际数据结构由虚拟机的实现者决定,某些实现允许用户指定Java栈的初始大小和最大最小值。

 

PC Register

 

       对于一个运行中的Java程序而言,其中的每一个线程都有他自己的PC(程序计数器)寄存器它是在该线程启动时创建的。PC寄存器的大小是一个字长,因此他既能够持有一个本地指针,也能够持有一个returnAddress。当线程执行某个Java方法时,PC寄存器的内容总是下一条将被执行指令的"地址",这里的“地址”可以是一个本地指针,也可以是在方法字节码中相对于该方法起始指令的偏移量。如果该线程正在执行一个本地方法,那么此时PC寄存器的值是“undefined”。

 

 

Native Method Stacks

 

        前面提到的所有运行时数据区都是Java虚拟机规范中明确定义的,除此之外,对已一个运行中的Java程序而言,他还可能会用到一些本地方法相关的数据区。当某个线程调用一个本地方法时,他就进入了一个全新的并且不再受虚拟机限制的世界,本地方法可以通过本地方法接口来访问虚拟机得运行时数据区,但不止于此,他还可以做任何他想做的事情。比如,他甚至可以直接使用本地处理器中的寄存器,或者直接从本地内存的堆中分配任意数量的内存等等。宗旨,他和虚拟机拥有同样的权限(或者说能力)。,

       本地方法本质上是依赖于实现的,虚拟机实现的设计者可以*地决定使用怎样的机制来让Java程序调用本地方法。

       任何本地方法接口都会使用某种本地方法栈。当线程调用Java方法时,虚拟机会创建一个新的栈帧并压入java栈。然而当他调用的是本地方法时,虚拟机会保持Java栈不变,不再在线程的java栈中压入新的帧,虚拟机只是简单地动态连接并直接调用指定的本地方法。可以把这看做是虚拟机利用本地方法来动态扩展自己。就如同Java虚拟机的实现在按照其中运行的Java程序的吩咐,调用属于虚拟机内部的另一个(动态连接的)方法。

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

        很可能本地方法接口需要回调Java虚拟机中的Java方法(这也是由设计者决定的),在这种情形下,该线程会保存本地方法栈的状态并进入到另一个Java栈。

 

 

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

java虚拟机学习

         上图所示,改线程首先调用了两个Java方法,而第二个Java方法又调用了一个本地方法,这样导致虚拟机使用了一个本地方法栈。图中的本地方法栈显示为一个连续的内存空间。假设这是一个C语言栈,期间有两个C函数,他们都以包围在虚线中的灰色块表示。第一个C函数被第二个Java方法当做本地方法调用,而这个C函数又调用了第二个C函数。之后第二个C函数被第二个Java方法当做本地方法调用,而这个C函数又调用了第二个C函数。之后第二个C函数又通过本地方法接口回调了一个Java方法(第三个Java方法)。最终这个Java方法又调用了一个Java方法(他成为图中的当前方法)。

         就像其他运行时内存区一样,本地方法栈占用的内存区也不必是固定大小的,他可以根据需要动态扩展或者收缩。某些是实现也允许用户或者程序员指定该内存区的厨师大小以及最大最小值。