JVM篇之类加载的生命周期

JVM篇之类加载的生命周期

写在最前面::一句话概括::整个类的加载过程

装载-》链接-》初始化-》使用-》卸载

装载阶段(Load)

装载阶段主要做三件事:

1、由类加载器加载class文件,也就是通过类全名获取该文件的二进制流
2、将该二进制流(类)中的静态存储结构转化为方法区的运行时数据结构,也就是将类中的静态结构存储到方法区
3、在堆中生成class对象,作为对方法区中的数据访问入口。

简单来说这三步其实就是将class读取一下,将里面的静态数据放到方法区,然后在堆中提供一个访问到静态数据的入口。

经过装载这个阶段,相当于是这个class对象已经在内存中,接下来就是对class信息的链接阶段。

类加载器

我们已经知道装载阶段就是读取二进制流,而实现这个步骤的就是类加载器。而类加载器的另外一个作用其实是确保**类在虚拟机中的唯一性。**这里要注意的是在整个jvm中,即使全名类相同,若类加载器不同,仍然不能算是同一个类。

类加载器分类

1)Bootstrap ClassLoader 负责加载$JAVA_HOME中 jre/lib/rt.jar 里所有的class或Xbootclassoath选项指定的jar包。由C++实现,不是ClassLoader子类。

2)Extension ClassLoader 负责加载java平台中扩展功能的一些jar包,包括$JAVA_HOME中jre/lib/*.jar 或 -Djava.ext.dirs指定目录下的jar包。

3)App ClassLoader 负责加载classpath中指定的jar包及 Djava.class.path 所指定目录下的类和jar包。

4)Custom ClassLoader 通过java.lang.ClassLoader的子类自定义加载class,属于应用程序根据自身需要自定义的ClaassLoader,如tomcat,jboss都会根据j2ee规范自行实现类加载器。

**思考:**为什么类加载器要分这么多?一个类加载器不行么?

在jvm1.2之前确实只有一个类加载,如果只存在一个类加载器,所有的类都由这个加载器进行加载,如果我们建个java.lang.String类(java中并不阻止我们建立同样包的同名类),那么在装载阶段就会出现问题,因为类加载器不知道去加载哪个类,所以才有了分层加载器,每层加载器只加载分属区域的类。

类加载器特性
类加载器负责机制

当一个类加载器负责加载某个Class时,该Class所依赖的和引用的其他Class也将由该类加载器负责载入,除非显示使用另外一个类加载器来载入。例如,系统类加载器AppClassLoader加载入口类(含有main方法的类)时,会把main方法所依赖的类及引用的类也载入。

注意:该机制(特性)只是调用了ClassLoader.loadClass(name)方法,并没有真正定义类。真正加载class字节码文件生成Class对象由“双亲委派”机制完成。

双亲委派机制

双亲委派是指子类加载器如果没有加载过该目标类,就先委托父类加载器加载该目标类,只有在父类加载器找不到字节码文件的情况下才从自己的类路径中查找并装载目标类。

具体过程:

  1. ClassLoader先判断该Class是否已加载,如果已加载,则返回Class对象;如果没有则委托给父类加载器。

  2. 父类加载器判断是否加载过该Class,如果已加载,则返回Class对象;如果没有则委托给祖父类加载器。

  3. 依此类推,直到始祖类加载器(引用类加载器-Bootstrap ClassLoader )。

  4. 始祖类加载器判断是否加载过该Class,如果已加载,则返回Class对象;如果没有则尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,则委托给始祖类加载器的子类加载器。

  5. 始祖类加载器的子类加载器尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,则委托给始祖类加载器的孙类加载器。

  6. 依此类推,直到源ClassLoader。

  7. 源ClassLoader尝试从其对应的类路径下寻找class字节码文件并载入。如果载入成功,则返回Class对象;如果载入失败,源ClassLoader不会再委托其子类加载器,而是抛出异常。

要注意的是双亲委派只是java推荐的机制,在此基础上我们可以通过自定义类加载器,实现java.lang.ClassLoader类),如果想打破双亲委派机制,就重写loadClass(name)方法,如果想要保持双亲委派机制,可以选择重写findClass(name)方法。

tip:打破双亲委派机制的根本其实都是自定义类加载器,具体的例子 比如 热部署的实现。

缓存机制

缓存机制将会保证所有加载过的Class都将在内存中缓存,当程序中需要使用某个Class时,类加载器先从内存的缓存区寻找该Class,只有缓存区不存在,系统才会读取该类对应的二进制数据,并将其转换成Class对象,存入缓存区。这就是为什么修改了Class后,必须重启JVM,程序的修改才会生效.对于一个类加载器实例来说,相同全名的类只加载一次,即 loadClass方法不会被重复调用。

tip:该机制也可以解释为什么类只会被初始化一次(会被缓存)。

链接阶段

可以理解为这个阶段主要是对加载进来的信息做一个校验和静态变量的一个默认赋值阶段。这个阶段又可以分为三个步骤:

1、验证

验证步骤主要是对class文件做一个安全校验。
文件格式的验证(class文件开头都是cafebabe)
class的版本号(校验是否满足当前jvm可执行的版本)
元数据校验(java语义校验(是否有父类、抽象类是否有实现、是否继承了final类等等)
相对来说,整个验证阶段十分复杂,如果可以确保class文件正确,可以通过参数-Xverify:none取消验证。

2、准备

这个阶段主要是为类中的静态变量和基本数据类型进行初始值赋值。注意是静态变量,也就是static修饰的类变量,这里也不包括用final修饰的类变量(在编译期就进行了赋值)。
在准备阶段static变量只会进行内存开辟,这时候变量值都为默认值。静态变量的赋值是放在初始化阶段进行的。

3、解析

这个步骤是将类中的符号引用替换为直接引用。
在装载阶段我们知道是将类中的方法、常量池等存放到了方法区,这个阶段主要就是将常量池中的符号引用(可以是任意值,简称符号)替换为真正的直接引用(直接指向目标的指针),主要针对类或接口、字段、类方法、接口方法、方法类型、方法句柄和调用限定符7类符号引用进行。

初始化阶段

初始化阶段其实就是执行方法,在链接阶段,我们知道是将静态变量和基本数据类型进行了默认值赋值,在初始化阶段,将真正的为这些值进行赋值。

初始化阶段步骤:

1、如果一个类还没有被装载和链接,就需要先加载并链接该类;
2、想要初始化一个子类,就需要先初始化父类(extends);
3、所谓类的初始化就是执行()方法:
()方法由静态类变量显式的赋值代码 和 静态代码块组成(static关键字),并且()方法只会执行一次。

通过编译查看源码:
在这里插入图片描述
在这里插入图片描述

补充:对象中变量和构造器等执行顺序:
public class Father {

    // 构造器
    public Father() {
        System.out.println("father构造器(1)");
    }

    // 静态代码块
    static {
        System.out.println("father静态代码块(2)");
    }

    // 普通代码块
    {
        System.out.println("father普通代码块(3)");
    }

    // 静态成员变量
    private static int i = staticMember();

    // 成员变量
    private int j = member();


    /**
     * 静态方法
     */
    public static int staticMember() {
        System.out.println("father静态变量引用的方法(4)");
        return 1;
    }

    /**
     * 成员方法
     */
    public int member() {
        System.out.println("father实例变量引用的普通方法(5)");
        return 1;
    }

}

public class Son extends Father {

    // 构造器
    public Son() {
        System.out.println("son构造器(6)");
    }

    // 静态代码块
    static {
        System.out.println("son静态代码块(7)");
    }

    // 普通代码块
    {
        System.out.println("son普通代码块(8)");
    }

    // 静态成员变量
    private static int i = staticMember();

    // 成员变量
    private int j = member();

    /**
     * 静态方法
     */
    public static int staticMember() {
        System.out. println("son静态变量引用的方法(9)");
        return 1;
    }

    /**
     * 成员方法
     */
    public int member() {
        System.out.println("son重写父类实例变量引用的普通方法(10)");
        return 1;
    }

    public static void main(String[] args) {
        System.out.println("---------第1次实例化Son对象----------");
        Son son1 = new Son();
        System.out.println();
        System.out.println("---------第二次实例化Son对象----------");
        Son son2 = new Son();
    }

}

打印结果:

father静态代码块(2)
father静态变量引用的方法(4)
son静态代码块(7)
son静态变量引用的方法(9)
---------1次实例化Son对象----------
father普通代码块(3)
son重写父类实例变量引用的普通方法(10)
father构造器(1)
son普通代码块(8)
son重写父类实例变量引用的普通方法(10)
son构造器(6)

---------第二次实例化Son对象----------
father普通代码块(3)
son重写父类实例变量引用的普通方法(10)
father构造器(1)
son普通代码块(8)
son重写父类实例变量引用的普通方法(10)
son构造器(6)

结论:

1、实例化子类的时候必须先实例化父类

2、先加载父类的静态变量和静态代码块(同级别的按照代码书写顺序执行),然后加载子类的静态变量和静态代码块

3、加载父类的普通变量、普通代码块、构造方法,再加载子类的普通变量、普通代码块、构造方法,注意:如果子类重写了父类的方法,父类在调用的时候实际调用的是子类的方法,可以看到 在father普通代码块(3)执行之后,实际要执行的是father类中的member方法,但打印的实际结果是子类重写的member方法。

4、静态变量和代码块只会执行一次,可以看到第二次实例化son对象的时候,只执行了非静态变量和构造器方法。

使用阶段

思考一个问题,类的初始化阶段是什么时候进行的?是jvm启动开始装载class就会进行初始化么?

只有当类被真正使用的时候才会进行初始化

主动触发初始化:

  • 创建类的实例,也就是new的方式
  • 访问某个类或接口的静态变量,或者对该静态变量赋值
  • 调用类的静态方法
  • 反射(如 Class.forName(“com.willer.Test”) )
  • 初始化某个类的子类,则其父类也会被初始化
  • Java虚拟机启动时被标明为启动类的类(JvmCaseApplication ),直接使用 java.exe 命令来运行某个主类

被动触发初始化:

  • 引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
  • 定义类数组,不会引起类的初始化。
  • 引用类的static final常量,不会引起类的初始化(如果只有static修饰,还是会引起该类初始化的)。

卸载

在类使用完之后,如果满足下面的情况,类就会被卸载:

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

Java虚拟机本身会始终引用这些类加载器,而这些类加载器则会始终引用它们所加载的类的Class对象,因此这些Class对象始终是可触及的。如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。但是一般情况下启动类加载器加载的类不会被卸载,而我们的其他两种基础类型的类加载器只有在极少数情况下才会被卸载。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值