`
stephen830
  • 浏览: 2968901 次
  • 性别: Icon_minigender_1
  • 来自: 上海
社区版块
存档分类
最新评论

Java的虚拟机JVM详细讲解(2)

    博客分类:
  • java
阅读更多
引用

文章内容摘自于一本很好的书:
《深入java虚拟机(原书第2版)》 作者:(美)Bill Venners 著,曹晓刚、蒋靖 译。

想深入了解JVM的的朋友可以去买来看看。


这篇文章在网络上转载的非常多,这里就没办法注明出处了!原来内容中缺少了几张图片,我重新将牵涉到的几张图片补充了进来,想要了解的朋友还是去买本书来看看吧。

回顾前篇《 Java的虚拟机JVM详细讲解(1) 》点这里:http://stephen830.iteye.com/blog/260537

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

  1)、类型的常量池(The constant pool for the type)

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

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

  4)、所有的静态类变量(非常量)信息(All class (static) variables declared in the type, except constants)

  5)、一个指向类加载器的引用(A reference to class ClassLoader)

  6)、一个指向Class类的引用(A reference to class Class)

  1)、类型的常量池(The constant pool for the type)

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

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

  字段名、字段类型、字段的修饰符(public,private,protected,static,final,volatile,transient等)、字段在类中定义的顺序。

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

  方法名、方法的返回值类型(或者是void)、方法参数的个数、类型和他们的顺序、字段的修饰符(public,private,protected,static,final,volatile,transient等)、方法在类中定义的顺序

  如果不是抽象和本地本法还需要保存

  方法的字节码、方法的操作数堆栈的大小和本地变量区的大小(稍候有详细信息)、异常列表(详细信息参见第十七章“Exceptions”。)

  4)、类(静态)变量(Class Variables)

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

  常量(final)的处理方式于这种类变量(non-final)不一样。每一个类型在用到一个常量的时候,都会复制一份到自己的常量池中。常量也像类变量一样保存在方法区中,只不过他保存在常量池中。(可能是,类变量被所有实例共享,而常量池是每个实例独有的)。Non-final类变量保存为定义他的类型数据(data for the type that declares them)的一部分,而final常量保存为使用他的类型数据(data for any type that uses them)的一部分。详情参见第六章“The Java Class FileThe Java Class File”

  5)、指向类加载器的引用(A reference to class ClassLoader)

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

  6)、指向Class类的引用(A reference to class Class)

  Java虚拟机为每一个加载的类型创建一个java.lang.Class类的实例。你也可以通过Class类的方法:

  public static Class forName(String className)来查找或者加载一个类,并取得相应的Class类的实例。通过这个Class类的实例,我们可以访问Java虚拟机方法区中的信息。具体参照Class类的JavaDoc.

  2、方法列表(Method Tables)

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

  八、堆

  当Java程序创建一个类的实例或者数组时,都在堆中为新的对象分配内存。虚拟机中只有一个堆,所有的线程都共享他。

  1、垃圾收集(Garbage Collection)

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

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

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

  一个可能的堆的设计是将堆分为两个部分:引用池和对象池。一个对象的引用就是指向引用池的本地指针。每一个引用池中的条目都包含两个部分:指向对象池中对象数据的指针和方法区中对象类数据的指针。这种设计能够方便Java虚拟机堆碎片的整理。当虚拟机在对象池中移动一个对象的时候,只需要修改对应引用池中的指针地址。但是每次访问对象的数据都需要处理两次指针。下图演示了这种堆的设计。在第九章的“垃圾收集”中的HeapOfFish Applet演示了这种设计。

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

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

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


  1)、一个指向类型数据的指针

  2)、一个对象的方法列表。方法列表是一个指向所有可能被调用对象方法的指针数组。方法数据包括三个部分:操作码堆栈的大小和方法堆栈的本地变量区;方法的字节码;异常列表。

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

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


3、数组的保存(Array Representation)

  在Java 中,数组是一种完全意义上的对象,他和对象一样保存在堆中、有一个指向Class类实例的引用。所有同一维度和类型的数组拥有同样的Class,数组的长度不做考虑。对应Class的名字表示为维度和类型。比如一个整型数据的Class为“[I”,字节型三维数组Class名为“[[[B”,两维对象数据 Class名为“[[Ljava.lang.Object”。

  多维数组被表示为数组的数组,如下图:


  数组必须在堆中保存数组的长度,数组的数据和一些对象数组类型数据的引用。通过一个数组引用的,虚拟机应该能够取得一个数组的长度,通过索引能够访问特定 的数据,能够调用Object定义的方法。Object是所有数据类的直接父类。更多信息参见第六章“类文件”。

  九、PC寄存器(程序计数器)(The Program Counter)

  每一个线程开始执行时都会被创建一个程序计数器。程序计数器只有一个字长(word),所以它能够保存一个本地指针和returnValue. 当线程执行时,程序计数器中存放了正在执行指令的地址,这个地址可以使一个本地指针,也可以使一个从方法字节码开始的偏移指针。如果执行本地方法,程序计数器的值没有被定义。

  十、Java堆栈(The Java Stack)

  当一个线程启动时,Java虚拟机会为他创建一个Java堆栈。Java堆栈用一些离散的frame类纪录线程的状态。Java虚拟机堆Java堆栈的操作只有两种:压入和弹出frames.

  线程中正在执行的方法被称为当前方法(current method),当前方法所对应的frame被称为当前帧(current frame)。定义当前方法的类被称为当前类(current class),当前类的常量池被称为当前常量池(current constant pool.)。当线程执行时,Java虚拟机会跟踪当前类和当前常量池。但线程操作保存在帧中的数据时,他只操作当前帧的数据。

  当线程调用一个方法时,虚拟机会生成一个新的帧,并压入线程的Java堆栈。这个新的帧变成当前帧。当方法执行时,他使用当前帧保存方法的参数、本地变量、中间结构和其他数据。方法有两种退出方式:正常退出和异常推出。无论方法以哪一种方式推出,Java虚拟机都会弹出并丢弃方法的帧,上一个方法的帧变为当前帧。

  所有保存在帧中的数据都只能被拥有它的线程访问,线程不能访问其他线程的堆栈中的数据。所以,访问方法的本地变量时,不需要考虑多线程同步。

  和方法区、堆一样,Java堆栈不需要连续的内存空间,它可以被保存在一个分散的内存空间或者堆上。堆栈具体的数据和长度都有Java虚拟机的实现者自己定义。一些实现可能提供了执行堆栈最大值和最小值的方法。

  十一、堆栈帧(The Stack Frame)

  堆栈帧包含三部分:本地变量、操作数堆栈和帧数据。本地变量和操作数堆栈的大小都是一字(word)为单位的,他们在编译就已经确定。帧数据的大小取决于 不同的实现。当程序调用一个方法时,虚拟机从类数据中取得本地变量和操作数堆栈的大小,创建一个合适大小和帧,然后压入Java堆栈中。

  1、本地变量(Local Variables)

  本地变量在Java堆栈帧中被组织为一个从0计数的数组,指令通过提供他们的索引从本地变量区中取得相应的值。 Int,float,reference, returnValue占一个字,byte,short,char被转换成int然后存储,long和doubel占两个字。

  指令通过提供两个字索引中的前一个来取得long,doubel的值。比如一个long的值存储在索引3,4上,指令就可以通过3来取得这个long类型的值。

  本地变量区中包含了方法的参数和本地变量。编译器将方法的参数以他们申明的顺序放在数组的前面。但是编译器却可以将本地变量任意排列在本地变量数组中,甚至两个本地变量可以公用一个地址,比如,当两个本地变量在两个不交叠的区域内,就像循环变量i,j.

  虚拟机的实现者可以使用任何结构来描述本地变量区中的数据,虚拟机规范中没有定义如何存储long和doubel.

  2、操作数堆栈(Operand Stack)

  向本地变量一样,操作数堆栈也被组织为一个以字为单位的数组。但是不像本地变量那样通过索引访问,而是通过push和pop值来实现访问的。如果一个指令push一个值到堆栈中,那么下一个指令就可以pop并且使用这个值。

  操作数堆栈不像程序计数器那样不可以被指令直接访问,指令可以直接访问操作数堆栈。Java虚拟机是一个以堆栈为基础,而不是以寄存器为基础的,因为它的指令从堆栈中取得操作数,而不是同寄存器中。当然,指令也可以从其他地方去的操作数,比如指令后面的操作码,或者常量池。但是Java虚拟机指令主要是从操作数堆栈中取得他们需要的操作数。

  Java虚拟机将操作数堆栈视为工作区,很多指令通过先从操作数堆栈中pop值,在处理完以后再将结果push回操作数堆栈。一个add的指令执行过程如下图所示:先执行iload_0和iload_1两条指令将需要相加的两个数,从本地方法区中取出,并push到操作数堆栈中;然后执行iadd指令,现 pop出两个值,相加,并将结果pusp进操作数堆栈中;最后执行istore_2指令,pop出结果,赋值到本地方法区中。

  3、帧数据(Frame Data)

  处理本地变量和操作数堆栈以外,java堆栈帧还包括了为了支持常量池,方法返回值和异常分发需要的数据,他们被保存在帧数据中。

  当虚拟机遇到使用指向常量池引用的指令时,就会通过帧数据中指向常量区的指针来访问所需要的信息。前面提到过,常量区中的引用在最开始时都是符号引用。即使当虚拟机检查这些引用时,他们也是字符引用。所以虚拟机需要在这时转换这个引用。

  当一个方法正常返回时,虚拟机需要重建那个调用这个方法的方法的堆栈帧。如果执行完的方法有返回值,虚拟机就需要将这个值push进调用方法的哪个操作数堆栈中。

  帧数据中也包含虚拟机用来处理异常的异常表的引用。异常表定义了一个被catch语句保护的一段字节码。每一个异常表中的个体又包含了需要保护的字节玛的范围,和异常被捕捉到时需要执行的字节码的位置。当一个方法抛出一个异常时,Java虚拟机就是用异常表去判断如何处理这个异常。如果虚拟机找到了一个匹配的catch,他就会将控制权交给catch语句。如果没有找到匹配的catch,方法就会异常返回,然后再调用的方法中继续这个过程。

  除了以上的三个用途外,帧数据还可能包含一些依赖于实现的数据,比如调试的信息。

  十二、本地方法堆栈

  本地方法区依赖于虚拟机的不同实现。虚拟机的实现者可以自己决定使用哪一种机制去执行本地方法。

  任何本地方法接口(Native Method Interface)都使用某种形式的本地方法堆栈。

十三、执行引擎

  一个java虚拟机实现的核心就是执行引擎。在Java虚拟机规范,执行引擎被描述为一系列的指令。对于每一个指令,规范都描述了他们应该做什么,但是没有说要如何去做。

  1、指令集

  在Java虚拟机中一个方法的字节码流就是一个指令的序列。每一个指令由一个字节的操作码(Opcode)和可能存在的操作数(Operands)。操作码指示去做什么,操作数提供一些执行这个操作码可能需要的额外的信息。一个抽象的执行引擎每次执行一个指令。这个过程发生在每一个执行的线程中。

  有时,执行引擎可能会遇到一个需要调用本地方法的指令,在这种情况下,执行引擎会去试图调用本地方法,但本地方法返回时,执行引擎会继续执行字节码流中的下一个指令。本地方法也可以看成对Java虚拟机中的指令集的一种扩充。

  决定下一步执行那一条指令也是执行引擎工作的一部分。执行引擎有三种方法去取得下一条指令。多数指令会执行跟在他会面的指令;一些像goto, return的指令,会在他们执行的时候决定他们的下一条指令;当一个指令抛出异常时,执行引擎通过匹配catch语句来决定下一条应该执行的指令。

  平台独立性、网络移动性、安全性左右了Java虚拟机指令集的设计。平台独立性是指令集设计的主要影响因素之一。基于堆栈的结构使得Java虚拟机可以在 更多的平台上实现。更小的操作码,紧凑的结构使得字节码可以更有效的利用网络带宽。一次性的字节码验证,使得字节码更安全,而不影响太多的性能。

  2、执行技术

  许多种执行技术可以用在Java虚拟机的实现中:解释执行,及时编译(just-in-time compiling),hot-spot compiling,native execution in silicon.

  3、线程

  Java虚拟机规范定义了一种为了在更多平台上实现的线程模型。Java线程模型的一个目标时可以利用本地线程。利用本地线程可以让Java程序中的线程能过在多处理器机器上真正的同时执行。

  Java线程模型的一个代价就是线程优先级,一个Java线程可以在1-10的优先级上运行。1最低,10最高。如果设计者使用了本地线程,他们可能将这 10个优先级映射到本地优先级上。Java虚拟机规范只定义了,高一点优先级的线程可以却一些cpu时间,低优先级的线程在所有高优先级线程都堵塞时,也可以获取一些cpu时间,但是这没有保证:低优先级的线程在高优先级线程没有堵塞时不可以获得一定的cpu时间。因此,如果需要在不同的线程间协作,你必须使用的“同步(synchronizatoin)”。

  同步意味着两个部分:对象锁(object locking)和线程等待、激活(thread wait and notify)。对象锁帮助线程可以不受其他线程的干扰。线程等待、激活可以让不同的线程进行协作。

  在Java虚拟机的规范中,Java线程被描述为变量、主内存、工作内存。每一个Java虚拟机的实例都有一个主内存,他包含了所有程序的变量:对象、数组合类变量。每一个线程都有自己的工作内存,他保存了哪些他可能用到的变量的拷贝。规则:

  1)、从主内存拷贝变量的值到工作内存中

  2)、将工作内存中的值写会主内存中

  如果一个变量没有被同步化,线程可能以任何顺序更新主内存中的变量。为了保证多线程程序的正确的执行,必须使用同步机制。

  十四、本地方法接口(Native Method Interface)

  Java虚拟机的实现并不是必须实现本地方法接口。一些实现可能根本不支持本地方法接口。Sun的本地方法接口是JNI(Java Native Interface)。
  • 大小: 93.8 KB
  • 大小: 19 KB
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics