深入理解Java虚拟机读书笔记之:第7章 类型的生命周期

类型装载、连接与初始化
    Java虚拟机通过装载、连接和初始化一个Java类型,使该类型可以被正在运行的Java程序所使用。其中,装载就是把二进制形式的Java类型读入Java虚拟机中;而连接就是把这种已经读入虚拟机的二进制形式的类型数据合并到虚拟机的运行时状态中去。连接阶段分为三个子步骤——验证、准备和解析。“验证”步骤确保了Java类型数据格式正确并且适于Java虚拟机使用。而“准备”步骤则负责为该类型分配它所需的内存,比如为它的类变量分配内存。“解析”步骤则负责把常量池中的符号引用转换为直接引用。虚拟机的实现可以推迟解析这一步,它可以在当运行中的程序真正使用某个符号引用时再去解析它(把该符号引用转换为直接引用)。当验证、准备和(可选的)解析步骤都完成了时,该类型就已经为初始化做好了准备。在初始化期间,都将给类变量赋予适当的初始值。整个过程下如图所示:


 
    Java虚拟机严格地定义了初始化的时机。所有的Java虚拟机实现必须在每个类或接口首次主动使用时初始化。下面这六种情形符合主动使用的要求。
  • 当创建某个类的新实例时(或者通过在字节码中执行new指令;或者通过不明确的创建、反射、克隆或者反序列化)。
  • 当调用某个类的静态方法时(即在字节码中执行invokestatic指令时)。
  • 当使用某个类或接口的静态字段,或者对该字段赋值时(即在字节码中,执行getstatic或putstatic指令时),用final修饰的静态字段除外,它被初始化为一个编译时的常量表达式。
  • 当调用Java API中的某些反射方法时,比如类Class中的方法或者java.lang.reflect包中的类的方法。
  • 当初始化某个类的子类时(某个类初始化时,要求它的超类已经被初始化了)。
  • 当虚拟机启动时某个被标明为启动类的类(即含有main()方法的那个类)。
    除上述这六种情形外,所有其他使用Java类型的方式都是被动使用,它们都不会导致Java类型的初始化。
    任何一个类的初始化都要求它的所有祖先类(而不是祖先接口)预先被初始化。而一个接口的初始化,并不要求它的祖先接口预先被初始化。只有在某个接口所声明的非常量字段被使用时,该接口才会被初始化。
 
装载
    装载阶段由三个基本动作组成,要装载一个类型,Java虚拟机必须:
  • 通过该类型的完全限定名,产生一个代表该类的二进制数据流。
  • 解析这个二进制数据流为方法区内的内部数据结构。
  • 创建一个表示该类型的java.lang.Class类的实例。
    装载步骤的最终产品就是这个Class类的实例对象,它成为Java程序与内部数据结构之间的接口。
 
验证
    确认类型符合Java语言的语义,并且它不会危及虚拟机的完整性。
    Java虚拟机规范列出了虚拟机可以抛出的异常以及在何种条件下必须抛出它们。不管Java虚拟机可能遇到了什么样的麻烦,都应该有一个异常或者错误可以抛出。
    
    在装载过程中,虚拟机必须解析代表类型的二进制数据流,在这个解析期间,虚拟机大多会检查二进制数据以确保数据全部是预期的格式。可能检查魔数,确保每一个部分都在正确的位置,拥有正确的长度,验证文件不是太长或者太短,等等。虽然这些检查在装载期间完成,但它们在逻辑上仍然属于验证阶段。检查被装载的类型是否有任何问题的整个过程都属于验证。
 
    在正式的验证阶段需要完成的候选检查在下面列出。首先列出确保各个类之间二进制兼容的检查:
  • 检查final的类不能拥有子类。
  • 检查final的方法不能被覆盖。
  • 确保在类型和超类型之间没有不兼容的方法声明(比如两个方法拥有同样的名字,参数在数量、顺序、类型上都相同,但是返回类型不同)。
    在验证期间,这个类和它所有的超类型都需要确保互相之间仍然二进制兼容。
  • 检查所有的常量池入口相互之间一致(比如,一个CONSTANT_String_info入口的string_index项目必须是一个CONSTANT_Utf8_info入口的索引)。
  • 检查常量池中的所有的特殊字符串(类名、字段名和方法名、字段描述符和方法描述符)是否符合格式。
  • 检查字节码的完整性。
 
准备
    在准备阶段,Java虚拟机为类变量分配内存,设置默认初始值。但在到达初始化阶段之前,类变量都没有被初始化为真正的初始值。(在准备阶段是不会执行Java代码的)。

主要类型和引用类型的默认初始值

类 型

默认初始值

int

0

long

0L

short

(short)0

char

'\u0000'

byte

(byte)0

boolean

false

reference

null

float

0.0f

double

0.0d

 
解析
    解析过程就是在类型的常量池中寻找类、接口、字段和方法的符号引用,把这些符号引用替换成直接引用的过程。
 
初始化
    为了准备让一个类或者接口被首次主动使用,最后一个步骤就是初始化,也就是为类变量赋予正确的初始值。这里的“正确”初始值指的是程序员希望这个类变量所具备的起始值。
 
    所有的类变量初始化语句和类型的静态初始化语句都被Java编译器收集在一起,放到一个特殊的方法中。对于类来说,这个方法被称作类初始化方法;对于接口来说,它被称为接口初始化方法。在类和接口的Java class文件中,这个方法被称为“<clinit>”。这种方法只能被Java虚拟机调用。
 
    第一个被初始化的类永远是Object。超类总是在子类之前被初始化。
 
1.<clinit>()方法
    思考下面的类的例子:
class Example1c {
    static int width;
    static int height = (int) (Math.random() * 2.0);
    // This is the static initializer
    static {
        width = 3 * (int) (Math.random() * 5.0);
    }
}
 
    Java编译器生成了下面的<clinit>()方法(命令:javap -c Example1c):
   0:   invokestatic    #6; //Method java/lang/Math.random:()D,调用Math.random()并将结果入栈
   3:   ldc2_w  #8;         //double 2.0d,将double常量2.0入栈
   6:   dmul                //将前两步的两个值出栈,相乘后将结果入栈
   7:   d2i                 //将上一步的结果出栈,并强制转换为int类型,再入栈
   8:   putstatic       #5; //Field height:I,将上一步的结果出栈,存储到类变量height中
   11:  iconst_3            //将int常量3入栈
   12:  invokestatic    #6; //Method java/lang/Math.random:()D,调用Math.random()并将结果入栈
   15:  ldc2_w  #10;        //double 5.0d,将double常量5.0入栈
   18:  dmul                //将前两步的两个值出栈,相乘后将结果入栈
   19:  d2i                 //将上一步的结果出栈,并强制转换为int类型,再入栈
   20:  imul                //将偏移量11入栈的值和上一步的值出栈,相乘后将结果入栈
   21:  putstatic       #7; //Field width:I,将上一步的结果出栈,存储到类变量width中
   24:  return              //<clinit>方法返回void
 
    其中,偏移量0~8是对类变量height的初始化;11~24是执行静态代码块的初始化。
 
    并非所有的类都需要在它们的class文件中拥有一个<clinit>()方法。以下情况不会有<clinit>()方法:
  • 如果类没有声明任何类变量,也没有静态初始化语句
  • 如果类声明了类变量,但是没有明确使用类变量初始化语句或者静态初始化语句初始化它们
  • 如果类仅包含静态final变量的类变量初始化语句,而且这些类变量初始化语句采用编译时常量表达式
    下面是一个不会产生<clinit>()方法的例子:
class Example1d {
    static final int angle = 35;
    static final int length = angle * 2;
}
 
    angle和length字段并非类变量,它们是常量,被Java编译器特殊处理了。Java虚拟机在使用它们的任何类的常量池或者字节码流中直接存放的是它们表示的常量的int值。
 
    下面是一个同时使用一个常量和一个其他类的类变量的例子:
class Example1e {
    // The class variable initializer for symbolicRef uses a symbolic
    // reference to the size class variable of class Example1a
    static int symbolicRef = Example1a.size;
    // The class variable initializer for localConst doesn't use a
    // symbolic reference to the length field of class Example1d.
    // Instead, it just uses a copy of the constant value 70.
    static int localConst = Example1d.length * (int) (Math.random()
        * 3.0);
}
 
class Example1a {
    // "= 3 * (int) (Math.random() * 5.0)" is the class variable
    // initializer
    static int size = 3 * (int) (Math.random() * 5.0);
}
 
    Java编译器为类Example1e生成了下面的<clinit>()方法(命令:javap -c Example1e):
   0:   getstatic       #9;  //Field Example1a.size:I,将Example1a.size的值入栈,使用一个符号引用指向类Example1a的size字段
   3:   putstatic       #10; //Field symbolicRef:I,将上一步的结果出栈,存储到类变量symbolicRef中
   6:   bipush  70           //将常量Example1d.length的拷贝入栈
   8:   invokestatic    #8;  //Method java/lang/Math.random:()D,调用Math.random()并将结果入栈
   11:  ldc2_w  #11;         //double 3.0d,将double常量3.0入栈
   14:  dmul                 //将前两步的两个值出栈,相乘后将结果入栈
   15:  d2i                  //将上一步的结果出栈,并强制转换为int类型,再入栈
   16:  imul                 //将偏移量6入栈的值和上一步的值出栈,相乘后将结果入栈
   17:  putstatic       #7;  //Field localConst:I,将上一步的结果出栈,存储到类变量localConst中
   20:  return               //<clinit>方法返回void
 
    其中,偏移量0~3是对类变量symbolicRef的初始化;6~20是对类变量localConst的初始化。
 
    所有在接口中声明的隐式公开(public)、静态(static)和最终(final)字段都必须在字段初始化语句中初始化。如果接口包含任何不能在编译时被解析成为一个常量的字段初始化语句,接口就会拥有一个<clinit>()方法。下面是一个例子:
interface Example1f {
    int ketchup = 5;
    int mustard = (int) (Math.random() * 5.0);
}
 
    Java编译器为接口Example1f生成了下面的<clinit>()方法(命令:javap -c Example1f):
   0:   invokestatic    #6; //Method java/lang/Math.random:()D,调用Math.random()并将结果入栈
   3:   ldc2_w  #7;         //double 5.0d,将double常量5.0入栈
   6:   dmul                //将前两步的两个值出栈,相乘后将结果入栈
   7:   d2i                 //将上一步的结果出栈,并强制转换为int类型,再入栈
   8:   putstatic       #5; //Field mustard:I,将上一步的结果出栈,存储到类变量mustard中
   11:  return              //<clinit>方法返回void
 
2.主动使用和被动使用
    使用一个非常量的静态字段只有当类或者接口的确声明了这个字段时才是主动使用。比如,类中声明的字段可能会被子类引用;接口中声明的字段可能会被子接口或者实现了这个接口的类引用 。对于子类、子接口和实现了接口的类来说,这就是被动使用——使用它们并不会触发它们的初始化。只有当字段的确是被类或者接口声明的时候才是主动使用。下面的例子说明了这个原理:
class NewParent {
    static int hoursOfSleep = (int) (Math.random() * 3.0);
    static {
        System.out.println("NewParent was initialized.");
    }
}

class NewbornBaby extends NewParent {
    static int hoursOfCrying = 6 + (int) (Math.random() * 2.0);
    static {
        System.out.println("NewbornBaby was initialized.");
    }
}

class Example2 {
    // Invoking main() is an active use of Example2
    public static void main(String[] args) {
        // Using hoursOfSleep is an active use of NewParent,
        // but a passive use of NewbornBaby
        int hours = NewbornBaby.hoursOfSleep;
        System.out.println(hours);
    }
    static {
        System.out.println("Example2 was initialized.");
    }
}

 

    在上面的例子中,执行Example2的main()方法只会Example2和NewParent被初始化。NewbornBaby没有被初始化,也不需要被装载。执行结果:
Example2 was initialized.
NewParent was initialized.
0

 

    如果一个字段既是静态(static)的又是最终(final)的,并且使用一个编译时常量表达式初始化,使用这样的字段,就不是对声明该字段的类的主动调用。下面是一个说明这种对静态final字段特殊处理的例子:
interface Angry {
    String greeting = "Grrrr!";
    int angerLevel = Dog.getAngerLevel();
}

class Dog {
    static final String greeting = "Woof, woof, world!";
    static {
        System.out.println("Dog was initialized.");
    }
    static int getAngerLevel() {
        System.out.println("Angry was initialized");
        return 1;
    }
}

class Example3 {
    // Invoking main() is an active use of Example3
    public static void main(String[] args) {
        // Using Angry.greeting is a passive use of Angry
        System.out.println(Angry.greeting);
    
        // Using Dog.greeting is a passive use of Dog
        System.out.println(Dog.greeting);
    }
    static {
        System.out.println("Example3 was initialized.");
    }
}

 

 

    运行Example3程序,执行结果:
Example3 was initialized.
Grrrr!
Woof, woof, world!
 
 
对象的生命周期
 
类实例化
    在Java程序中,类可以被明确或者隐含地实例化。实例化一个类有四种途径:明确地使用new操作符;调用Class或者java.lang.reflect.Constructor对象的newInstance()方法;调用任何现有对象的clone()方法;或者通过java.io.ObjectInputStream类的getObject()方法反序列化。
    下面的例子中演示了其中三种创建新的类实例的方法:
class Example4 implements Cloneable {
    Example4() {
        System.out.println("Created by invoking newInstance()");
    }
    Example4(String msg) {
        System.out.println(msg);
    }
    public static void main(String[] args)
        throws ClassNotFoundException, InstantiationException,
        IllegalAccessException, CloneNotSupportedException {
        // Create a new Example4 object with the new operator
        Example4 obj1 = new Example4("Created with new.");
        // Get a reference to the Class instance for Example4, then
        // invoke newInstance() on it to create a new Example4 object
        Class myClass = Class.forName("Example4"); 
        Example4 obj2 = (Example4) myClass.newInstance();
        // Make an identical copy of the the second Example4 object
        Example4 obj3 = (Example4) obj2.clone();
    }
}
 
    执行结果:
Created with new.
Created by invoking newInstance()
 
    还有几种情况下对象会被隐含地实例化:
  • 在任何Java程序中第一个隐含实例化对象可能就是保存命令行参数的String对象(main(String[] args))。每一个命令行参数都会有一个String对象的引用
  • 对于Java虚拟机装载的每一个类型,它会暗中实例化一个Class对象来代表这个类型
  • 当Java虚拟机装载了在常量池中包含CONSTANT_String_info入口的类的时候,它会创建新的String对象的实例来表示这些常量字符串
  • 执行包含字符串连接操作符的表达式产生对象
 
垃圾收集和对象的终结
    垃圾收集器(最多)只会调用一个对象的终结方法一次——在对象变成不再被引用的之后的某个时候,在占据的对象被重用之前。如果终结方法代码执行后,对象重新被引用了(复活了),随后再次变得不被引用,垃圾收集器不会第二次调用终结方法。
 
卸载类型
    虚拟机装载、连接并初始化类,使程序能使用类,当程序不再引用它们的时候可选地卸载它们。
 
(注:本文中<clinit>()方法的内容是在JDK1.6环境下使用javap命令生成的,与原书提供的内容有细微的差别。另外,原书对<clinit>()方法的各行使用英文注释, 我使用中文 注释)
 
    在此说明,本系列文章的内容均出自《深入理解Java虚拟机》一书,除了极少数的“注”或对内容的裁剪整理外,内容原则上与原书保持一致。由于这是一本原理性的书籍,本人不想因为自己能力与理解的问题对大家造成误解,所以除了对原书内容的裁剪整理之外,基本不做任何内容的延伸思考与扩展。
    另外,如果您对本系列文章的内容感兴趣,建议您去阅读原版书籍,谢谢!
 
(转载请注明来源:http://zhanjia.iteye.com/blog/1877236)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值