java类生命周期

在聊java 类加载机制之前,需要了解java字节码,见:https://blog.csdn.net/liuxiao723846/article/details/109156658

类(.java文件)首先被编译成字节码(.class文件),从字节码被JVM加载到内存开始,到卸载出内存结束,生命周期为:加载>验证>准备>解析>初始化>使用>卸载,其中验证、准备、解析称作连接。

和那些编译时需要连接工作的语言不同,在java中类的加载和初始化都是在程序运行时期完成的,虽然会造成性能上的一点影响,但却能为java应用程序带来高度的灵活性。java中天生可以动态扩展的语言特性就是依赖运行期动态加载和动态连接实现的。

说明:在java语言中,将.java到.class是编译期完成的,这个阶段可以通过JSR 269 API来增强字节码(静态的);当程序启动,开始接下来类的loading、initializationg等,这些都是在运行期完的,这个阶段也可以动态的对字节码增强。本文要讲的内容,都是属于运行期的内容

1、loading阶段

java程序都是由若干个.class文件组织的,当程序在运行时会调用该程序的一个入口函数来调用程序的相关功能,这些功能都被封装在不同的class文件当中,所以经常要从这个class文件中要调用另外一个class文件中的方法,如果另外一个文件不存在的,则会引发系统异常。

而程序在启动的时候,并不会一次性加载程序所要用的所有class文件,而是根据程序的需要,通过Java的类加载机制(ClassLoader)来动态加载某个class文件到内存当中的。

1.1)工作内容:

JVM 在该阶段的主要目的是将字节码从不同的数据源(可能是 class 文件、 jar 包,甚至网络资源)转化为二进制字节流加载到内存中,并生成一个代表该类的 java.lang.Class 对象

1.2)ClassLoader(类加载器):

loading过程主要靠类加载器来完成。JVM提供了三个默认的类加载器。

1)BootStrap ClassLoader:

称为启动类加载器,C++实现的,是Java类加载层次中最顶层的类加载器(JVM启动后初始化的),负责加载JDK中的核心类库,如:rt.jar、resources.jar、charsets.jar等。

扫描二维码关注公众号,回复: 12654014 查看本文章

Bootstrap ClassLoader加载核心类后,构造Extension ClassLoader,并且将ExtClassLoader的父加载器设置为Bootstrp loader;当Bootstrp loader加载完ExtClassLoader后,继续会加载AppClassLoader,并且将AppClassLoader的父加载器指定为 ExtClassLoader。

2)Extension ClassLoader:

称为扩展类加载器,负责加载Java的扩展类库,默认加载JAVA_HOME/jre/lib/ext/目下的所有jar。该加载器是有java实现的,由Bootstrp loader加载ExtClassLoader,并且将ExtClassLoader的父加载器设置为Bootstrp loader;

 3)App ClassLoader:

称为系统类加载器,负责加载应用程序classpath目录下的所有jar和class文件。

注: 除了Java默认提供的三个ClassLoader之外,用户还可以根据需要定义自已的ClassLoader,而这些自定义的ClassLoader都必须继承自java.lang.ClassLoader类(包括JVM提供的Extension ClassLoader和App ClassLoader在内)。

1.3)双亲委派机制:

ClassLoader使用的是双亲委托模型来搜索类的,每个ClassLoader实例都有一个父类加载器的引用(不是继承的关系,是一个包含的关系),虚拟机内置的类加载器(Bootstrap ClassLoader)本身没有父类加载器,但可以用作其它ClassLoader实例的的父类加载器。

1)双亲委派模型:

当一个ClassLoader实例需要加载某个类时,它会试图自下而上检查(检查父类加载器)类是否已经加载过,如果某个加载器已经加载该类则直接返回该类对象,直到bootstrap ClassLoader。然后由上至下依次加载类,首先由最顶层的类加载器Bootstrap ClassLoader试图加载,如果没加载到,则把任务转交给Extension ClassLoader试图加载,如果也没加载到,则转交给App ClassLoader 进行加载,如果它也没有加载得到的话,则返回给委托的发起者,由它到指定的文件系统或网络等URL中加载该类。如果它们都没有加载到这个类时,则抛出ClassNotFoundException异常。

2)为什么要使用双亲委托这种模型呢?

  • 避免重复加载,当父亲已经加载了该类的时候,就没有必要子ClassLoader再加载一次。
  • 考虑到安全因素,我们试想一下,如果不使用这种委托模式,那我们就可以随时使用自定义的String来动态替代java核心api中定义的类型,这样会存在非常大的安全隐患,而双亲委托的方式,就可以避免这种情况,因为String已经在启动时就被引导类加载器(Bootstrcp ClassLoader)加载,所以用户自定义的ClassLoader永远也无法加载一个自己写的String,除非你改变JDK中ClassLoader搜索类的默认算法。

1.4)线程上下文类加载器:

jdk的“双亲委派”模型很好的解决了各个类加载器的基础类的统一问题(越基础的类越由上层类加载器进行加载),基础类之所以被加载,是因为他们总是作为被用户调用的api,但事与愿违,如果基础类又要回调用户的代码,那该怎么办?

一个典型的例子是JNDI服务,JNDI已经是java的标准服务,他的代码由booter类加载器去加载,但JNDI需要调用各个厂商实现的JNDI接口提供者(SPI,server provider Interface)的代码,但是booter类加载器不可能加载到应用程序classpath下的这些实现类,怎么办?

为了解决上面问题,java提出了线程上下文类加载器(context class loader)。有了他,JNDI服务使用这个线程上下文类加载器去加载所需要的SPI代码,也就是父类加载器请求子类加载器去完成类加载动作,这实际上是打破了“双亲委派”模型,逆向使用类加载器。

1)线程上下文类加载器(context class loader)使用:

从 JDK 1.2 开始引入的。类 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器,如果在应用程序全局范围内都没设置,那么这个类加载器默认是app类加载器。

2)示例:

在jdbc的类中使用下面代码,获取mysql的实现类对象

//在main方法运行时,当前线程类加载器就是app的类加载器,所以在booter类加载器加载jdbc代码时,可以通过下面方式获得mysql实现类、并调用其方法

ClassLoader cl = Thread.currentThread().getContextClassLoader();

Class<?> aClass = Class.forName("com.mysql.jdbc.Driver", true, cl);

//强转成jdk中的Driver接口

Driver  test = (Driver)aClass.newInstance();

2、linking阶段

该阶段包括了三个过程。

2.1)verfication(验证):

JVM 会在该阶段对二进制字节流进行校验,只有符合 JVM 字节码规范的才能被 JVM 正确执行。该阶段是保证 JVM 安全的重要屏障,下面是一些主要的检查。

  • 确保二进制字节流格式符合预期(比如说是否以 cafe bene 开头)。
  • 是否所有方法都遵守访问控制关键字的限定。
  • 方法调用的参数个数和类型是否正确。
  • 确保变量在使用之前被正确初始化了。
  • 检查变量是否被赋予恰当类型的值。

2.2)preparation(准备):

JVM 会在该阶段对类变量(也称为静态变量,static 关键字修饰的)分配内存并初始化(对应数据类型的默认初始值,如 0、0L、null、false 等);同时,将常量分配到方法区。示例:

public String chenmo = "沉默";

public static String wanger = "王二";

public static final String cmower = "沉默王二";

分析:

  • chenmo 是实例变量所以不会被分配内存;
  • wanger 类变量会分配内存,同时初始值设为null(不是“王二”);
  • cmower 被static final 修饰,被称作为全局常量,和类变量不同,常量一旦赋值就不会改变了,所以 cmower 在准备阶段的值为“沉默王二”而不是 null。

注:类变量、常量都存储在方法区中。

2.3)resolution(解析):

该阶段将常量池中的符号引用转化为直接引用

  • 符号引用:以一组符号(任何形式的字面量,只要在使用时能够无歧义的定位到目标即可)来描述所引用的目标。在编译时,Java 类并不知道所引用的类的实际地址,因此只能使用符号引用来代替。比如 com.Wanger 类引用了 com.Chenmo 类,编译时 Wanger 类并不知道 Chenmo 类的实际内存地址,因此只能使用符号 com.Chenmo。
  • 直接引用:通过对符号引用进行解析,找到引用的实际内存地址。

3、initialization(初始化)

从字节码到运行时的对象,大致上经过了三个阶段:loading、linking、initialization。在jdk官方文档中说明,经过了initialization后,类就变成了实例对象从而可以使用了。但这个过程非常复杂,为了能更清晰的说明问题,人为的把这里的initialization分成两个阶段:类的初始化、对象的实例化(实例初始化)。

3.1)类初始化、实例初始化:

在准备阶段,类变量已经被赋过默认初始值,而在初始化阶段,类变量将被赋值为代码期望赋的值;也就是说,Initialization这个阶段会去真正执行代码,按照代码书写顺序进行初始化,注意:如果一个类有父类,会先去执行父类的initialization阶段,然后在执行自己的。

1)类初始化:

在类初始化之前的准备阶段,JVM会将类变量(static 修饰的变量)分配内存并设置初始值,在类初始化阶段,JVM执行类构造器 <cinit>() 方法。<cinit> 方法有如下特点:

  1. 编译器会在将 .java 文件编译成 .class 文件时,收集所有类变量初始化代码和 static {} 块的代码,组成 <cinit>() 方法;
  2. 子类初始化时会首先调用父类的 <cinit>() 方法;
  3. JVM 会保证 <cinit>() 方法的线程安全,保证同一时间只有一个线程执行;

总之,这个过程是按照代码书写顺序执行static变量赋值、static代码块。

并非所有类都拥有<clinit>()方法。以下类不会拥有<clinit>方法:

  • 该类既没有声明任何类变量,也没有静态初始化语句。
  • 该类声明了类变量,但没有使用类变量初始化语句或静态初始化语句初始化。
  • 该类只包含静态final变量的类变量初始化语句,并且类变量初始化语句是常量表达式。

2)实例初始化:

编译器为每个类生成至少一个实例初始化方法,即<init>()方法。如果类没有声明构造方法,JVM会自动生成一个默认构造方法,该方法仅调用父类的默认构造方法。实例初始化过程:

  1. 编译器收集实例初始化变量初始化语句和 {} 块的代码,组成实例初始化方法 <init>();
  2. 实例初始化时首先执行 <init>() 方法,然后执行构造函数;
  3. 子类通过构造函数构造实例时会首先调用父类的 <init>() 方法和父类的构造函数,如果没有显示调用父类的构造函数,那么 JVM 会自动调用父类的无参构造函数,保证父类构造函数一定被调用,然后再是子类自己的 <init>() 方法和构造函数;

3)总结:

  • 类初始化 <cinit>:类第一次加载到内存时进行的过程,类初始化只进行一次(前提是被同一类加载器加载)。初始化的都是属于类的内容(静态),对所有实例共享。后续使用 new 等实例化对象时都不再重复进行类初始化了。
  • 实例初始化 <init>:实例化对象时每次都会进行的过程(可以进行多次),初始化属于实例的内容(非静态),每个实例所拥有的内容是独有的。

类初始化时,代码的执行顺序:(包括实例初始化)

  1. 父类的static变量赋值等、static代码块中的语句;(编译后转成了<cinit>方法)
  2. 子类的static变量赋值等、static代码块中的语句;(编译后转成了<cinit>方法)
  3. 父类的实例变量、代码快、构造函数;
  4. 子类的实例变量、代码快、构造函数;

说明:1和2是类的初始化过程、后面3和4是对象的初始化过程。

3.2)类初始化时机:

1)类生命周期各阶段的执行顺序:

加载、验证、准备、初始化、卸载 这五个阶段顺序是确定的,类加载过程一定是这个顺序开始的;但解析阶段不一定,有些时候可以在初始化之后在进行,这是为了支持java语言的运行时绑定(动态绑定)

2)第一次、主动调用类时,会触发类的初始化:

JVM对类的加载时机没有做严格规定;但是初始化时机,虚拟机规范是严格规定了的,以下六种主动调用是会触发类的初始化:(加载、验证、准备自然要在初始化之前)

  • 一个类的实例被创建(new操作、反射、cloning、反序列化);
  • 调用类的static方法;
  • 设置、读取类的static字段(被final修饰已经在编译期把结果放入常量池的静态字段除外);
  • 使用reflect包的方法对类进行反射操作时,如果类没有初始化,则立即初始化;
  • 当初始化一个类时,如果发现父类还没有进行初始化,则需要先处罚父类的初始化;
  • 被设定为JVM启动时的启动类(具有main方法的类);

除了以上6种情形,java中类的其他使用方式都是被动使用,不会导致类的初始化。(后面有示例)

3)执行次数:

在JVM中对初始化时机的定义中有两个关键词:第一次与主动调用。第一次是说只在第一次时才会有初始化过程,以后就不需要了,可以理解为每个类有且仅有一次初始化的机会。(这里的初始化特指”类初始化“,不包含实例的初始化,很明显实例的初始化可以有多次,每次new创建对象都会触发实例对象的创建、初始化)

根据类加载的双亲委派算法,也不难得出每个类有且只有一次加载的机会(除了用不同类加载器加载某个类)。

4、示例

class Singleton  {

//    private static Singleton  mInstance = new Singleton ();// 位置1
    public static int counter1;
    public static int counter2 = 0;

    private static Singleton mInstance = new Singleton();// 位置2

    private Singleton () {
        counter1++;
        counter2++;
    }

    public static Singleton  getInstantce() {
        return mInstance;
    }
}

public class InitDemo {

    public static void main(String[] args) {

    	Singleton  singleton = Singleton .getInstantce();
        System.out.println("counter1: " + singleton.counter1);
        System.out.println("counter2: " + singleton.counter2);
    }
}

当mInstance在位置1时,输出:

counter1:1

counter2:0

当mInstance在位置2时,输出:

counter1:1

counter2:1

原因:Singleton中的三个属性在Preparation阶段会根据类型赋予默认值,在Initialization阶段会根据显示赋值的表达式再次进行赋值(按顺序自上而下执行)

4.2)主动调用vs被动调用:

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.");
    }
}

public class ActiveUsageDemo {
    public static void main(String[] args) {
        // Using hoursOfSleep is an active use of NewParent,
        // but a passive use of NewbornBaby
        System.out.println(NewbornBaby.hoursOfSleep);
    }

    static {
        System.out.println("ActiveUsageDemo was initialized.");
    }
}

输出:

ActiveUsageDemo was initialized.
NewParent was initialized.
1

之所以没有输出NewbornBaby was initialized.是因为没有主动去调用NewbornBaby,如果把打印的内容改为NewbornBaby.hoursOfCrying 那么这时就是主动调用NewbornBaby了,相应的语句也会打印出来。
 

参考:

https://blog.csdn.net/w1196726224/article/details/56529615

https://www.cnblogs.com/jxzheng/p/5191037.html

https://blog.csdn.net/justloveyou_/article/details/72466105

https://liujiacai.net/blog/2014/07/12/order-of-initialization-in-java/

https://blog.csdn.net/justloveyou_/article/details/72466416

https://juejin.cn/post/6844903886512193550

https://juejin.cn/post/6844903460635148301

猜你喜欢

转载自blog.csdn.net/liuxiao723846/article/details/109901300