类加载机制
概述:
类的加载分为加载,链接,初始化。
加载
所谓加载,就是将java类的字节码文件加载到机器内存中,并在内存中构建出java类的原形——类模板对象。类模板对象其实是java类在JVM内存中的一个快照,JVM将从字节码文件中解析出的常量池,类字段,类方法等信息存储到类模板中,这样JVM在运行期便能通过类模板而获取java类中的任意信息。
加载完成的操作
加载阶段:查找并加载类的二进制数据,生成Class的实例
在加载类时,JVM必须完成以下3件事情:
- 通过类的全名,获取类的二进制数据流
- 解析类的二进制数据流为方法区内的数据结构(java类模型)
- 创建java.lang.Class类的实例,表示该类型,作为方法区这个类的各种数据的访问入口
二进制流的获取方式
对于类的二进制数据流,虚拟机可以通过多种途径产生获得。(只要读取的字节码符合JVM规范即可)
- 虚拟机可能通过文件系统读入一个class后缀的文件**(最常见)**
- 读入jar,zip等归档数据包,提取类文件
- 事先存放在数据库中的类的二进制数据
- 使用类似于HTTP之类的协议通过网络进行加载
- 在运行时生成一段Class的二进制信息等
在获取到类的二进制信息后,java虚拟机就会处理这些数据,并最终转为一个java.lang.Class的实例。
如果输入数据不是ClassFile的结构,则会抛出ClassFormatError。
1.类模型位置
加载的类在JVM中创建相应的类结构,类结构会存储在方法区(JDK1.8之前:永久代。JDK1.8之后:元空间)
2.Class实例的位置
类将.class文件加载至元空间后,会在堆中创建一个java.lang.Class对象,用来封装类位于方法区内的数据结构,该Class对象是在加载类的过程中创建的,每个类对应有一个Class类型的对象。
说明
Class类的构造方法是私有的,只有JVM能够创建
java.lang.Class实例是访问类型元数据的接口,也是实现反射的关键数据、入口。通过Class类提供的结构,可以获得目标类所关联的.class文件中具体的数据结构:方法、字段等信息。
数组类的加载
创建数组类的情况比较特殊,数组类本身不是由加载器负责创建,而是由JVM在运行时根据需要而直接创建的,但数组的元素类型仍然需要依靠类加载器去创建。
1.如果数组的元素类型是引用类型,那就遵循定义的加载过程递归加载和创建数组的元素类型
2.JVM使用指定的元素类型和数组维度来创建新的数组类。
链接
1.验证
当类加载到系统后就开始链接操作,验证是链接操作的第一步。
验证的目的是保证加载的字节码是合法,合理并符合规范的。
- 格式检查:
- 魔术检查:cafe babe
- 版本检查:父版本子版本是否一致
- 长度检查:字节码长度
- 语义检查:
- 是否继承final
- 是否有父类
- 抽象方法是否有实现
- 字节码验证:
- 跳转指令是否指向正确位置
- 操作数类型是否合理
- 符号引用验证
- 符号引用的直接引用是否存在
说明:
验证的内容包含了类数据信息的格式验证,语义检查,字节码验证,以及符号引用验证等。
其中格式验证会和加载阶段一起执行,验证通过之后,类加载器才会成功将类的二进制数据信息加载到方法区中。
格式验证之外的验证操作将会在方法区中进行。
2.准备
准备阶段为类的静态变量分配内存,并将其初始化为默认值
当一个类验证通过时,虚拟机就会进入准备阶段。在这个阶段,虚拟机就会为这个类分配相应的内存空间,并设置默认初始值。
说明:
java并不支持boolean类型,对于boolean类型,内部实现是int,由于int的默认值是0,对应的boolean的默认值就是false。
注意:
1.这里不包含基本数据类型的字段用static final修饰的情况,因为final在编译的时候就分配了,准备阶段会显示赋值。
2.准备阶段不会为实例变量分配初始化,类变量会分配在方法区中,而实例变量是随着对象一起分配到java堆当中
3.准备阶段不会像初始化阶段中那要会有初始化或者代码被执行。
4.对于String来说,如果使用字面量的方式赋值,使用static final修饰的话,则显示赋值通常是在链接阶段的准备环节进行
3.解析
解析环节是将类,接口,字段和方法的符号引用转为直接引用。
概述:
符号引用就是一些字面量的引用,和虚拟机的内部数据结构和内存布局无关。可以理解为:在Class类文件中,通过常量池进行了大量的符号引用。但是在程序实际运行环境中,需要将符号引用转换为直接引用调用目标信息的位置。
初始化
初始化阶段为类的静态变量赋予正确的初始值。
概述:
类的初始化是类加载的最后一个阶段,如果前面阶段都没有问题,那该类可以顺利加载到系统中。此时,类才会开始执行java字节码。(到了初始化阶段。才真正开始执行java代码)
初始化阶段的工作是执行类的初始化方法:()方法
说明:
在加载一个类之前,虚拟机总是会试图加载该类的父类,因此父类的总是在子类之前被调用。也就是说,父类的static块优先级高于子类
并不是每个类都会产生()初始化方法,以下要求将不会产生()初始化方法
- 一个类中没有声明任何的类变量,也没有静态代码块时
- 一个类中声明类变量,但是没有明确使用类变量的初始化语句以及静态代码块来执行初始化操作时
- 一个类中包含static final修饰的基本数据类型的字段,这些类字段初始化语句采用编译时常量表达式
()的线程安全性
虚拟机会保证一个类的()方法在多线程环境中被正确的加锁,同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的()方法,其他线程都需要阻塞等待,直到活动线程执行()方法完毕
正是因为()是带锁线程安全的,因此,如果一个类的()方法中有耗时很长的操作,就可能导致多个线程阻塞,引发死锁。
如果之前的线程成功加载了类,则等在队列中的线程就没有机会再执行()方法了,当需要使用这个类时,虚拟机会直接返回给它已经准备好的信息。
主动使用
Class只有在必须要首次使用的时候才会被加载,java虚拟机不会无条件的加载Class类型,java虚拟机规定,一个类或接口在初次使用前,必须要进行初始化。
1.当创建一个类的实例时,比如:new,反射,克隆,序列化。
2.当调用类的静态方法
3.当使用类,接口的静态字段(final修饰的除外)
4.当使用java.lang.reflect包中的方法反射类的方法时
5.当初始化子类时,如果发现其父类还没有进行初始化,则需要执行父类的初始化,不适用接口
6.如果一个接口定义了default方法,那么直接实现或者间接实现该接口的类的初始化,该接口要在其之前被初始化
7.当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类。
被动使用
除了主动使用的情况剩余的情况都是被动使用。被动使用不会引起类的初始化
1.当访问一个静态字段时,只有真正声明这个字段的类才会被初始化。
当通过子类引用父类的静态变量,不会导致子类初始化
2.通过数组定义类引用,不会触发此类的初始化
3.引用常量不会触发此类或接口的初始化,因为常量在链接准备阶段已经显示赋值
4.调用ClassLoader类的loadClass()方法加载一个类,并不是对类的主动使用,不会导致类的初始化。
启动类加载器(Bootstrap ClassLoader)
1.启动类加载器使用C/C++语言实现,嵌套在JVM内部
2.启动类加载器用来加载java的核心库
3.没有父类加载器
4.加载扩展类和应用程序类加载器,并制定为他们的父类
5.出于安全考虑,启动类加载器只加载为包名为java、javax、sun等开头的类.
扩展类加载器(Extension ClassLoader)
1.Java语言编写的
2.派生于ClassLoader类
3.父类加载器为启动类加载器
应用程序类加载器(App ClassLoader)
1.应用程序类加载器由java语言编写
2.派生于ClassLoader类
3.父类加载器为扩展类加载器
4.应用程序类加载器是程序中默认的类加载器,一般来说,java应用的类都是又应用程序类加载器完成加载
双亲委派
概念:
Java虚拟机对class文件采用的是按需加载的方式,也就是说当需要使用该类时才会将它的class文件加载到内存生成class对象。而且加载某个类的class文件时,Java虚拟机采用的是双亲委派模式,即把请求交由父类处理,它是一种任务委派模式。
工作原理:
1.一个类进行了加载,该类不会马上加载,而是去找该类加载器的父类进行去执行
2.如果该类加载器的父类还存在父类加载器,则进一步向上委托,依次递归,请求最终到达顶层的启动类加载器
3.如果父类加载器可以完成类的加载则加载,如果不可以则依次向下委托并进行请求加载。
特点:
1.避免类加载重复
2.保护程序安全,防止核心API被随机篡改