Java类的生命周期(类加载)

Java类的生命周期

类加载的时机

在Java中有且仅有5种情况必须立即对类进行“初始化”

1、创建类的实例(new 的方式)。访问某个类或接口的静态变量,或者对该静态变量赋值,调用类的静态方法(类.xxx调用)
2、反射的方式
3、初始化某个类的子类,则其父类也会被初始化
4、Java虚拟机启动时被标明为启动类的类,直接使用java.exe命令来运行某个主类(包含main方法的那个类)
5、当使用JDK1.7的动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果为 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类还没初始化,则需要先触发初始化。

这 5 种场景中的行为称为对一个类进行主动引用,除此之外,其它所有引用类的方式都不会触发初始化,称为被动引用。

被动引用:

1.通过子类引用父类的静态字段,不会导致子类初始化。对于静态字段,只有直接定义这个字段的类才会被初始化。
2.通过数组定义来引用类,不会触发此类的初始化
3.常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发定义常量的类的初始化。

类的生命周期的七个步骤

在这里插入图片描述
1、加载(Loading)
2、验证(Verification)
3、准备(Preparation)
4、解析(Resolution)
5、初始化(Initialization)
6、使用(Using)
7、卸载(Unloading)

我只说一下类加载过程的步骤,也就是前五个阶段

第一、加载

1、通过一个类的全限定名来获取定义此类的二进制字节流。
2、将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
3、在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。

一般加载都是都编译好的.class文件中获取

第二、验证

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

第三、准备

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

注意:只是为变量分配内存,并且进行初始化(并不是真正的初始化,而是给变量赋予默认值,如:int 默认为0,String 默认为nul),真正的初始化而是在初始化阶段进行的。

第四、解析

把类中的符号引用转换成直接引用
在解析阶段,JVM会把二进制数据中的符号引用替换为直接引用。例如在A类的a方法中调用B类的b方法。

public class A {
    
    B b = new B();
    public void a() {
        b.b();//这行代码在A类的二进制数据中表示为符号引用
    }
    
}

在A类的二进制数据中,包含了一个对B类b()方法的符号引用,它由b()方法的全名和相关描述组成。在解析阶段,JVM将这个符号引用替换成为一个指针,该指针指向B类b()方法在方法区内的内存位置,这个指针就是直接引用。

第五、初始化

给类的静态变量赋予正确的初始值

准备阶段,变量已经赋过一次系统默认的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源。

可以从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。

<init>():为 Class 类实例构造器,对非静态变量解析初始化,一个类构造器对应个。

<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问

public class Test{
    static{
        i=0; //给变量赋值可以正常编译通过
        System.out.print(i); //这句编译器会提示"非法向前引用"
    }
    static int i=1}

<clinit>()方法与类的构造函数(或者说实例构造器<init>()方法)不同,它不需要显式地调用父类构造器,虚拟机会保证在子类的<init>()方法执行之前,父类的<clinit>()方法已经执行完毕。因此在虚拟机中第一个被执行的<clinit>()方法的类肯定是java.lang.Object。

<clinit>()方法对于类或接口来说并不是必需的,如果一个类中没有静态语句块,也没有对变量的赋值操作,那么编译器可以不为这个类生成<clinit>()方法。

接口中定义的变量使用时,接口才会初始化:接口中不能使用静态语句块,但仍然有变量初始化的赋值操作,因此接口与类一样都会生<clinit>()方法。但接口与类不同的是,执行接口的<clinit>()方法不需要先执行父接口的<clinit>()方法。只有当父接口中定义的变量使用时,父接口才会初始化。另外,接口的实现类在初始化时也一样不会执行接口的<clinit>()方法。

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞,在实际应用中这种阻塞往往是很隐蔽的。这也就是单例模式可以使用静态内部类的方式,保证了线程安全,因为静态内部类只被加载一次

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值