java虚拟机总结

类型的生命周期:

java虚拟机通过装载、连接和初始化一个java类型,使该类型可以被正在运行的java程序所使用。

装载:是把二进制形式的java类型读入java虚拟机中。

连接:是把读入的二进制形式的类型数据合并到虚拟机的运行时状态中去。连接分三个子步骤(验证、准

            备和解析)。验证步骤确保java类型数据格式正确并且适合于java虚拟机使用;准备步骤则负责为

            该类型分配它所需的内存,并将类变量设置为默认值;解析步骤负责把常量池中的符号引用转换为

            直接引用。虚拟机的实现可以推迟解析这一步,它可以在程序真正使用某个符号引用时再去解析它。

初始化:当验证、准备和解析(可选)步骤都完成时,该类型就已经为初始化做好了准备,在初始化期

            间,将给类变量赋以适当的初始值。

初始化时机:

所有的虚拟机实现必须在每个类或接口首次主动使用时初始化。

符合主动使用的情形:

(1) 当创建某个类的新实例时(在字节码中执行new指令,不明确的创建:反射、clone和反序列化)。

(2) 当调用某个类的静态方法时(在字节码中执行invokestatic指令)。

(3) 当使用某个类或接口的静态字段,或者对该字段赋值时(在字节码中执行getstatic或putstatic),用

     final修饰的静态字段除外,它被初始化为一个编译时的常量表达式。

(4) 当调用java API中的某些反射方法时,比如Class类中的方法或者java.lang.reflect包中的类的方法。

(5) 当初始化某个类的子类时(类初始化时,要求它的超类已经被初始化)。

(6) 当虚拟机启动时某个被标明为启动类的类(含有main方法的那个类)。

    除了这6种情形外,所有其他使用java类型的方式都是被动使用,它们不会导致java类型的初始化。

    任何一个类的初始化都要求它的超类在此之前已经完成初始化;然而,对于接口来说,这条规则并不适

    用。只有在某个接口所声明的非常量字段被使用时,该接口才会被初始化,而不会因为实现这个接口的子

    接口或类要初始化而被初始化。而且一个接口的初始化,并不要求它的祖先接口预先初始化。

主动使用和被动使用:

    使用一个非常量的静态字段,只有当类或者接口的确声明了这个字段时才是主动使用。比如,类中声明的静态字段可能会被子类引用,对于子类来说就是被动使用,使用它们不会触发子类的初始化。

    如果一个字段是static final的,并且使用一个编译时常量表达式初始化,使用这样的字段就不是对声明该字段的类的主动使用。java编译器把这样的字段解析成对常量的本地拷贝(该常量存在于引用者类的常量池或者字节码流中)。

类的初始化(clinit):

    通过类变量初始化语句或静态初始化语句块为类变量赋予正确的初始值。java源码中所有的类变量初始化语句和静态初始化块都被java编译器收集到一起,放到一个特殊的方法中。对于类来说,这个方法被称作类初始化方法;对于接口来说,它被称为接口初始化方法。在类和接口的class文件中,这个方法被称为<clinit>。java程序是无法调用这个方法的,它只能被虚拟机调用,专门用于为类型的静态变量设置正确的初始值。

初始化同步:

    java虚拟机确保初始化过程被正确地同步。如果多个线程需要初始化一个类,仅仅允许一个线程来进行初始化,其他线程需要等待。当活动的线程完成了初始化过程之后,它必须通知其他等待的线程。

初始化步骤:

(1) 如果类存在直接超类,且直接超类还没有被初始化,就先进行直接超类的初始化(第一个被初始化的类

      永远是Objec)。初始化接口并不需要初始化它的父接口。

(2) 如果类或接口存在一个初始化方法,就执行此方法(<clinit>)。

    <clinit>()方法的代码并不显式地调用超类的<clinit>()方法。在java虚拟机调用类的<clinit>()方法之前,它必须确认超类的<clinit>()方法已经被执行了。

    并非所有的类都需要在它们的class文件中拥有一个<clinit>()方法。如果类没有声明任何类变量,也没有静态初始化语句,那么它就不会有<clinit>()方法。如果类声明了类变量,但是没有明确使用类变量初始化语句或者静态初始化语句,那么类也不会有<clinit>()方法。如果类仅包含static final变量的初始化语句,而且这些类变量的初始化语句采用编译时常量表达式,类也不会有<clinit>()方法。只有那些的确需要执行java代码来赋予类变量正确初始值的类才会有类初始化<clinit>()方法。

    所有在接口中声明的隐式 public static final字段都必须在字段初始化语句中初始化。如果接口包含任何不能在编译时被解析成一个常量的字段初始化语句,接口就会拥有一个<clinit>()方法。

类实例化:

类可以被明确或者隐含地实例化,实例化一个类有四种途径:

(1) 明确地使用new操作符。 

(2) 调用Class或者java.lang.reflect.Construtor对象的newInstance()方法。

(3) 调用任何现有对象的clone()方法。

(4) 通过java.io.ObjectInputStream类的getObject()方法反序列化。

类实例的初始化:

    当java虚拟机创建一个类的新实例时,首先需要在堆中为保存对象的实例变量分配内存。所有在对象的类中和它的超类中声明的变量(包括private的实例变量)都要分配内存。一旦虚拟机为新的对象准备好了堆内存,它立即把实例变量初始化为默认的初始值。随后虚拟机就会为实例变量赋正确的初始值。

    根据创建对象的方法不同,java虚拟机使用三种技术之一来完成这个工作。

    如果对象通过clone()调用创建的,虚拟机把原来被克隆的实例变量中的值拷贝到新对象中。

    如果对象是通过readObject()调用反序列化的,虚拟机通过从输入流中读入的值来初始化那些非transient类型的实例变量。

    否则,虚拟机调用对象的实例初始化方法<init>。

    java编译器为它编译的每一个类都至少生成一个实例初始化方法。在class文件中这个实例初始化方法被称为<init>。针对源码中类的每一个构造方法,java编译器都产生一个<init>()方法。如果类没有明确声明任何构造方法,编译器默认产生一个无参数的构造方法,它仅仅调用超类的无参构造方法;编译器在class文件中创建一个<init>()方法,对应它的默认构造方法。

    如果构造方法中通过明确的this()调用另一个构造方法。它对应的<init>()方法由两部分组成:一个同类与this()参数相同的<init>()方法的调用;实现了对应构造方法的方法体的字节码。

    如果构造方法不是通过this()调用开始的(代码第一行不是this()),而且这个类型不是Object,<init>()方法则由三部分组成:一个超类的无参<init>()方法调用(如果是Object,这项不存在);任意实例变量初始化语句的字节码;实现了对应构造方法的方法体的字节码。

    如果构造方法通过明确的super()开始,它的<init>()方法会调用对应参数类型的超类<init>()方法;任意实例变量初始化语句直接码;实现了对应构造方法的的方法体的字节码。

    对于除Object外的每一个类,<init>()方法都必须从另一个<init>()方法调用开始。<init>()方法不允许捕捉由它们所调用的<init>()方法抛出的任何异常;如果超类的<init>()方法被意外中止了,那么子类的<init>()方法也必须同样被意外中止。

卸载类型:

    java虚拟机判断一个动态装载的类型是否仍然被程序需要,其方式与判断对象是否仍然被程序需要的方式很类似。如果程序不再引用某个类型,那么这个类型就无法在对未来的计算过程产生影响。类型编程不可触及的,而且可以被垃圾收集。

    判断动态装载的类型的Class实例在正常的垃圾收集过程中是否可触及有两种方式:

    (1) 如果程序保存对Class实例的明确引用,它就是可触及的。

    (2) 如果在堆中还存在一个可触及的对象,在方法区中它的类型数据指向一个Class实例,那么这个Class实

         例就是可触及的。

虚拟机垃圾收集:

    垃圾检测通常通过建立一个根对象的集合,并且检查从这些根对象开始的可触及性来实现。如果正在执行的程序可以访问到的根对象与某个对象之间存在引用路径,那么这个对象就是可触及的。对于程序来说,根对象总是可以访问的,从这些根对象开始,任何无法被触及的对象被认为是垃圾,它们不再影响程序的未来运行。

    区分活动对象和垃圾的两个基本方法时引用计数和跟踪。引用计数垃圾收集器通过为堆中的每一个对象保持一个计数来区分活动对象和垃圾对象,引用计数记录下了对那个对象的引用次数。跟踪垃圾收集器实际上追踪从根节点开始的引用图,在追踪中遇到的对象以某种方式打上标记,当追踪结束时,没有被打上标记的对象被判定为不可触及的,可以被当作垃圾收集。

 

引用计数垃圾收集器:

    引用计数是垃圾收集的早期策略。当一个对象被创建并且指向该对象的引用被分配给一个变量,这个对象的引用计数被置为1;当任何其他变量被赋值为堆这个对象的引用时,计数加1;当一个对象的引用超过了生存期或被设置一个新的值时,对象的引用计数减1。任何引用计数为0的对象都可以被当作垃圾收集。这种算法的缺点是:引用计数无法检测出循环引用(多个对象互相引用);每次引用计数的增加或者减少都带来额外的开销。

跟踪垃圾收集器:

    因为引用计数方法固有的缺陷,这种技术已经不为人所接受。现实所遇到的java虚拟机更可能在垃圾收集中使用追踪算法。跟踪收集器追踪从根节点开始的对象引用图。基本的追踪算法被称作“标记并清除”,在标记阶段,垃圾收集器遍历引用数,标记每一个遇到的对象;在清除阶段,未被标记的对象被释放了,使用的内存被返回给正在执行的程序;清除步骤必须包括对象的终结。标记并清除收集器通常使用两种策略对付堆碎块:压缩和拷贝。

    压缩收集器:把活动的对象越过空闲区滑动到堆的一端。在这个过程中,堆的另一端出现一个大的连续空闲区,所有被移动的引用也被更新,指向新的位置。更新被移动的对象引用有时候通过一个间接对象引用层。不直接引用堆中的对象,对象的引用实际上指向一个对象句柄表;对象句柄才指向堆中对象的实际位置。当对象被移动了,只有这个句柄需要被更新为新位置。这种方法简化了消除碎块的工作,但每一次对象访问都带来性能损失。

 

    拷贝收集器:把所有活动对象移动到一个新的区域。在拷贝的过程中,它们被紧挨着布置,所以可以消除原本他们在旧区域的空隙。原本的区域被认为都是空闲区。这种方法的好处是对象可以在从根对象开始遍历的过程中随着发现而被拷贝,不再有标记和清除的区分。对象被快速拷贝到新区域,同时转向指针仍然留在原来的位置。转向指针可以让垃圾收集器发现已经被转移的对象的引用,然后垃圾收集器可以把这些引用设置为转向指针的值,所以它们现在指向对象的新位置。拷贝收集器算法被称为“停止并拷贝”。这个方案中,堆被分为两个区域,任何时候都只使用其中的一个区域;对象在同一个区域中分配,直到这个区域被耗尽;此时,程序执行被中止,堆被遍历,遍历时遇到的活动对象被拷贝到另一个区域;当停止和拷贝过程结束时,程序恢复执行,内存将从新的堆区域中分配,直到它也被耗尽。这种方法的代价是,对于指定大小的堆来说需要两倍大小的内存。

    按代收集的收集器:简单的停止拷贝收集器的缺点是,每一次收集时,所有的活动对象都必须被拷贝,它每次都把那些生命周期很长的对象来回拷贝,消耗大量的时间。按代收集的收集器通过把对象按照寿命来分组解决这个效率低下的问题,更多地收集那些短暂出现的年幼对象,而非寿命较长的对象。在这种方案中,堆被分成多个子堆,每个子堆为一代对象服务;最年幼的那一代进行最频繁的垃圾收集,如果一个最年幼的对象进过几次垃圾收集后仍然存活,那么它就被转移到另外一个代表更高寿命的子堆中去;每当对象在它所属的子堆中变得成熟(逃过多次垃圾收集)之后,它们就被转移到代表更高年龄的子堆中去。

火车算法:

    垃圾收集算法和明确释放对象比起来有一个潜在的缺点,即垃圾收集算法中程序员堆安排CPU时间进行内存回收缺乏控制。因为垃圾收集一般都会停止整个程序的运行来查找和收集垃圾对象,垃圾收集可能使得程序对事件响应迟钝,无法满足实时系统的要求。达到非破坏性垃圾收集的方法是使用渐进式的收集算法。渐进式垃圾收集器不会试图一次性发现并回收所有不可触及的对象,而是每次发现并回收一部分,因此理论上说每一次收集会持续更短的时间,如果每次可以保证(或者非常接近)不超过一个最大时间长度,就可以让java虚拟机适合实时环境;这样的收集器也可以消除用户可察觉的到的垃圾收集停顿。

    火车算法把成熟对象空间划分为固定长度的内存块,算法每次只会在一个块中单独执行。每一个块归属于一个集合,在一个集合内的块排了序,这些集合本身也排了序。火车算法中,块被称为车厢,集合被称为火车,成熟空间扮演火车站的角色。火车按照他们创建时的顺序分配号码,号码较小的火车总是更早出现的火车。在火车内部,车厢(块)总是被附加到火车的尾部,因此,较小数字表示更早出现的车厢。用这种命名方式表示成熟对象空间中所有的块的总体顺序。

    火车算法每次执行的时候,只会对一个块(号码最低的块)执行垃圾收集。对象从更年轻的子堆提出来进入成熟对象空间,不管何时提出,它们都被附加到任何已经存在的火车中(最小号码火车除外),或者专门为容纳它们创建的一列新火车中。

    车厢收集:每次火车算法被执行的时候,它要么收集最小数字火车中的最小数字车厢,要么收集整列最小数字火车。算法首先检查执行整列火车中任何车厢的引用,如果不存在任何来自火车以外的引用指向它内部包含的对象,那么整列火车都是垃圾,算法归还火车中所有车厢中的对象并返回。这步算法使得火车算法可以一次收集大型的,无法在一个块中容纳的循环数据结构。如果火车里并不都是垃圾,那么算法把注意力放到最小数字车厢上。算法首先把被车厢外部的车厢引用的对象转移到其他车厢去;当进行这个移动后,车厢里任何保留下来的对象都是没有引用的,可以被垃圾收集。算法归还最小数字车厢占据的空间并返回。

    保证整列火车中没有循环的数据结构的关键是算法如何移动对象。如果正被收集的车厢中有一个对象被来自成熟空间以外的对象引用,这个对象被转移到正在被收集的火车之外的其他车厢去。如果对象被成熟对象空间的其他火车引用,对象就被转移到引用它的那列火车中去;然后转移过去的对象被扫描,查找对原车厢的引用,发现的任何被转移对象引用的对象都被转移到引用它的火车中去;新转移的对象也被扫描,这个过程不断重复,直到没有任何来自其他火车的引用指向正被收集的那节车厢。如果接收对象的车厢没有空间了,算法会创建新的车厢并附加到那列火车的尾部。一旦没有来自火车外的引用了,那么这节车厢剩余的外部引用都是来自于同一列火车的其他车厢;算法把这样的对象转移到最小数字火车的最后一个车厢去,然后扫描这些对象,查找对原被收集车厢的引用,任何新发现的被引用对象也都被转移到同一列列车的尾部,这个过程不断重复,直到没有任何形式的引用指向被收集的车厢。然后算法归还整个最小数字车厢占据的空间并且返回。

    火车算法最重要的方面之一,就是它保证大型的循环数据会完全被收集,即使它们不能被放置在一个车厢中。因为对象被转移到引用它们的火车,相关的对象会变得集中。最后,成为垃圾的循环数据结构中的所有对象,不管有多大,会被放置到同一列火车中去,增大循环数据结构的大小只会增加最终组成同一列火车的车厢数。

    记忆集合:为了促进收集过程,火车算法使用了记忆集合。一个记忆集合是一个数据结构,它包含了所有对一节车厢或者一列火车的外部引用。算法为成熟对象空间内每节车厢和每列火车都维护一个记忆集合。一个空的记忆集合显示车厢或者火车中的对象都不再被外部引用,它们是不可触及的,可以被垃圾收集。

动态连接和常量池解析:

    java class文件把它所有的引用符号保存在常量池中。每一个class文件有一个常量池,每一个被java虚拟机装载的类或者接口都有一份内部版本的独立常量池,被称为运行时常量池。运行时常量池是一个特定于虚拟机实现的数据结构,数据结构映射到class文件中的常量池。因此,当一个类被首次装载时,所有来自于类型的符号引用都装载到了类型的运行时常量池。

    在程序运行时,如果某个特定的符号引用将要被使用,它首先要被解析。解析过程就是根据符号引用查找到实体,在把符号引用替换成一个直接引用的过程。因为所有的符号引用都保存在常量池中,所以这个过程也称作常量池解析。

    java虚拟机为每一个装载的类和接口保存一份独立的常量池。每一个常量池入口可能被多个指令引用,但它只被解析一次,当符号引用被一条指令解析过后,来自其他指令后续访问尝试会认为解析工作已经完成,都使用第一次解析的直接引用结果。

    虽然虚拟机的实现有选择何时解析符号引用的自由,但不管怎样,都应该给外界一个迟解析的印象。不管何时解析,都应该在程序第一次实际访问一个符号引用的时候抛出错误。

    在java虚拟机解析符号引用时,它可以选择类装载器,当解析常量池中的入口需要装载类型的时候,它使用装载引用类型的同一个类装载器来装载所需的类型。比如,使用启动类装载器装载的类型,当它的符号引用被解析时,虚拟机也使用启动类的装载器来装载被引用的类型。使用用户自定义装载器装载的类型,当它的符号引用被解析时,虚拟机也使用同一个用户自定义的类装载器来装载被引用的类型。

常量池解析:如果解析过程中抛出了错误,错误被看成是由指向执行解析的常量池入口的引用者抛出的。

类装载器和双亲委派模型:

(1) Bootstrap ClassLoader : 将存放于<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(仅按照文件名识别,如 rt.jar 名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。启动类加载器无法被Java程序直接引用

(2) Extension ClassLoader : 将<JAVA_HOME>\lib\ext目录下的,或者被java.ext.dirs系统变量所指定的路径中的所有类库加载。开发者可以直接使用扩展类加载器。

(3) Application ClassLoader : 负责加载用户类路径(ClassPath)上所指定的类库,开发者可直接使用。

    java1.2版本中,每一个用户自定义的类装载器在创建时被分配一个双亲类装载器,如果没有显式传递一个双亲类装载器给用户自定义的类装载器的构造方法,系统类装载器被默认指定为双亲。如果向构造方法传递了null,启动类装载器成为双亲。

    当符合双亲委派模型的类装载器装载一个类型的时候,它首先委派给它的双亲(请求它的双亲试着装载这个类型),这个委派的过程一直进行到委派链的末端,一般来说时启动类装载器。

    在java术语中,要求某个类装载器装载一个类型,但是却返回了其他类装载器装载的类型,这种装载器被称为是那个类型的初始类装载器。而实际装载和定义那个类型的类装载器被称为那个类型的定义类装载器。任何被要求装载类型,并且能够返回Class实例的引用的类装载器都是这个类型的初始类装载器。

类型转换:

    java虚拟机包括许多进行基本类型转换工作的操作码,这些执行转换工作的操作码后面没有操作数,转换的值从栈顶端获得。java虚拟机从栈顶端弹出一个值,对它进行转换, 然后再把转换结果压入栈。

    不存在把byte、char、short转换为int的指令,因为任何byte、char、short类型值压入栈的时候,就已经有效地被转换为int类型值。从数组或堆中的对象中接受byte、char、short类型值的指令和把这些值压入栈的指令都会把它们转换为int类型值。

    涉及byte、char、short类型的运算操作首先会把它们转换为int类型,然后对int类型值进行运算,最后得到int类型的结果。如果需要得到相应更小类型的结果,必须将这个int结果显式转换为对应的类型。

二进制补码运算:

    java虚拟机支持的所有整数类型:byte、short、int、long,它们都是带符号的二进制补码数。在一个二进制补码数中,最重要的位是它的符号位(最高位),0表示正整数和0,1表示负整数。

    能够被二进制补码表示的数值范围为:2的总位数的次幂。其中一半是0和正整数,一半是负整数。

    在带符号二进制补码数上进行的加法运算与在无符号二进制数上进行的加法运算一样。两个数相加(忽略最高位的溢出),结果被解释为一个带符号的二进制补码数。java虚拟机中出现的整数运算的溢出并不会导致抛出异常,其结果只被简单地截短以符合数据类型(int类型或者long类型)。在java编程中,必须随时注意可能发生的溢出,确认所选择的数据类型是否正确。

微型子例程:

    字节码中的finally子句在方法内部的表现很想“微型子例程”。java虚拟机在每个try语句块和与其相关的catch子句的结尾都会调用finally子句的子例程,finally子句结束后(这里结束指的是finally子句中的最后一条语句正常执行完毕,没有抛出异常,或执行return、continue、break等情况),隶属于这个finally子句的微型子例程执行“返回”操作;程序在第一次调用微型子例程的地方继续执行后面的语句。

    jsr指令是使java虚拟机跳转到微型子例程的操作码。jsr使用一个双字节长度的操作数,这个操作数指出从jsr指令到微型子例程开始处的16位带符号的偏移量。当java虚拟机遇到jsr指令时,它会把返回地址压入栈,然后从finally微型子例程的开始处继续执行。返回地址是紧接在jsr操作码和操作数后面的字节码地址(偏移量或本地指针),该地址的类型为returnAddress。

    微型子例程执行完毕后,将调用ret指令,ret指令执行从子例程中返回的操作,ret指令只有一个操作数,这个操作数是一个存储返回地址的局部变量的索引。

    jsr指令并不会调用java方法,它只能跳转到相同方法的不同操作码处;同样,ret指令也不能令java方法返回,它只能使虚拟机调回相同方法中调用jsr操作码和它的操作数之后的位置。

    在每一个finally微型子例程的开始处,返回地址都会从栈顶弹出,并且存储在局部变量中,ret指令将会从这个局部变量中取出返回地址。这样做是由于,finally微型子例程本身会抛出异常或者含有return、break、continue等语句;由于这些可能性的存在,被jsr指令压入栈的额外返回地址必须立即从栈中移除。当finally子例程通过return、break、continue或者抛出异常退出时,栈中的额外返回地址就不必考虑了。

方法调用和返回:

    当java虚拟机调用一个类方法时,它会基于对象引用的类型(编译时可知)来选择所调用的方法;相反,当虚拟机调用一个实例方法时,它会基于对象实际的类(只能在运行时得知)来选择所调用的方法。

    最初,所有的调用指令都指向一个包含符号引用的常量池入口。当java虚拟机遇到一条调用指令时,如果还没有解析符号引用,那么虚拟机把符号引用的解析作为执行指令调用执行过程的一部分。一旦解析了一个方法后,java虚拟机就准备调用它;如果这个方法是一个实例方法,它必须在一个对象中被调用;对实例方法的调用,虚拟机需要在栈里存在一个对象引用this(objectref);如果方法需要参数,那么虚拟机还需要在栈中存在该方法所需要的参数(args)。如果这个方法是一个类方法,虚拟机只需要栈中存在args参数。objectref和args必须在调用指令执行前,被其他指令压入所调用方法的操作数栈。

    虚拟机为每一个调用的java方法(非本地方法)建立一个新的栈帧。栈帧包括:为方法的局部变量所预留的空间、该方法的操作数栈以及特定虚拟机实现需要的其他所有信息。局部变量表和操作数栈的大小在编译时计算出来,并放置到class文件中去。虚拟机借此可以了解方法的栈帧需要多少内存;当它调用一个方法时,它为该方法创建恰当大小的栈帧,再将新的栈帧压入java栈。

    处理实例方法时,虚拟机把所调用方法栈帧中的操作数栈中弹出objectref和args。虚拟机把objectref作为局部变量0放到新的栈帧中,把所有的args作为局部变量1、2 、。。。objectref的值是隐式传递给所有实例方法的this指针。对于类方法,虚拟机只弹出参数,并将它们放到局部变量的0、1、2、。。。然后虚拟机把新的栈帧作为当前栈帧,并将PC寄存器(程序计数器)指向方法的第一条指令。

 

    所有方法调用中的目标方法在class文件中都是一个常量池中的符号引用,在类加载的解析阶段,会将其中一部分符号引用转化为直接引用,这种解析能成立的前提是:方法在程序真正运行之前就有一可确定的调用版本,并且这个方法的调用版本是运行期是不可改变的。换句话说,调用目标在程序代码写好、编译器进行编译时就必须确定下来。这类方法的调用称为解析(Resolution)。

    在Java语言中,符合“编译期可知,运行期不可变”这个要求的方法有静态方法和私有方法两大类,前者与类型直接相关联,后者在外部不可被访问,这两种方法都不可能通过继承或者别的方式重写出其它版本,因此它们都适合在类加载阶段进行静态解析。

    与之相对应,在Java虚拟机里提供了四条方法调用字节码指令,分别是:

    a. invokestatic:调用静态方法

    b. invokespecial:调用实例构造器<init>方法,私有方法和超类方法。

    c. invokevirtual:调用虚方法。

    d. invokeinterface:调用接口方法,会在运行时再确定一个实现此接口的对象。

    只要能被invokestatic与invokespecial指令调用的方法,都可以在解析阶段确定唯一的调用版本,符合这个条件的有静态方法,私有方法,实例构造器和超类方法四类,它们在类加载的时候就会把符号引用解析为该方法的直接引用。这些方法可以统称为非虚方法,与之相反,其它方法就称为虚方法(除去final方法)。

    Java中的非虚方法除了使用invokestatic与invokespecial指令调用的方法之后还有一种,就是被final修饰的方法。虽然final方法是使用invokevirtual指令来调用的,但是由于它无法被覆盖,没有其它版本,所以也无须对方法接收进行多态选择,又或者说多态选择的结果是唯一的。在Java语言规范中明确说明了final方法是一种非虚方法。

    invokespecial的例外情况:调用超类方法时,class中保存的时编译时的超类方法类型,运行时,如果超类中的继承结构变化,将会动态使用相应的方法版本。

    调用接口引用方法可能要比调用类引用方法慢。因为,当java虚拟机遇到invokevirtual指令时,它把实例方法的符号引用解析为直接引用,所生成的直接引用很可能是方法表中的一个偏移量,而且从此往后都可以使用同样的偏移量。但对于invokeinterface指令,虚拟机每一次遇到invokeinterface指令,都不得不重新搜索一遍方法表,因为虚拟机不能够假设这一次的偏移量与上一次的偏移量相同。

    最快的指令是invokespecial和invokestatic,当java虚拟机为这些指令解析符号引用时,将符合引用转换为直接引用,所生成的直接引用将包含一个指向实际操作码的指针。

 

从方法中返回:

    每一种操作码对应一种返回的数据类型,它们都没有操作数,如果有返回值,必须被放置在操作数栈中。返回值从操作数栈中弹出,然后被压入调用方法(调用代码所在的方法)的栈帧的操作数栈中。弹出当前栈帧,调用方法的栈帧成为当前栈帧;程序计数器被重置,指向紧随调用返回方法那条指令的下一条指令。

 

invokespecial和invokevirtual的主要区别:

    invokespecail通常根据引用的类型来选择方法,而不是根据对象的类型来选择。它使用静态(编译时)绑定而不是动态(运行时)绑定。

    当根据引用的类型来调用实例方法,而不是根据对象的类来调用的时候,通常使用invokespecial指令,分为3种情况:

    (1) 实例初始化方法(<init>())。

    (2) 私有方法。

    (3) 使用super关键字所调用的方法。

 

 

线程同步之监视器:

    java所使用的同步机制是监视器,java中的监视器支持两种线程:互斥和协作。java虚拟机通过对象锁来实现互斥,允许多个线程在同一个共享数据上独立而互不干扰地工作。协作则是通过Object类的wait、notify和notifyAll方法来实现,允许多个线程为了同一个目标而共同工作。

    除了与数据关联之外,监视器还会关联到一些或更多的代码,这样的代码被称作监视区域。对于一个监视器来说,监视区域是最小的、不可分割的代码块。换句话说,在同一个监视器中,监视区域只会同时被一个线程执行,即使同时有多个并发的线程,监视器会保证在监视区域上同一时间只会执行一个线程。一个线程想要进入监视器的唯一途径就是到达监视器所关联的一个监视区域的开始处,而线程想要继续执行监视区域的唯一途径就是获得该监视器。

    当一个线程到达一个监视区域的开始出,它就会被放置到该监视器的入口区。如果没有其他线程在入口区总等待,也没有线程正持有监视器,则这个线程就可以获得监视,并继续执行监视区域中的代码;当这个线程执行完监视区域后,它就会退出并释放该监视器。如果已经有线程持有该监视器,则这个刚刚到达的线程必须在入口区等待,当监视器的持有者退出监视器后,新到达的线程必须与其他已经在入口区等待的线程进行一次比赛,最终只会有一个线程赢得比赛并获得监视器。

    互斥帮助线程在访问共享数据时不被其他线程干扰,而协作帮助线程与其他线程共同工作。java虚拟机所使用的这种监视器被称作“等待并唤醒”监视器(也被称作“发信号并继续”)。在这种监视器中,一个已经持有监视器的线程,可以通过一个等待命令,暂停自身的执行;当线程执行了等待命令后,它会释放监视器,并进入一个等待区,这个线程会在等待区一直持续暂停状态,直到这个线程中的其他线程执行了唤醒命令。当

一个线程执行了唤醒命令后,它会继续持有监视器,直到它主动释放监视器(执行一个等待命令或者执行完监视区域的代码)。当执行唤醒的线程释放了监视器后,等待线程才会苏醒,并重新获得监视器。

    唤醒线程在它将监视器保护数据值为等待线程想要的状态后执行唤醒命令,但是因为唤醒线程会继续执行,它可能会在执行唤醒后又修改了数据的状态,让等待线程不能继续工作。另一种情况是,第三个线程可能在唤醒线程释放了监视器,而等待线程还没有获得监视器之前抢先获得监视器,而这个线程可能会修改监视器保护的数据的状态。因为以上事实,一次唤醒往往被等待线程看作是一次提醒,告诉它“数据已经是你想要的状态了”。每次等待线程苏醒并获得监视器的时候,它都需要再次检查数据的状态,以确定是否可以继续完成工作;如果数据不是他所需要的状态,这个线程可能会再次执行等待命令或者放弃等待退出监视器。

    活动线程会通过两条途径释放监视器:执行一个等待命令,或者完成它正在执行的监视区域。

    如果一个监视器的持有者在它释放监视器前没有执行唤醒命令(同时在此之前也没有任何等待线程被唤醒并等待苏醒),那么位于入口区的线程将竞争获得监视器。如果当前持有者执行了唤醒命令,那么入口区中的线程就不得不与一个或多个等待区中的线程竞争。一个线程只有在它正持有监视器时才能执行等待命令,而且它只能通过再次成为监视器的持有者才能离开等待区。

    在java虚拟机中,线程在执行等待命令时可以随意指定一个暂停时间,如果在暂停时间截止之前没有其他线程执行唤醒命令,那么这个等待线程会从虚拟机中得到一个自动唤醒的命令,也就是说,在暂停时间到了之后,即使没有来自其他线程的明确的唤醒命令,它也会自动苏醒。

    java虚拟机提供了两种唤醒命令:notify和notifyAll。notify命令随机从等待区中选择一个线程并将其标志为可能苏醒,而notifyAll命令会将等待区中的所有线程都标志为可能苏醒。

    java虚拟机如何从等待区以及入口区选择下一个线程来执行,在很大程度上取决于java虚拟机的设计者。程序员必须不依赖任何特定的有关优先级的算法或安排。只有当绝对确认只会有一个线程在等待区中挂起的时候,才应该使用notify;只要存在同时有多个线程在等待区中被挂起的可能性,就应该使用notifyAll。

 

线程同步之对象锁:

    java虚拟机的一些运行时数据区会被所有的线程共享,其他的数据是各个线程私有的。因为堆和方法区是被所有线程共享的,java程序需要为两种多线程访问的数据进行协调:保存在堆中的实例变量、保存在方法区中的类变量。程序不需要协调保存在java栈中的局部变量,java栈中的数据是属于线程私有的。

    在java虚拟机中,每个对象和类在逻辑上都是和一个监视器相关联的。对于对象,监视器保护对象的实例变量;对于类,监视器保护类的类变量。如果一个对象没有实例变量,或者一个类没有类变量,相关联的监视器就什么都不监视。

    为了实现监视器的排他性监视能力,java虚拟机为每一个对象和类都关联一个锁(互斥体mutex)。线程访问实例变量或者类变量不需要获取锁;但是如果线程获取了对象或类的锁,那么在它释放这个锁之前,就没有其他线程可以获取同一个对象或类的锁(锁住一个对象就是获取相关联的监视器)。

    类的锁实际上用对象锁实现,当java虚拟机装载一个class文件的时候,它会创建一个java.lang.Class类的实例来代表该类型;当锁住一个类的时候,实际上锁住的是那个类的Class对象。

    一个线程可以允许多次对同一个对象上锁。对于每一个对象来说,java虚拟机维护一个计数器,记录对象被加了多少次锁。没有被锁的对象的计数器是0,线程每加锁一次,计数器加1(只有已经拥有这个对象的锁的线程才能对该对象再次加锁,在它释放锁之前,其他线程不能对这个对象加锁);线程每释放一次锁,计数器就减1。当计数器为0的时候,锁就被完全释放了,其他的线程才可以使用它。

    java虚拟机中的一个线程在它到达监视区域开始处的时候请求一个锁。java中有两种监视区域:同步块和同步方法。当线程到达监视区域的第一条指令的时候,线程必须对该引用对象加锁,否则线程不允许执行其中的代码。一旦它获得了锁,线程就进入被保护的代码;当线程离开这块代码时,不管它是如何离开的,它都会释放相关对象上的锁。

    java程序员不需要自己动手加锁,对象锁是在java虚拟机内部使用的。在java程序中,只需要编写同步块或者同步方法就可以标志一个监视区域。当java虚拟机运行程序的时候,每一次进入一个监视区域的时候,它都会自动锁上对象或者类的Class对象。

 

    同步语句块:方法内的同步语句块会使用monitorenter和monitorexit操作码。当虚拟机遇到monitorenter的时候,它获得栈中objectref所引用对象的锁,如果线程已经拥有那个对象的锁,锁的计数器加1。线程中的每条monitorexit都会引起计数器减1。class文件的字节码中总会使用catch子句来确保被加锁的对象将被释放,即使从同步语句块中抛出异常。不管被同步的语句块是如何退出的,线程进入这个块时获得的锁总是一定会被释放的。

    同步方法:java虚拟机调用同步方法或者从同步方法中返回没有使用任何特别的操作码。当虚拟机解析对方法的符号引用时,它判断这个方法是否是同步的;如果是同步的,虚拟机就在调用方法之前获取一个锁。当同步方法执行完毕的时候,不管是正常结束还是抛出异常,虚拟机都会释放这个锁。

    同步方法的类在编译为字节码时,没有使用进入和离开监视的指令,也没有为方法创建同步块的异常表,所以更加高效。

 

 

 

猜你喜欢

转载自jaesonchen.iteye.com/blog/2329259