您的位置:首页 > 编程语言 > Java开发

Java类的生命周期

2015-07-18 12:06 411 查看
       编写一个java的源文件后,经过编译会生成一个后缀名为class的字节码文件,只有这种字节码文件才能够在Java虚拟机中运行,Java类的生命周期就是指一个class文件从加载到卸载的全过程。
       一个Java类的完整的生命周期会经历加载链接初始化使用卸载五个阶段,当然也有在加载或者链接之后没有被初始化就直接被使用的情况。

加载

       类的加载是指把类的.class文件中的数据读入到内存中,通常是创建一个字节数组读入.class文件, 然后产生与所加载类对应的Class对象。
       在加载阶段,Java虚拟机通过类的全限定名和ClassLoader类,找到需要加载的类并把类的信息加载到JVM的方法区中,然后在堆区中实例化一个java.lang.Class对象,作为方法区中这个类的信息入口。当类被加载以后,在JVM内部就以“类的全限定名+ClassLoader实例ID”来标明类。
       对于加载的时机,各个虚拟机的做法并不一样,但是有一个原则,就是当JVM “预期”到一个类将要被使用时,就会在使用它之前对这个类进行加载。比如说,在一段代码中出现了一个类的名字,JVM在执行这段代码之前并不能确定这个类是否会被使用到,于是,有些JVM会在执行前就加载这个类,而有些则在真正需要用的时候才会去加载它,这取决于具体的JVM实现。常用的hotspot虚拟机是采用的后者,就是说当真正用到一个类的时候才对它进行加载。
       加载阶段是类的生命周期中的第一个阶段,加载阶段之后,是链接阶段。有一点需要注意,就是有时链接阶段并不会等加载阶段完全完成之后才开始,而是交叉进行,可能一个类只加载了一部分之后,链接阶段就已经开始了。但是这两个阶段总的开始时间和完成时间总是固定的:加载阶段总是在链接阶段之前开始,链接阶段总是在加载阶段完成之后完成。

链接

       链接阶段比较复杂,一般会跟加载阶段和初始化阶段交叉进行,这个阶段的主要任务就是做一些加载后的验证工作以及一些初始化前的准备工作,将二进制的类型信息合并到JVM运行时状态中去。可以细分为三个步骤:验证、准备和解析。
       1. 验证:当一个类被加载之后,必须要验证一下这个类是否合法,比如文件格式的验证、变量与方法是不是有重复、数据类型是不是有效、继承与实现是否合乎标准等等。总之,这个阶段的目的就是保证加载的类是能够被JVM所运行。
       2. 准备:准备阶段的工作就是为类的静态变量分配内存并设置初始值(方法区中)。对于非静态的变量,则不会为它们分配内存。需要注意,这时候静态变量的初值为JVM默认的初值,而不是在程序中设定的初值。JVM默认的初值是这样的: 
基本类型(int、long、short、char、byte、boolean、float、double)的默认值为0。比如static int a = 1,则准备阶段中a的初值是0,在初始化的<clinit>中才会被设置为1。
引用类型的默认值为null。
常量的默认值为程序中设定的值,比如定义final static int a = 100,则准备阶段中a的初值就是100。
       3. 解析:这一阶段的任务就是把常量池中的符号引用转换为直接引用。那么什么是符号引用,什么又是直接引用呢?举个例子:比如要在内存中找一个类里面的一个叫做show的方法,显然是找不到。但是在解析阶段,JVM就会把show这个名字转换为指向方法区的一块内存地址,比如c17164,通过c17164就可以找到show这个方法具体分配在内存的哪一个区域了。这里show就是符号引用,而c17164就是直接引用。在解析阶段,JVM会将所有的类或接口名、字段名、方法名转换为具体的内存地址。
       链接阶段完成之后会根据使用的情况(直接引用还是被动引用)来选择是否对类进行初始化。

初始化

       初始化阶段才真正开始执行类中定义的Java程序代码,初始化阶段是执行类构造器<clinit>()方法的过程。
       <clinit>()方法是由编译器自动收集类中的所有静态类变量的复制动作和静态语句块中的语句合并而成,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在它之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。
       <clinit>()方法与类的构造函数<init>()不同,不需要显式的调用父类构造器,虚拟机会保证在子类的<clinit>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此,虚拟机执行的第一个<clinit>()方法的类肯定是java.lang.Object.
       由于父类<clinit>()方法先执行,也就意味着父类中定义的静态语句要优先于子类的变量赋值操作。
       <clinit>()方法对于类或接口来说并不是必须的,如果一个类没有静态语句块,也没有对变量赋值操作,那么编译器可以不为这个类生成<clinit>()方法。
       接口中不能使用静态语句块,但仍有变量初始化赋值的操作,因此也会生成<clinit>()方法,但与类不同的是,接口的<clinit>()方法不需要执行父接口的<clinit>()方法。只有当父接口中定义的变量被使用时,父接口才初始化,另外,接口的实现类在初始化时一样不会执行接口的<clinit>()方法。
       虚拟机会保证一个类的<clinit>()方法在多线程环境中正确的加锁同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都会阻塞,直到该方法执行完,如果在一个类的<clinit>()方法中有耗时很长的操作,可能会造成多个进程阻塞,在实际应用中,这种阻塞往往很隐蔽。

       一个类被直接引用,就会触发类的初始化。在Java中,直接引用的情况有:
       • 通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法。
       • 通过反射方式执行以上三种行为。
       • 初始化子类的时候,会触发父类的初始化。
       • 作为程序入口直接运行时(也就是直接调用main方法)。
       除了以上四种情况,其他使用类的方式叫做被动引用,而被动引用不会触发类的初始化。请看主动引用的示例代码:
class InitClass{
static {
System.out.println("初始化InitClass");
}
public static String a = null;
public static void method(){}
}

class SubInitClass extends InitClass{}

public class Test1 {

/**
* 主动引用引起类的初始化的第四种情况就是运行Test1的main方法时
* 导致Test1初始化,这一点好理解
*/
public static void main(String[] args) throws Exception{
//  主动引用引起类的初始化一: new对象、读取或设置类的静态变量、调用类的静态方法。
//	new InitClass();
//	InitClass.a = "";
//	String a = InitClass.a;
//	InitClass.method();

//  主动引用引起类的初始化二:通过反射实例化对象、读取或设置类的静态变量、调用类的静态方法。
//	Class cls = InitClass.class; //不会调用static块
//	cls.newInstance(); //会调用

//	Field f = cls.getDeclaredField("a"); //不会调用static块
//	f.get(null);
//	f.set(null, "s");

//	Method md = cls.getDeclaredMethod("method");  //不会调用static块
//	md.invoke(null, null);

//Class.forName("xxx.xxx.xxx.InitClass"); //会调用static块,这就是为什么使用JDBC时要Class.forName("com.mysql.jdbc.Driver");

//  主动引用引起类的初始化三:实例化子类,引起父类初始化。
//	new SubInitClass();

}
}
       类的初始化过程是这样的:按照顺序自上而下运行类中的变量赋值语句和静态语句,如果有父类,则首先按照顺序运行父类中的变量赋值语句和静态语句。
class Field1 {
static {
System.out.println("Field1静态方法");
}
public Field1() {
System.out.println("Field1构造方法");
}
}

class Field2 {
public Field2() {
System.out.println("Field2构造方法");
}
static {
System.out.println("Field2静态方法");
}
}

class InitClass2 {
static {
System.out.println("运行父类静态代码");
}

public static Field2 f2;
public static Field1 f1 = new Field1();
}

class SubInitClass2 extends InitClass2 {
public static Field2 f2 = new Field2();
static {
System.out.println("运行子类静态代码");
}
}

public class ClassInit002 {
public static void main(String[] args) {
new SubInitClass2();
}
}
       运行结果:
运行父类静态代码
Field1静态方法
Field1构造方法
Field2静态方法
Field2构造方法
运行子类静态代码
       在类的初始化阶段,只会顺序初始化与类相关的静态赋值语句和静态语句,也就是有static关键字修饰的信息,而没有static修饰的赋值语句和执行语句在实例化对象的时候才会运行。
        loader.loadClass("....");只是对类的加载,而不是初始化。

使用

       类的使用包括主动引用和被动引用,主动引用在初始化的章节中已经说过了,下面主要来说一下被动引用:
       • 引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
       • 定义类数组,不会引起类的初始化。
       • 引用类的常量,不会引起类的初始化。
class InitClass{
static {
System.out.println("初始化InitClass");
}
public static String a = null;
public final static String b = "b";
public static void method(){}
}
class SubInitClass extends InitClass{
static {
System.out.println("初始化SubInitClass");
}
}
public class Test4 {

public static void main(String[] args) throws Exception{
//	String a = SubInitClass.a;// 引用父类的静态字段,只会引起父类初始化,而不会引起子类的初始化
//	String b = InitClass.b;// 使用类的常量不会引起类的初始化
SubInitClass[] sc = new SubInitClass[10];// 定义类数组不会引起类的初始化
}
}
       最后总结一下使用阶段:使用阶段包括主动引用和被动引用,主动引用会引起类的初始化,而被动引用不会引起类的初始化。
      当使用阶段完成之后,java类就进入了卸载阶段。

卸载

       在类使用完之后,如果满足下面的情况,类就会被卸载:
       • 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
       • 加载该类的ClassLoader已经被回收。
       • 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
       如果以上三个条件全部满足,JVM就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,Java类的整个生命周期就结束了。HotSpot提供-Xnoclassgc进行控制,使用-verbose,-XX:+TraceClassLoading、-XX:+TraceClassUnLoading可以查看类加载和卸载信息。在大量使用反射、动态代理、CCLib等场景都需要虚拟机具备类卸载的功能,以保证永久代不会溢出。

PS:根可达搜索可用对象时,可以作为根的对象有:

       • 虚拟机栈(栈桢中的本地变量表)中的引用的对象。
       • 方法区中的类静态属性引用的对象。
       • 方法区中的常量引用的对象。
       • 本地方法栈中JNI的引用的对象。

参考:http://blog.csdn.net/feihong247/article/details/7905814
           http://raging-sweet.iteye.com/blog/1195036
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: