Java对象的生命周期

生命周期概览

类生命周期包括:加载、验证、准备、解析、初始化、使用和卸载七个流程,其中验证、准备、解析这三个流程也被称之为连接(除了解析外,其他流程是顺序发生的,而解析可以与这些阶段交叉进行,因为Java支持动态绑定(晚期绑定),需要运行时才能确定具体类型),如下图所示:

在这里插入图片描述

加载(loading)

在此阶段,主要做了三件事:

  1. 类加载器(ClassLoader)通过一个类的全限定名来获取其定义的二进制字节流(class文件),并加载到jvm内存中(如果已经获取过则直接返回其Class对象)。

  2. 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。

  3. 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口。

关于类加载模型的详细介绍请参考这篇博文:JVM双亲委派模型

验证(Verifaction)

​ 验证的目的是为了确保Class文件中的字节流包含的信息符合当前虚拟机的要求,而且不会危害虚拟机自身的安全。

不同的虚拟机对类验证的实现可能会有所不同,但大致都会完成以下四个阶段的验证:

  • 文件格式的验证:验证字节流是否符合Class文件格式的规范,并且能被当前版本的虚拟机处理,该验证的主要目的是保证输入的字节流能正确地解析并存储于方法区之内。经过该阶段的验证后,字节流才会进入内存的方法区中进行存储,后面的三个验证都是基于方法区的存储结构进行的。
  • 元数据验证:对类的元数据信息进行语义校验(其实就是对类中的各数据类型进行语法校验),保证不存在不符合Java语法规范的元数据信息。
  • 字节码验证:该阶段验证的主要工作是进行数据流和控制流分析,对类的方法体进行校验分析,以保证被校验的类的方法在运行时不会做出危害虚拟机安全的行为。
  • 符号引用验证:这是最后一个阶段的验证,它发生在虚拟机将符号引用转化为直接引用的时候(解析阶段中发生该转化,后面会有讲解),主要是对类自身以外的信息(常量池中的各种符号引用)进行匹配性的校验。

准备(Preparation)

准备阶段是正式为类变量分配内存并设置类变量初始值的阶段,这些内存都将在方法区中分配。

对于该阶段有以下几点需要注意:

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

解析(Resolution)

只有解析可以与其它阶段交叉进行

解析阶段就是虚拟机将常量池中的符号引用转化为直接引用的过程

  • 符号引用:

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

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

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

解析阶段可能开始于初始化之前,也可能在初始化之后开始,虚拟机会根据需要来判断,到底是在类被加载器加载时就对常量池中的符号引用进行解析(初始化之前),还是等到一个符号引用将要被使用前才去解析它(初始化之后)。
对同一个符号引用可以进行多次解析请求,但是默认情况下虚拟机会对第一次解析的结果进行缓存(在运行时常量池中记录直接引用,并把常量标示为已解析状态),从而避免解析动作重复进行。
解析动作主要针对类或接口、字段、类方法、接口方法四类符号引用进行,分别对应于常量池中的CONSTANT_Class_infoCONSTANT_Fieldref_infoCONSTANT_Methodref_infoCONSTANT_InterfaceMethodref_info四种常量类型。

解析主要分为两种情况:

   1. 类或接口的解析:判断所要转化成的直接引用是对数组类型,还是普通的对象类型的引用,从而进行不同的解析。
   2. 字段解析:对字段进行解析时,会先在本类中查找是否包含有简单名称和字段描述符都与目标相匹配的字段,如果有,则查找结束;如果没有,则会按照继承关系从上往下递归搜索该类所实现的各个接口和它们的父接口,还没有,则按照继承关系递归搜索其父类,直至查找结束。
解析阶段的静态绑定和动态绑定
  • 静态绑定(static binding):也叫前期绑定,在程序执行前,该方法就能够确定所在的类,此时由编译器或其它连接程序实现,比如构造方法或者被static或final修饰的。
  • 动态绑定(auto binding):也叫后期绑定,在运行时,虚拟机根据具体对象的类型进行绑定,或者说是只有对象在虚拟机中创建了之后,才能确定方法属于哪一个对象,比如含有泛型的。

初始化(Initialization)

​ 到了此阶段,说明对于该对象对应类(即Class对象)的加载流程已经完成,才真正开始执行对象实例的创建流程。

需要注意的是加载、验证和装备阶段只会进行一次,而初始化是可以重复进行的。

在准备阶段,类变量已经被初始化过一次系统提供的默认值,而在初始化阶段,则是根据java代码中实际指定的值去初始化类变量和其它内容。

类的初始化阶段

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

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

<init>是对象实例构造器所调用的方法,Java 在编译之后会在字节码文件中生成 init 方法,称之为实例构造器,该实例构造器会将语句块,变量初始化,如果存在父类,则先递归处理父类的语句块、变量。

<init>()方法执行完毕后则先递归调用父类的构造方法,直到Object类的构造方法被调用后,再依次执行下面的构造方法

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

初始化的原则:按照顺序自上而下运行类中的变量赋值语句和静态语句,如果有父类,则首先按照顺序依次递归运行父类中的变量赋值语句和静态语句。
具体案例请参考:class的内容在jvm中的加载顺序

使用(Using)

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

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

可以通过对象的getClass()方法获取到该对象对应的class对象并使用。

java栈上的reference只存储了对象的引用,至于如何通过这个引用去定位、访问堆中的对象的具体位置则取决于虚拟机实现而定,目前主流的访问方式有使用句柄和直接指针。

我们常用的Sun HotSpot 虚拟机则是使用直接指针方式进行对象访问的。

卸载(Unloading)

对象的卸载

当类被加载、连接和初始化后,它就可以被其它对象或类所调用,当一个对象不再被引用,即不可达时,该对象就会被JVM垃圾回收器所回收掉,从而结束对象的生命周期,此时类在方法区内的数据并不一定会被卸载。

类的卸载

一个类何时结束生命周期,取决于代表它的Class对象何时结束生命周期。

由java虚拟机自带的三种类加载加载的类在虚拟机的整个生命周期中是不会被卸载的,由用户自定义的类加载器所加载的类才可以被卸载

java虚拟机自带的类加载器包含启动类加载器、扩展类加载器、应用类加载器,Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用他们所加载类的Class对象,因此这些Class对象始终是可触及的。

如果出现以下三种情况,类就会被卸载:

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

如果满足以上三个条件情况下,JVM就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,这样一来java类的整个生命周期就结束了。

注意:本文中所讲到的类实际上指的是类的Class对象

面试十问

读完本文你应该能回答以下问题了:

  1. 一个类可以生成多少个Class对象?

  2. Class对象的生命周期有哪几个阶段?

  3. 类的连接是哪几个阶段?

  4. 一个类变量在对象实例化过程中最多可以被赋值几次?

  5. 静态绑定和动态绑定的区别?

  6. 什么情况下会触发对象的初始化?

  7. 对象初始化过程执行了哪些操作?

  8. 父类静态代码块和子类静态代码块哪个先执行?

  9. 以下代码,哪个类先被JVM加载?

    MyObject myObject = new MyObject();
    Object o = new Object();
    
  10. 通过克隆方法复制一个对象的过程中会重新初始化吗?

  • 20
    点赞
  • 73
    收藏
    觉得还不错? 一键收藏
  • 3
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值