【Java基础】Java对象的生命周期

一、概述

一个类通过编译器将一个Java文件编译为Class字节码文件,然后通过JVM中的解释器编译成不同操作系统的机器码。虽然操作系统不同,但是基于解释器的虚拟机是相同的。java类的生命周期就是指一个class文件加载到类文件注销整个过程。

一个java类的完整的生命周期会经历加载-连接-初始化-使用-卸载五个阶段,当然也有在加载或连接之后没有被初始化就直接被使用的情况,类加载的过程如下图:

【Java基础】Java对象的生命周期-LMLPHP

二、加载(loading)

java类生命周期加载(loading)是有类加载器完成(类加载器分为:BootstrapClassLoader,ExtClassLoader,AppClassLoader),类的class二进制文件读取到内存后,并将其保存到方法区内,然后就创建一个java.lang.Class类型的对象。类被加载入JVM中,同一个类只被载入一次。加载是类加载的第一个环节。

java生命周期加载(loading)阶段主要做三件事

  1. 类加载器(ClassLoader)通过一个类的全称获取二进制Class文件,加载到JVM内存中(如果已经获取过则直接返回其Class对象)。
  2. 将字节流所代表的静态存储结构转化为JVM方法区的运行时数据结构。
  3. 在内存中生产一个代表此类的java.lang.Class的对象,作为访问这个类的入口。

二、连接(Linking)

当类被加载之后,系统为了生成一个对应的java.lang.Class对象,接着将会进入连接阶段,连接阶段会负载把类的二进制数据合并到JVM的运行状态中。类的连接可以分为如下三个阶段:

  1. 验证:确保java类型的数据格式正确并使用与jvm使用;验证作为连接阶段的第一个阶段,这个阶段确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

    验证包含一下内容

    • 文件格式验证:如是否以魔数 0xCAFEBABE 开头、主、次版本号是否在当前虚拟机处理范围之内、常量合理性验证等。此阶段保证输入的字节流能正确地解析并存储于方法区之内,格式上符合描述一个 Java类型信息的要求;
    • 元数据验证:是否存在父类,父类的继承链是否正确,抽象类是否实现了其父类或接口之中要求实现的所有方法,字段、方法是否与父类产生矛盾等。第二阶段,保证不存在不符合 Java 语言规范的元数据信息;
    • 字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的。例如保证跳转指令不会跳转到方法体以外的字节码指令上;
    • 符号引用验证:在解析阶段中发生,保证可以将符号引用转化为直接引用。

    可以考虑使用 -Xverify:none 参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。

  2. 准备 :为该类分配内存;就是为类的静态变量分配内存并设为jvm默认的初值,对于非静态的变量,则不会为它们分配内存。静态变量的初值是由JVM自动分配初始值。

    JVM默认的初值如下:

    • 内存分配的仅包括类变量(static),而不包括实例变量,实例变量会在对象实例化时随着对象一块分配在Java堆中;
    • 设置的初始值通常情况下是数据类型默认的零值(如0、0L、null、false等),而不是被在Java代码中被显式地赋予的值;
    • 对基本数据类型来说,对于类变量(static)和全局变量,如果不显式地对其赋值而直接使用,则系统会为其赋予默认的零值,而对于局部变量来说,在使用前必须显式地为其赋值,否则编译时不通过。
    • 对于同时被static和final修饰的常量,必须在声明的时候就为其显式地赋值,否则编译时不通过;而只被final修饰的常量则既可以在声明时显式地为其赋值,也可以在类初始化时显式地为其赋值,总之,在使用前必须为其显式地赋值,系统不会为其赋予默认零值。
    • 如果在数组初始化时没有对数组中的各元素赋值,那么其中的元素将根据对应的数据类型而被赋予默认的零值。
  3. 解析:解析阶段就是虚拟机将常量池中的符号引用转化为直接引用的过程。

    • 符号引用

      符号引用是一个字符串,它给出了被引用的内容的名字并且可能会包含一些其他关于这个被引用项的信息——这些信息必须足以唯一的识别一个类、字段、方法。这样,对于其他类的符号引用必须给出类的全名。对于其他类的字段,必须给出类名、字段名以及字段描述符。对于其他类的方法的引用必须给出类名、方法名以及方法的描述符。

    • 直接引用可以是以下三种情况

      1. 直接指向目标的指针(比如,指向“类型”【Class对象】、类变量、类方法的直接引用可能是指向方法区的指针)
      2. 相对偏移量(比如,指向实例变量、实例方法的直接引用都是偏移量)
      3. 个能间接定位到目标的句柄 直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。如果有了直接引用,那引用的目标必定已经被加载入内存中了

    解析阶段可能开始于初始化之前,也可能在初始化之后开始,虚拟机会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析(初始化之前),还是等到一个符号引用将要被使用前才去解析它(初始化之后)

    解析主要分为两种情况

    1. 类或接口的解析:判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用,从而进行不同的解析。
    2. 字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系递归搜索其父类,直至查找结束。

    解析阶段的静态绑定和动态绑定:

    • 静态绑定(static binding):也叫前期绑定,在程序执行前,该方法就能够确定所在的类,此时由编译器或其它连接程序实现,比如构造方法或者被static或final修饰的。
    • 动态绑定(auto binding):也叫后期绑定,在运行时,虚拟机根据具体对象的类型进行绑定,或者说是只有对象在虚拟机中创建了之后,才能确定方法属于哪一个对象,比如含有泛型的。

三、初始化(Initialization)

执行静态变量的初始化和静态Java代码块,并初始化已设置好的变量值。需要注意的是加载、验证和装备阶段只会进行一次,而初始化是可以重复进行的。在准备阶段,类变量已经被初始化过一次系统提供的默认值,而在初始化阶段,则是根据java代码中实际指定的值去初始化类变量和其它内容。

类的初始化即是执行类构造器<clinit>()方法的过程,规则如下:

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

触发对象初始化的场景:

  1. 通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法。
  2. 通过反射方式实例化对象,如Class.forName()方法。
  3. 初始化子类的时候,会触发父类的初始化。
  4. 虚拟机启动时,初始化一个执行主类(也就是直接调用main方法)。
  5. 使用(反)序列化机制创建对象。
  6. 使用JDK7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStaticREF_putStaticRE_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。

初始化的原则: 按照顺序自上而下运行类中的变量赋值语句和静态语句,如果有父类,则首先按照顺序依次递归运行父类中的变量赋值语句和静态语句。

四、使用(Using)

当一个对象初始化完成后就生成了一个对象的实例。

  1. 访问类变量和方法不需要实例化
  2. 静态代码块只会被调用一次,而实例的代码块则是每次初始化调用一次
  3. 通过final修饰符可以防止类被继承或者变量的值被修改
  4. 设置访问权限限制其它对象的访问

实例化一个类大概有四种途径

  1. New操作符;
  2. 调用Class或者Java.lang.reflect.Constructor对象的newInstance()方法;
  3. 调用任何现有对象的Clone()方法;
  4. 通过java.io.ObjectInputStream类的getObject()方法反序列化;

实例化步骤:

  1. 在堆中为保存对象的实例变量分配内存;
  2. 为实例变量初始化为默认的初始值;
  3. 为实例变量赋正确的初始值,有三种技术完成赋值:
    • 如果对象是clone() 创建的,jvm把原实例变量中的值拷贝到新对象中;
    • 如果是通过ObjectInputStream类的readObject()调用反序列化的,jvm从输入流中读取的值来初始化实例变量;
    • jvm调用对象的实例化方法把对象的实例变量初始化为正确的初始值;

五、卸载(Unloading)

jvm实现必须具有某种自动堆存储管理策略,大部分是使用垃圾收集器。如果类声明了 void finalize()方法,垃圾收集器在释放实例内存前会执行这个方法。垃圾收集器自动调用的finalize()方法抛出的任何异常都将被忽略。

从jvm中卸载类型,很多情况,jvm中类的生命周期和对象的生命周期很相似。jvm如何判断动态装载的类型是否仍然被程序使用,其判断方式和判断对象是否仍然被使用很相似。在类使用完之后如果满足下面的情况,类就会被卸载:

  1. 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
  2. 加载该类的ClassLoader已经被回收。
  3. 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。如果使用启动类装载器装载的类型永远都是可触及的,所以永远不会被卸载。只有使用用户定义的类装载器装载的类型才会变成不可触及,才会被卸载。

六、总结

Java生命周期中,对象基本上都是在jvm的堆区中创建,在创建对象之前,会触发类加载(加载、连接、初始化),当类初始化完成后,根据类信息在堆区中实例化类对象,初始化非静态变量、非静态代码以及默认构造方法,当对象使用完之后会在合适的时候被jvm垃圾收集器回收。读完本文后我们知道,对象的生命周期只是类的生命周期中使用阶段的主动引用的一种情况(即实例化类对象)。而类的整个生命周期则要比对象的生命周期长的多。

08-16 20:14