类的生命周期

来源:互联网 发布:网络攻防技术教材下载 编辑:程序博客网 时间:2024/06/11 00:25

类的生命周期

一、简介

Java类型的声明周期大概如下:

1. JVM编译java文件成二级制文件 (.class文件)

2. 类型装载、链接、初始化(方法区生成数据结构,堆区生成Class文件)

3. 对象实例化(堆区分配内存)

4. 垃圾收集

5. 对象终结

二、类的装载、链接、初始化

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

装载:把二进制形式的Java类型读入Java虚拟机中;即从硬盘加载进内存。分三个动作
1. 通过该类型的完全限定名,产生一个代表该类型的二进制数据流

2. 解析这个二进制数据流为方法区的内部数据结构。

3. 创建一个表示该类型的java.lang.Class类的实例。

装载步骤的最终产品就是这个Class类的实例对象,它成为Java程序与内部数据结构之间的接口。要访问关于该类型的信息(它们是存储在内部数据结构中的),程序就要调用该类型对应的Class实例对象方法。这样一个过程,就是把一个类型的二进制数据解析为方法区中的内部数据结构、并在堆上建立一个Class对象的过程,这被称为“创建”类型。

Java类型要通过类装载器装载,而类装载器并不需要一直等到某个类型“首次主动使用”时再去装入它。Java虚拟机规范允许类装载器缓存Java类型的二进制表现形式,可以预装载。如果一个类装载器在预先装载时遇到缺失或者错误的class文件,它必须等到程序首次主动使用该类时才报告错误。如果这个类一直没有被程序主动使用,那么该类装载器将不会报告错误。

连接:把这种已经读入虚拟机的二进制形式的类型数据合并到虚拟机的运行时状态中去。

验证:确保Java类型数据格式正确且适用于Java虚拟机使用。

验证主要是确认类型符合Java语言的语义,并且它不会危及虚拟机的完整性。如检查魔数,确保每一个部分都在正确的位置,拥有正确的长度,验证文件不是太长或者太短,等等。

首先列出确保各个类之间二进制兼容的检查:

1. 检查final的类不能拥有子类

2. 检查final的方法不能被覆盖

3. 确保在类型和超类型之间没有不兼容的方法声明(如两个方法拥有同样的名字,参数在数量、顺序、类型上都相同,但是返回类型不同)

4. 检查所有的常量池入口相互之间一致

5. 检查常量池中的所有的特殊字符串(类名、字段名和方法名、字段描述符和方法描述符)是否符合格式。

6. 检查字节码的完整性

准备:为该类型分配它所需要的内存,如为类变量分配内存(方法区),赋默认值。(static int a; 这时默认int类型初始值为0,则a的值为0)

在准备阶段,Java虚拟机为类变量分配内存,设置默认初始值。但在到达初始化阶段之前,类变量都没有被初始化为真正的初始值。在准备阶段是不会执行Java代码的。

Java虚拟机实现可能也为一些数据结构分配内存,目的是提高运行程序的性能。这种数据结构的例子如方法表,它包含指向类中每一个方法的指针。

解析:把常量池中的符号引用转换为直接引用(如声明了一个变量a,这时改为具体的内存指针或者方法表的偏移量id),此步可以在初始化之后进行。

初始化:为类变量赋初始值(如static a=3; 或执行static代码段)

为了准备让一个类或者接口被首次主动使用,最后一个步骤就是初始化,也就是为类变量赋予正确的初始值。这里的“正确”初始值指的是程序员希望这类变量所具备的起始值。正确的初始值是和在准备阶段赋予的默认初始值对比而言的。

Java代码中,一个正确的初始值是通过类变量初始化语句或者静态初始化语句给出的。如:

    static double d = Math.random() * 5;  

    static {

        a = Math.random() * 5;       

    }

所有的类变量初始化语句和类型的静态初始化器都被Java编译器收集在一起,放到一个特殊的方法中。在class文件中,这个方法被称为“<clinit>.这种方法只能被Java虚拟机调用,专门把类型的静态变量设置为它们的正确初始值。

初始化一个类包含两个步骤:

1. 如果类存在直接超类的话,切直接超类还没有被初始化,就先初始化直接超类。

2. 如果类存在一个类初始化方法,就执行此方法。

当初始化一个类的时候,第一个被初始化的类永远都是Object,然后是被主动使用的累的继承树上的所有类。超类总是在子类之前被初始化。

初始化接口并不需要初始化它的父接口,因此初始化一个接口只需要一步:如果接口存在一个接口初始化方法的话,就执行此方法。

Java虚拟机必须确保初始化过程被正确同步,如果多个线程需要初始化一个类,仅需要一个线程来执行初始化,其他线程都需要等待。当活动线程完成初始化后,必须通知其他等待的线程。

三、主动使用

所有的Java虚拟机实现必须在每个类或接口首次主动使用时初始化。下面这六种情况符合主动使用的需求。只有这六种情况

3.1创建某个类的实例时(通过在字节码中执行new指令,或者通过不明确的创建、反射、克隆或者反序列化)

class Sample {

    public static int a;

    public static final int e = 3; // final修饰的静态字段除外

    public static final int c = 6 / 2; // final修饰的编译时常量也不是主动使用

    public static final double d = Math.random() * 5;   //final修饰的运行时常量是主动引用

    public int b;

    static {

        a = 3;

        System.out.println("Sample类执行初始化,为类变量赋值,a=" + a);

    }

    public Sample() {

        b = 4;

        System.out.println("Sample类执行构造方法,为实例变量赋值,b=" + b);

    }

    public static void metod1() {

        System.out.println("静态方法execute,为类变量赋值,a=" + a);

    }

    public void metod2() {

        System.out.println("实例方法execute");

    }

}

public class InitInstance {

    public static void main(String args[]) {

        //1.创建某个类的实例时

        Sample sample1 = new Sample();

}

执行结果:

Sample类执行初始化,为类变量赋值,a=3

Sample类执行构造方法,为实例变量赋值,b=4

可见,通过new创建对象时,先初始化静态变量,然后执行构造方法

3.2当调用某个类的静态方法时

  Sample.metod1();

执行结果:

Sample类执行初始化,为类变量赋值,a=3

静态方法execute,为类变量赋值,a=3

3.3当使用某个类或接口的静态字段,或者对该字段赋值时,用final修饰的静态字段除外。

System.out.println(Sample.a);

结果:

Sample类执行初始化,为类变量赋值,a=3

3

final修饰的静态字段:

 public static final int e = 3; // final修饰的静态字段除外

System.out.println(Sample.e);

结果:

3

final修饰的编译时常量:

public static final int c = 6 / 2; // final修饰的编译时常量也不是主动使用

 System.out.println(Sample.c);

结果:

3

final

public static final double d = Math.random() * 5;   //final修饰的运行时常量是主动引用

  

 System.out.println(Sample.d);  //运行时时常量

结果:

Sample类执行初始化,为类变量赋值,a=3

0.5279444669622002

3.4当调用反射方法时

  4.1 //forName是可以初始化的

 Object obj = Class.forName("com.mylearn.j2ee.classloader.init.Sample");

结果:

Sample类执行初始化,为类变量赋值,a=3

4.2.1  loadClass方法只会加载,得到的class是还没有连接的,更不会初始化,只有在首次使用的时候才会链接,初始化

//            ClassLoader classLoader =Sample.class.getClassLoader();

//            classLoader.loadClass("com.mylearn.j2ee.classloader.init.Sample");

        //4.2.2    loadClass方法提供重载方法,第二个参数代表是否执行链接,true代表执行链接,false代表不执行,只不过这个方法是protected,上边的loadClass方法掉的其实就是false的此方法。

//            classLoader.loadClass("com.mylearn.j2ee.classloader.init.Sample",true);

3.5初始化某个类的子类时(某个类初始化时,要求它的超类已经被初始化了)

class Parent {

    static int a = 3;

    static {

        System.out.println("Parent 初始化类变量!a=" + a);

    }

    public void doSth() {

        System.out.println("Parent dosth");

    }

}

class Child extends Parent {

    static int a = 4;

    static {

        System.out.println("Child 初始化类变量!a=" + a);

    }

    public void doSth() {

        System.out.println("Child dosth");

    }

}

执行:

  Child child = new Child();

结果:

Parent 初始化类变量!a=3

Child 初始化类变量!a=4

注意一个插曲:

  Parent parent = new Child();

        System.out.println(parent.a); //注意这里,a是父类的

        parent.doSth(); //而方法是子类的

结果:

Parent 初始化类变量!a=3

Child 初始化类变量!a=4

3

Child dosth

特殊说明:

此规则不适用于接口。只有在某个接口所声明的非常量字段被使用时,该接口才会被初始化,而不会因为实现这个接口的子接口或类要初始化而被初始化。因而,任何一个类的初始化都要求它的所有祖先类(而非祖先接口)预先被初始化。而一个接口的初始化,并不要求它的祖先接口预先被初始化。

interface Angry {

    String greeting = "Grrr";

    //如果Angry被初始化了,则 Dog.getAngerLevel();会被调用,则会打印 Angry was initialized!

    int angerLevel = Dog.getAngerLevel();

}

class Dog implements Angry {

    static final String greeting = "Woof, woof, woof";

    static {

        System.out.println("Dog was initialized!");

    }

    public static int getAngerLevel() {

        System.out.println("Angry was initialized!");

        return 1;

    }

}

测试代码1

      System.out.println(Angry.greeting);

        System.out.println(Dog.greeting);

结果:

Grrr

Woof, woof, woof

可见,调用子类或父类的常量字段时不会被初始化。

测试2

     Angry angry =new Dog();  //初始化子类,接口不会被初始化

        System.out.println("-----------");

        System.out.println(angry.greeting); //调用接口的常量字段,也不会被初始化

        System.out.println("-----------");

        System.out.println(angry.angerLevel);  //调用接口的非常量字段,可以被初始化

结果:

Dog was initialized!

-----------

Grrr

-----------

Angry was initialized!

1

可见:初始化子类,接口不会被初始化;调用接口的非常量字段时接口才会初始化

3.6当虚拟机启动时某个被标注为启动类的类(即含有main方法的类,如我们执行Java命令执行某个类时)

除上述这六种情况外,所有其他使用Java类型的方式都是被动使用,它们都不会导致Java类型的初始化。

“在首次主动使用时初始化”这个规则直接影响着装载、链接和初始化的机制。在首次主动使用时,其类型必须被初始化。然而,在类型能被初始化之前,它必须已经连接了,而在它能被连接之前,它必须已经被装载了。Java虚拟机的实现可以根据需要在更早的时候装载以及链接类型,没有必要一直要等到该类型的首次主动使用才去装载和链接它。无论如何,如果一个类型在它的首次主动使用之前还没有被装载和链接的话,那它必须在此时被装载和链接,这样才能被初始化。

四、类装载器

4.1类加载器的树状组织结构

Java 中的类加载器大致可以分成两类,一类是系统提供的,另外一类则是由 Java 应用开发人员编写的。系统提供的类加载器主要有下面三个:

 引导类加载器(bootstrap class loader:它用来加载 Java 的核心库,是用原生代码来实现的,并不继承自 java.lang.ClassLoader

 扩展类加载器(extensions class loader:它用来加载 Java 的扩展库。Java 虚拟机的实现会提供一个扩展库目录。该类加载器在此目录里面查找并加载 Java 类。

 系统类加载器(system class loader):也叫应用类加载器,它根据 Java 应用的类路径(CLASSPATH)来加载 Java 类。一般来说,Java 应用的类都是由它来完成加载的。可以通过 ClassLoader.getSystemClassLoader()来获取它。

除了系统提供的类加载器以外,开发人员可以通过继承 java.lang.ClassLoader类的方式实现自己的类加载器,以满足一些特殊的需求。

除了引导类加载器之外,所有的类加载器都有一个父类加载器。通过 表 1中给出的 getParent()方法可以得到。对于系统提供的类加载器来说,系统类加载器的父类加载器是扩展类加载器,而扩展类加载器的父类加载器是引导类加载器;对于开发人员编写的类加载器来说,其父类加载器是加载此类加载器 Java 类的类加载器。因为类加载器 Java 类如同其它的 Java 类一样,也是要由类加载器来加载的。一般来说,开发人员编写的类加载器的父类加载器是系统类加载器。类加载器通过这种方式组织起来,形成树状结构。树的根节点就是引导类加载器。图 1中给出了一个典型的类加载器树状组织结构示意图,其中的箭头指向的是父类加载器。

图 1. 类加载器树状组织结构示意图

 

4.2类加载的双亲委派模型
  双亲委派模型要求除了顶层的启动类加载器外,其他的类加载器都应当有自己的父类加载器。这里类加载器之间的父子关系一般不会以继承关系来实现,而是都使用组合关系来复用父加载器的代码
  工作过程:
   如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传递到顶层的启动类加载器中,
   只有当父类加载器反馈自己无法完成这个请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去加载
  好处:
   Java类随着它的类加载器一起具备了一种带有优先级的层次关系。例如类Object,它放在rt.jar中,无论哪一个类加载器要加载这个类,最终都是委派给启动类加载器进行加载,因此Object类在程序的各种类加载器环境中都是同一个类
   判断两个类是否相同是通过classloader.class这种方式进行的,所以哪怕是同一个class文件如果被两个classloader加载,那么他们也是不同的类

再有一点就是增加安全性,如果用户随便定义一个类加载器就能加载类,那么对Java虚拟机是非常危险的。

五、类的实例化

Java程序中,类可以被明确或者隐含地实例化。

实例化一个类有四种途径:

1. 明确地使用new操作符;

2. 调用Class或者Java.lang.reflect.Constructor对象的newInstance方法;

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

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

隐含的实例化方法:

1. Main方法的String对象引用,每一个命令行参数都会有一个String对象的引用。

2. Java虚拟机装载的每一个类型,它会暗中实例化一个Class对象来代表这个类型。

Java虚拟机装载了在常量池中包含CONSTANT_String_info入口的类的时候,它会创建新的String对象的实例来表示这些常量字符串。

3. 通过执行包含字符串连接操作符的表达式产生对象。

Java虚拟机创建一个类的新实例时,不管是明确的还是隐含的,首先都需要在堆中为保存对象的实例变量分配内存。所有在对象的类中和它的超类中声明的变量都要分配内存。一旦虚拟机为新的对象准备好了堆内存,它立即把实例变量初始化为默认的初始值。

一旦虚拟机完成了为新对象分配内存和为实例变量赋默认初始值后,它随后就会为实例变量赋正确的初始值。如果对象是通过clone来的,则直接拷贝;如果是通过流的readObject读来的,则执行反序列化,继续从流中读取实际值;否则,虚拟机调用对象的实例初始化方法。实例初始化方法把对象的实例变量初始化为正确的初始值。

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

六、垃圾回收和对象终结

见垃圾收集器。

七、卸载类型

在很多方面,Java虚拟机中类的生命周期和对象的生命周期很相似。虚拟机创建并初始化对象,使程序能使用对象,然后在对象变得不再被引用后可选地执行垃圾收集。同样,虚拟机装载、链接并初始化类,使程序能使用类,当程序不在引用它们的时候可选地卸载它们。

如果程序不再引用某类型,那么这个类型就无法再对未来的计算过程产生影响。类型变成不可触及的,而且可以被垃圾回收。

使用启动类装载器的类型永远是可触及的,所以永远不会被卸载。只有使用用户定义的类装载器装载的类型才会变成不可触及的,从而被垃圾回收。如果某个类型的Class实例被发现无法通过正常的垃圾收集堆触及,那么这个类型就是不可触及的。

0 0
原创粉丝点击