`
水上风荷
  • 浏览: 8863 次
  • 性别: Icon_minigender_1
  • 来自: 上海
最近访客 更多访客>>
社区版块
存档分类

《深入Java虚拟机》学习笔记五:类型生命周期

    博客分类:
  • Java
阅读更多

第七章 类型的生命周期
1、 类型装载、链接与初始化
      Java虚拟机通过装载、链接和初始化一个Java类型,使该类型可以被正在运行的Java程序所使用。其中,装载就是把二进制的Java类型读入Java虚拟机中,而连接就是把这种已经读入虚拟机的二进制形式的类型数据合并到虚拟机的运行状态中去。连接阶段分了三个步骤:验证、准备和解析验证确保了Java类型数据格式的正确并且适于Java虚拟机使用,而准备步骤则负责为该类型分配它所需的内存,比如为它的类变量分配内存,“解析”步骤则负责把常量池中的符号引用转换为直接引用(也可以在运行中的程序真正使用某个符号引用时再去解析它)。初始化将给类变量赋予适当的初始值。装载、连接和初始化这三个阶段必须按顺序进行。
虚拟机严格定义了初始化的时机:
     ① 当创建某个类的新实例时(或者通过在字节码中执行new指令,或者通过不明确的创建、反射、克隆或者反序列化)。
     ② 当调用某个类的静态方法时(即在字节码中执行invokestatie指令时)。
     ③ 当使用某个类或接口的静态字段,或者对该字段赋值时(在字节码中,执行getstatic或者putstatic指令时),用final修饰的静态字段除外,它被初始化为一个编译时的常量表达式。
     ④ 当调用JavaAPI中某些反射方法时,比如类Class中的方法或者java.reflect包中类的方法。
     ⑤ 当初始化某个类的子类时(某个类初始化时,要求它的超类已经被初始化了)。
     ⑥ 当虚拟机启动时某个标明为启动类的类(即含main()方法的那个类。)
任何一个类的初始化都要求它的所有祖先类(而不是祖先接口)预先被初始化。而一个接口的初始化,并不要求它的祖先接口预先被初始化。

    1) 装载阶段由三个基本动作组成:
    ① 通过该类型的完全限定名,产生一个代表该类型的二进制数据流。
    ② 解析这个二进制数据流为方法区内的内部数据结构。
    ③ 创建一个表示该类型的java.lang.Class类的实例。

     如果一个类装载器在预先装载时遇到缺失或者错误的class文件,它必须等到程序首次主动使用该类时才报告错误,如果一个类一直没有被程序主动使用,那么该类装载器就不会报告错误。
      2) 验证,类型被转载后,就准备进行连接了,连接过程的第一步是验证,确认类型符合Java语言的语义,并且他不会危及虚拟机的完整性。
      3) 准备阶段,Java虚拟机为类变量分配内存,设置默认初始值,虚拟机把给类变量新分配的内存根据类型设置为默认值。准备阶段,Java虚拟机实现可能也为一些数据结构分配内存,目的是提高运行程序的性能。
      4) 解析过程就是在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用换成直接引用的过程。
      5) 初始化就是为类变量赋予正确的初始值,“正确”初始值指的是程序员希望这个类变量所具备的起始值,正确的初始值是和在准备阶段赋予的默认初始值对比而言的。
      初始化一个类包含两个步骤:
      ① 如果类存在直接超类的话,且直接超类还没有被初始化,就先初始化直接超类。
      ② 如果类存在一个类初始化方法,就执行此方法。
      当初始化一个超类时,也需要包含这两个步骤,因此第一个被初始化的类用远视Object,然后是被主动使用的类的继承树上所有的类,超类总是在子类之前被初始化。初始化接口并不需要初始化它的父接口,因此初始化一个接口只需要一步,如果接口初始化方法的话,就执行此方法。
     <clinit>()方法的代码并不显式地调用超类的<clinit>()方法,在Java虚拟机调用类的<clinit>()方法之前,它必须确认超类<clinit>()方法已经被执行了。
      Java虚拟机必须确保初始化过程被正确地同步,如果多个线程需要初始化一个类,仅仅允许一个线程初始化,其他线程需要等待,当活动的线程完成了初始化过程后,他必须通知其他等待的线程。
      Java编译器把类变量初始化语句和静态初始化语句的代码都放到class文件的<clinit> ()方法中,顺序就按照它们在类或者接口声明中出现的顺序。并非所有的类都需要在它们的class文件中拥有一个<clinit>()方法。如果类声明了类变量,但是没有明确使用类变量初始化语句或者静态初始化语句初始化它们,那么类不会有<clinit> ()方法。如果累仅仅包含静态final变量的类变量初始化语句,而且这些类变量初始化语句采用编译时常量表达式,类也不会有<clinit>()方法,只有那些的确需要执行Java代码来赋予类变量正确初始值的类才会有类初始化方法。
主动使用和被动使用:

2、 对象声明周期
      一旦一个类被装载、连接和初始化,它就随时可以使用了。程序可以访问它的静态字段,调用它的静态方法,或者创建它的实例。
     1) 类实例化:在Java程序中,类可以被明确或者隐含地实例化。实例化一个类有四种途径:明确地使用new操作符;调用Class或者java.lang.reflect.Constuctor对象的newInstance()方法,调用任何现有对象的clone();或者通用java.io.ObjectInputStream类的getObject()方法反序列化。
     当Java虚拟机创建一个类的新实例时,不管是明确的还是隐含的,首先都需要在堆中为保存对象的实例变量分配内存,所有在对象的类中它的超类中声明的变量都要分配内存。
    2) 垃圾收集和对象的终结
程序可以明确或者隐含地位对象分配内存,但是不能明确地释放内存。如果类声明了一个名为finalize()的返回void的方法,垃圾收集器会在释放这个实例所占据的内存空间之前执行这个方法(被称为终结方法)一次。垃圾收集器最多只会调用一个对象的终结方法一次(在对象变成不再被引用的之后的某个时候,在占据的对象被重用之前)。

3、 卸载类型
      在很多方面,Java虚拟机中类的生命周期和对象的生命周期很相似。类的垃圾收集和卸载之所以在java虚拟机中很重要,因为java程序可以在运行时通过用户自定义的类装载器装载类型来动态的扩展程序。所有被装载的类型都在方法区占据内存空间,如果Java程序持续通过用户自定义的类装载器装载类型,方法区的内存足迹就会不断增长,如果某些动态装载的类型只是临时需要,当它们不再被引用之后,占据的内存空间可以通过卸载类型而释放。
      Java虚拟机通过何种方法来确定一个动态装载类型是否仍然需要被程序需要呢?其判断方式与判断对象是否仍然被程序需要的方式很类似。如果程序不再引用某类型,那么这个类型就无法再对未来的计算过程产生影响。类型变成不可触及的,而且可以被垃圾收集。
      使用启动类装载器装载的类型永远是可触及的,所以永远不会被卸载,只有使用用户自定义类装载器装载的类型才会变成不可触及的。
      判断动态装载的类型Class实例在正常的垃圾收集过程中是否可以触及有两种方式,第一,也是最明显的,如果程序保持对Class实例的明确引用,它就是可触及的。其次,如果在堆中还存在一个触及的对象,在方法区中它的类型数据指向一个Class实例,那么这个Class实例就是可触及的。

 

分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics