java虚拟机体系分析

时间:2021-08-13 08:56:57

  一、JVM的生命周期:

1)程序开始执行,他就运行,程序停止,它就结束。有几个程序在执行,就有几个虚拟机在工作。只要Java虚拟机中还有普通的线程在执行,Java虚拟机就不会停止。

2)Java虚拟机总是开始于一个main()方法,这个方法必须是公有、返回void、接受一个字符串数组。在程序执行时,你必须给Java虚拟机指明这个包含main()方法的类名。 Main()方法是程序的起点,他被执行的线程初始化为程序的初始线程。程序中其他的线程都由他来启动。

3)Java中的线程分为两种:守护线程 (daemon)和普通线程(non-daemon)。守护线程是Java虚拟机自己使用的线程,比如负责垃圾收集的线程就是一个守护线程。包含Main()方法的初始线程不是守护线程。

4)在Java虚拟机的规范中定义了一系列的子系统、内存区域、数据类型和使用指南。这些组件构成了Java虚拟机的内部结构,他们不仅仅为Java虚拟机的实现提供了清晰的内部结构,更是严格规定了Java虚拟机实现的外部行为。

java虚拟机体系分析

 

 

 

 

                                                                                                  二、java虚拟机的体系结构: 

     每一个Java虚拟机都由一个类加载器子系统负责加载程序中的类型(类和接口),并赋予唯一的名字。每一个Java虚拟机都有一个执行引擎(execution engine)负责执行被加载类中包含的指令。

     程序的执行需要一定的内存空间,如字节码、被加载类的其他额外信息、程序中的对象、方法的参数、返回值、本地变量、处理的中间变量等等。Java虚拟机将这些信息统统保存在数据区中。

     每个Java虚拟机的实现中都包含数据区,但是Java虚拟机规范对数据区的规定却非常的抽象。许多结构上的细节部分都留给了 Java虚拟机实现者自己发挥。不同Java虚拟机实现上的内存结构千差万别。一部分实现可能占用很多内存,而其他以下可能只占用很少的内存;一些实现可能会使用虚拟内存,而其他的则不使用。这种比较精炼的Java虚拟机内存规约,可以使得Java虚拟机可以在广泛的平台上被实现。

     数据区中的一部分是整个程序共有,其他部分被单独的线程控制。每一个Java虚拟机都包含方法区(method area)和堆(heap),他们都被整个程序共享。

    Java虚拟机加载并解析一个类以后,将从类文件中解析出来的信息保存在方法区(保存解析出的类的信息)中。程序执行时创建的对象都保存在堆(保存对象)中。 

     当一个线程被创建时,会被分配只属于他自己的PC寄存器“pc register”(程序计数器)和Java堆栈(Java stack)。当线程不掉用本地方法时,其作用是,PC寄存器中保存线程执行的下一条指令。Java堆栈保存了一个线程调用方法时的状态,包括本地变量、调用方法的 参数、返回值、处理的中间变量。调用本地方法时的状态保存在本地方法堆栈中,可能在寄存器或者其他非平*立的内存中。

     Java堆栈有堆栈块组成。堆栈块包含Java方法调用的状态。当一个线程调用一个方法时,Java虚拟机会将一个新的块压到Java堆栈中,当这个方法运行结束时,Java虚拟机会将对应的块弹出并抛弃。

     区别:Java虚拟机不使用寄存器保存计算的中间结果,而是用Java堆栈在存放中间结果。这是的Java虚拟机的指令更紧凑,也更容易在一个没有寄存器的设备上实现Java虚拟机。 

   

                                                                                                三、类加载器子系统:

    Java虚拟机中的类加载器分为两种:原始类加载器(primordial class loader)和类加载器对象(class loader objects)。特点:原始类加载器是Java虚拟机实现的一部分,类加载器对象是运行中的程序的一部分。不同类加载器加载的类被不同的命名空间所分割。

     类加载器调用了许多Java虚拟机中其他的部分和java.lang包中的很多类。比如,类加载对象就是java.lang.ClassLoader子类的实例,ClassLoader类中的方法可以访问虚拟机中的类加载机制。

     每一个被Java虚拟机加载的类都会被表示为一个java.lang.Class类的实例。像其他对象一样,类加载器对象和Class对象都保存在堆中,被加载的信息被保存在方法区中。

     类加载器所做的工作:

     1、加载、连接、初始化(Loading, Linking and Initialization)

类加载子系统不仅仅负责定位并加载类文件,他按照以下严格的步骤作了很多其他的事情:

          1)、加载:寻找并导入指定类型(类和接口)的二进制信息

          2)、连接:进行验证、准备和解析

               ①验证:确保导入类型的正确性

               ②准备:为类型分配内存并初始化为默认值

               ③解析:将字符引用解析为直接引用

          3)、初始化:调用Java代码,初始化类变量为合适的值

     2、原始类加载器

    每个Java虚拟机都必须实现一个原始类加载器,他能够加载那些遵守类文件格式并且被信任的类。但是,Java虚拟机的规范并没有定义如何加载类,这由 Java虚拟机实现者自己决定。对于给定类型名的类型,原始类加载器必须找到那个类型名加“.class”的文件并加载入虚拟机中。

     3、类加载器对象

     虽然类加载器对象是Java程序的一部分,但是ClassLoader类中的三个方法可以访问Java虚拟机中的类加载子系统。

          1)、protected final Class defineClass(…):使用这个方法可以输入一个字节数组,定义一个新的类型。

          2)、protected Class findSystemClass(String name):加载指定的类,如果已经加载,就直接返回。

          3)、protected final void resolveClass(Class c):defineClass()方法只是加载一个类,这个方法负责后续的动态连接和初始化。

     4、命名空间

     当多个类加载器加载了同一个类时,为了保证他们名字的唯一性,需要在类名前加上加载该类的类加载器的标识。

 

  java虚拟机体系分析

 

 

                                                                                             四、方法区:

    在Java虚拟机中,被加载类型的信息都保存在方法区中。这些信息在内存中的组织形式由虚拟机的实现者定义,比如,虚拟机工作在一个“little- endian”的处理器上,他就可以将信息保存为“little-endian”格式的,虽然在Java类文件中他们是以“big-endian”格式保存的。设计者可以用最适合的表示格式来存储数据,以保证程序能够以最快的速度执行。但是,在一个只有很小内存的设备上,虚拟机的实现者就不会占用很大的内存。

     程序中的所有线程共享一个方法区,所以访问方法区信息的方法必须是线程安全的。如果你有两个线程都去加载一个叫Lava的类,那只能由一个线程被容许去加载这个类,另一个必须等待。

     在程序运行时,方法区的大小是可变的,程序在运行时可以扩展。有些Java虚拟机的实现也可以通过参数设定方法区的初始大小,最小值和最大值。

     方法区也可以被垃圾收集。因为程序中的由类加载器动态加载,所有类可能变成没有被引用(unreferenced)(虽然已经加载进来,但是没有引用!)的状态。当类变成这种状态时,他就可能被垃圾收集掉。没有加载的类包括两种状态,一种是真正的没有加载,另一个种是“unreferenced”的状态。

     1、类型信息(Type Information)

          每一个被加载的类型,在Java虚拟机中都会在方法区中保存如下信息:

          1)、类型的全名

          2)、类型的父类型的全名

          3)、给类型是一个类还是接口

          4)、类型的修饰符

          5)、 所有父接口全名的列表

          类型全名保存的数据结构由虚拟机实现者定义。除此之外,Java虚拟机还要为每个类型保存如下信息:

          1)、类型的常量池

          2)、类型字段的信息

          3)、类型方法的信息

          4)、 所有的静态类变量(非常量)信息

          5)、一个指向类加载器的引用

          6)、一个指向Class类的引用

          7)、类型的常量池

常量池中保存中所有类型是用的有序的常量集合,包含直接常量(literals)如字符串、整数、浮点数的常量,和对类型、字段、方法的符号引用。常量池中每一个保存的常量都有一个索引,就像数组中的字段一样。因为常量池中保存中所有类型使用到的类型、字段、方法的字符引用,所以它也是动态连接的主要对象。

     2)、类型字段的信息(Field information)

      字段名、字段类型、字段的修饰符、字段在类中定义的顺序。

     3)、类型方法的信息(Method information)

    方法名、方法的返回值类型(或者是void)、方法参数的个数、类型和他们的顺序、字段的修饰符、方法在类中定义的顺序

      4)、类(静态static)变量

     类变量被所有类的实例共享,即使不通过类的实例也可以访问。这些变量绑定在类上(而不是类的实例上),所以他们是类的逻辑数据的一部分。在Java虚拟机使用这个类之前就需要为类变量分配内存

     常量(final)的处理方式于这种类变量不一样。每一个类型在用到一个常量的时候,都会复制一份到自己的常量池中。常量也像类变量一样保存在方法区中,只不过他保存在常量池中。(可能是,类变量被所有实例共享,而常量池是每个实例独有的)。Non-final类变量保存为定义他的类型数据的一部分,而final常量保存为使用他的类型数据的一部分。

      5)、指向类加载器的引用

    每一个被Java虚拟机加载的类型,虚拟机必须保存这个类型是否由原始类加载器或者类加载器加载。那些被类加载器加载的类型必须保存一个指向类加载器的引用。当类加载器动态连接时,会使用这条信息。当一个类引用另一个类时,虚拟机必须保存那个被引用的类型是被同一个类加载器加载的,这也是虚拟机维护不同命名空间的过程。

      6)、指向Class类的引用(class.forName()的作用)

    Java虚拟机为每一个加载的类型创建一个java.lang.Class类的实例。你也可以通过Class类的方法:public static Class forName(String className)来查找或者加载一个类,并取得相应的Class类的实例。

     2、方法列表(Method Tables)

     为了更有效的访问所有保存在方法区中的数据,这些数据的存储结构必须经过仔细的设计。所有方法区中,除了保存了上边的那些原始信息外,还有一个为了加快存取速度而设计的数据结构,比如方法列表。每一个被加载的非抽象类,Java虚拟机都会为他们产生一个方法列表,这个列表中保存了这个类可能调用的所有实例方法的引用,报错那些父类中调用的方法。

                                                                                                五、堆:

当Java程序创建一个类的实例或者数组时,都在堆中为新的对象分配内存。虚拟机中只有一个堆,所有的线程都共享它(方法区也是可以被共享的!)。

     1、垃圾收集(Garbage Collection)

     垃圾收集是释放没有被引用的对象的主要方法。它也可能会为了减少堆的碎片,而移动对象。在Java虚拟机的规范中没有严格定义垃圾收集,只是定义一个Java虚拟机的实现必须通过某种方式管理自己的堆。

     2、对象存储结构(Object Representation)

     Java虚拟机的规范中没有定义对象怎样在堆中存储。每一个对象主要存储的是他的类和父类中定义的对象变量。对于给定的对象的引用,虚拟机必须能很快的定位到这个对象的数据。另外,必须提供一种通过对象的引用方法对象数据的方法,比如方法区中的对象的引用,所以一个对象保存的数据中往往含有一个某种形式指向方法区的指针。

     1)一个可能的堆的设计是将堆分为两个部分:引用池和对象池。一个对象的引用就是指向引用池的本地指针。每一个引用池中的条目都包含两个部分:指向对象池中对象数据的指针和方法区中对象类数据的指针。这种设计能够方便Java虚拟机堆碎片的整理。当虚拟机在对象池中移动一个对象的时候,只需要修改对应引用池中的指针地址。(理解:在引用池中一个引用就指向了一个对应的对象,移动对象的实质就是修改引用池的地址!)但是每次访问对象的数据都需要处理两次指针。

     2)另一种堆的设计是:一个对象的引用就是一个指向一堆数据和指向相应对象的偏移指针。这种设计方便了对象的访问,可是对象的移动要变的异常复杂。

     当程序试图将一个对象转换为另一种类型时,虚拟机需要判断这种转换是否是这个对象的类型,或者是他的父类型。当程序适用instanceof语句的时候也会做类似的事情。当程序调用一个对象的方法时,虚拟机需要进行动态绑定,他必须判断调用哪一个类型的方法。这也需要做上面的判断。

     无论虚拟机实现者使用哪一种设计,他都可能为每一个对象保存一个类似方法列表的信息。因为他可以提升对象方法调用的速度,对提升虚拟机的性能非常重要,但是虚拟机的规范中比没有要求必须实现类似的数据结构。

     每一个Java虚拟机中的对象必须关联一个用于同步多线程lock(mutex)。同一时刻,只能有一个对象拥有这个对象的锁。当一个拥有这个对象的锁,他就可以多次申请这个锁,但是也必须释放相应次数的锁才能真正释放这个对象锁。很多对象在整个生命周期中都不会被锁,所以这个信息只有在需要时才需要添加。很多Java虚拟机的实现都没有在对象的数据中包含“锁定数据”,只是在需要时才生成相应的数据。除了实现对象的锁定,每一个对象还逻辑关联到一 个“wait set”的实现。锁定帮组线程独立处理共享的数据,不需要妨碍其他的线程。“wait set”帮组线程协作完成同一个目标。“wait set”往往通过Object类的wait()和notify()方法来实现。

     垃圾收集也需要堆中的对象是否被关联的信息。Java虚拟机规范中指出垃圾收集一个运行一个对象的finalize方法一次,但是容许 finalize方法重新引用这个对象,当这个对象再次不被引用时,就不需要再次调用finalize方法。所以虚拟机也需要保存finalize方法 是否运行过的信息。

     3、数组的保存(Array Representation)

在Java 中,数组是一种完全意义上的对象,他和对象一样保存在堆中、有一个指向Class类实例的引用。所有同一维度和类型的数组拥有同样的Class,数组的长度不做考虑。对应Class的名字表示为维度和类型数组必须在堆中保存数组的长度,数组的数据和一些对象数组类型数据的引用。通过一个数组引用的,虚拟机应该能够取得一个数组的长度,通过索引能够访问特定的数据,能够调用Object定义的方法。Object是所有数据类的直接父类。

java虚拟机体系分析