类的加载过程

前置知识

class文件的的结构

概述

前端编译器会将HelloWorld.java文件编译为HelloWorld.class文件,让后jvm就可以读取HelloWorld.class文件进入内存,但是进入内存之前的操作,称之为类的加载。
进行类的加载的时候,是jvm程序已经启动的时候,这个时候程序已经运行了。
在加载阶段,会进行加载,连接,初始化三个大体步骤,也就是说,java是在运行时才进行连接的,这样比起一些编译的时候进行连接的语言要多一些开销,但是这却增加了java的可扩展性和灵活性,使得java天生就可以动态扩展。

动态扩展的例子:
根据规范,下面这个才是规范的创建一个list,list中的类型是Object类型,即等号右边也要写Object类型
List<Object> list = new ArrayList<Object>();
但是,我们通常只写等号左边的object类型,即:
List<Object> list = new ArrayList<>();
也就是说,我们右边写了object类型后,在加载的时候,会自己的给我们补充成在这里插入代码片new ArrayList<Object>();的样子

类的加载阶段

概述

一个类从加载到虚拟机的内存开始,到结束,总共经历了五个阶段,其中链接阶段分为3个小阶段,七个阶段的顺序如图所示:
类的生命周期

注意:大多数时候下,类的生命周期是顺序开始执行的,但是,在某些情况下,解析可能会在初始化之后进行
开始执行的意思是:它们的开始顺序固定,但是,在开始之后,没结束之前,下一阶段也可能开始

加载

在加载阶段,jvm需要完成三件事情(摘至深入理解Java虚拟机第三版P267):

  1. 通过一个类的全限定名(包名+类名)来获得定义此类的二进制字节流---->获得二进制字节流
  2. 将这个字节流所代表的静态存储结构转换为方法区(jdk8后叫元空间)的运行时数据结构----> 生成类模板
  3. 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据访问入口----> 生成对应的class对象

总结:将.class的字节码文件加载到机器的内存中,并生成对应的类模板对象和Class实例

什么是类模板对象

类模板对象是java类在jvm中存入的一个快照,jvm将字节码文件中解析出的常量池、类字段、类的方法等信息存入类模板中,这样jvm在运行期间便能通过类模板获得java类的任意信息,可以对java类的成员变量进行访问和方法进行调用,反射也就是用了这个原理。
而且在堆中存储的某一个具体对象的对象头中,是包指向含类模板对象的信息

Class实例和类模板所在的位置

class实例,是一个具体的对象,放入堆中
类模板:因为几乎不会被回收,所以放入方法区(元空间)

关于class实例:
class类的构造方法是私有的,只有jvm能够创建class类。class类是对类模板的一个访问接口,只有通过class类,才可以放入类模板


数组的加载

数组本身不是通过类加载器来进行加载的,它是由jvm虚拟机直接在内存中动态的构造出来的,但是数组的类型也是需要靠类加载器进行加载,创建数组大致如下过程(摘自深入理解java虚拟机第三版P268):

  1. 如果数组的组件类型(数组去掉一个维度的类性,如Object[] 去掉一个维度为Object)是引用类型,那么让对应的加载器去加载此类,并将该数组和该加载器关联起来
  2. 若不上引用类型,那么将该数组和引导类加载器关联
  3. 数组类的可访问性和组件类型的访问性一致,若不是引用类型,则为public。



关于验证

前面说过,每个阶段的开始顺序大致是概述中的图示,但是可能在加载的时候,也就开始了,也就是说,加载和验证阶段可以同时进行,但是加载的开始一定是在验证的前面



链接

验证

目的:

它的目的是保证加载的字节码是合法、合理并符合规范的

验证类型

大致分为如下的类型进行验证,
文件的格式验证–> 语义验证 -> 字节码验证 -> 符号引用验证

对验证的说明

在字节码验证中,若验证没通过,一定不安全,若通过,不一定安全。(参考停机问题)
符号引用验证,是保证解析阶段可以正常执行,发生在将符号引用转换为直接引用的时候

符号引用转换为直接引用:将class文件中的常量池中的数据(符号引用)转换为内存中某个具体位置的数据(直接引用)

关于加载

加载的时候说过,要将数据存入内存中的方法区,但是前提是要在文件格式验证阶段结束后,通过了验证,才可以放入内存中方法区存储,放入方法区后,后面的三种验证都是在方法区进行

验证中的参数以及错误

-XX:-UseSplitVerifier:jdk6之后,关闭javac编译器的对验证进行的优化
-XX:+FailOverToOldVerifier: 在类型校验失败后,退回到旧的类型推导方式,对应jdk6后的版本来说,不允许退回原来的类型推导的校验方式,但是虚拟机中仍保留着推导校验的代码
-Xverify:none: 关闭大部分类的验证措施

错误类性:
验证失败会抛出一个java.lang.IncompatibleClassChangeError的子类异常,如:
java.lang.IllegalAccessError,java.lang.NoSuchFieldError,java.lang.NoSuchMethodError




准备

目的

为类中的静态变量分配内存空间并设置默认值,并对类中的静态常量分配空间并赋值
下面的代码,在准备阶段分配空间并设置值后分别是后面的情况

public static int value = 13; ===>>> public static int value = 0;


public static final int value = 13; ===>>> public static final int value = 13;

因为int类型的默认值就是0;
各种基本类型的默认值:
基本类型默认值

解析

目的

将符号引用转换为直接引用,也就是得到类、字段、方法在内存中的偏移量。

不过,由于java支持运行时绑定(晚期绑定),也就是字节码中支持了的invokedynamic指令,所以有些时候解析在初始化后面才开始,如lambda表达式。

初始化

jvm规定,有且仅有主动引用的情况下,才会对类进行初始化。

主动引用

  1. 创建一个对象,如new、反射、序列化、克隆
  2. 调用静态方法
  3. 读取或修改一个类型的静态字段(strict final是属于常量字段)
  4. 父类没有进行初始化,则要初始化父类
  5. jvm启动的时候,用户执行的主线程的类,即main方法的类
  6. jdk8加的默认方法,如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化。
  7. 当使用java.lang.reflect包中的方法反射类的方法时。比如:Class.forName(“com.xing.java.Test”)
  8. 当初次调用 java.lang.invoke.MethodHandle 实例时,初始化该 MethodHandle 指向的方法所在的类。(涉及解析REF_getStatic、REF_putStatic、REF_invokeStatic方法句柄对应的类)


如果针对代码,设置参数-XX:+TraceClassLoading,可以追踪类的加载信息并打印出来。

除了主动使用以外,其他的都是被动使用,不会进行初始化,也就不会调用<clinit>()方法

()方法

<clinit>()方法不是程序员写的代码,他是编译器自动生成的,初始化阶段也就是执行这个方法。

深入理解Java虚拟性第三版中说到(P277):他是编译器自动生成物,但是我们非常有必要了解这个方法是如何具体产生的,以及<clinit>()方法执行过程中各种有可能影响程序运行行为的细节。



1. 收集静态变量和静态语句块

()方法是由编译器自动收集类中的所有类变量的赋值和静态代码块,并对它们进行合并产生的。编译器收集的顺序由语句在源文件中出现的顺序决定的。
静态语句块中,如果要访问在源文件中还在该语句块后出现的数据的时候,会报非法前向引用,但是可以修改该变量
如图,在赋值的时候idea没报错,但是在访问的时候,idea报错
在这里插入图片描述



2. 父类的()

()和构造方法()不同,它不需要显示的调用父类构造器,jvm会保证父类的()以及别调用过了,在调用子类的();

// 父类
public class Father {
    public static int id = 1;
    public static int number;

    static {
        number = 2;
        System.out.println("father static{}");
    }

}
// 子类
public class SubInitialization extends Father {
    static{
        number = 4;//number属性必须提前已经加载:一定会先加载父类。
        System.out.println("son static{}");
    }

    public static void main(String[] args) {
        System.out.println(number);
    }
}

如果上述的结论正确,那么这里输出的答案就是4,并且father先输出
输出结果:

father static{}
son static{}
4

3. 在主动使用的前提下无()方法

类:<clinit>()方法的主要作用就是对静态代码块和静态变量进行操作,如果类中没有静态代码块和静态变量的赋值操作,那么也就不需要这个方法了。
接口:接口不能使用静态代码块,但是可以使用静态变量初始化进行赋值,也可以生成()方法,但是接口执行的时候,父接口不用先执行(),只有父接口在使用的时候,才会执行(),同样,接口是实现类在初始化 的时候也不会执行接口的初始化方法

4. ()的死锁问题

如果多个线程进行初始化的时候,若其中一个线程初始化的 时间长,其他的线程就会处于阻塞状态
或者两个类都使用反射进行初始化,但是两者都没有初始化完成,但是都卡住了对方进行初始化。
A进行初始化,但是会暂停一会,然后调用反射去加载 另外一个类

class StaticA {
    static {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
        try {
            Class.forName("com.xxx.jvm.StaticB");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println("StaticA init OK");
    }
}

class StaticB {
    static {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
        }
        try {
            Class.forName("com.xxx.jvm.StaticA");
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
        System.out.println("StaticB init OK");
    }
}
public static void main(String[] args) {
        new Thread(()->{
            try {
                Class.forName("com.xxx.jvm.StaticA");
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        },"a").start();
    
        new Thread(()->{
            try {
                Class.forName("com.xxx.jvm.StaticB");
            } catch (ClassNotFoundException e) {
                e.printStackTrace();
            }
        },"b").start();
    }

两个线程利用反射调用两个类,但是两个类初始化的时候,也利用反射调用另外一个类,但是另外一个类还在初始化中,所以产生了死锁。


()中的static和static final

一般来说,static定义的变量是在初始化阶段进行赋值的,static final 定义的常量是在准备阶段进行赋值的,但是也有例外的
static修饰的静态变量,都是在初始化阶段进行赋值。
static final修饰的常量,要分情况:

  1. 对象:即左边的类型是一个对象,左边的类型是new或者是调用静态方法产生的;

public static final Object XXX = Object;

因为涉及到对象,所以在左边阶段无法完成static final的初始化,需要在初始化阶段完成。

  1. string字符串

字符串分两种情况

  1. 直接设置字符串常量

public static final String _s0 _= “helloworld0”; //在链接阶段的准备环节赋值

  1. new 一个对象设置字符串常量

public static final String _s1 _= new String(“helloworld1”); //在初始化阶段赋值

关于两种不同的字符串情况,要理解字符串在jvm内存中 的结构的时候才好理解

总结: 除了直接设置字符串外,其他的引用类型的常量都要在初始化的时候才可以完成赋值,直接设置字符串和基本类型一样,可以在准备阶段赋值


类的使用

在类经历过前三个步骤后,就可以正常的使用了,调用静态方法,new 一个对象。


类的卸载

在加载的时候,说过一句类加载器要和此类相关联,所以在了解类的卸载之前,先处理类、类加载器、实例对象直接的关系:
在这里插入图片描述

可以看到,class实例和加载器是双向关联的关系,若要卸载类,则要卸载 类的模板,卸载类的模板,则要卸载Class类的实例,要去除Class类的实例,就要让xxx对象的实例为null,并且将类加载器也卸载,但是通常类加载器是加载很多的类,所以无法因为一个类要 类型就将其卸载。

所以,类加载到内存中后,基本是无法卸载的,只能卸载类的实例对象

参考资料

宋红康老师的jvm课程
周志明老师的《深入理解java虚拟机–第三版》

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值