类加载机制 读书笔记
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的java类型,这是虚拟机类加载机制。
1.类加载时机
类从被加载到内存开始,到卸载出内存为止,整个生命周期:
加载 ----> 验证 ----> 准备 ----> 解析 ----> 初始化 ----> 使用 ----> 卸载
(Loading) (Verfication) (Preparation) (Resolution) (Initialization) (Using) (Unloading)
验证、准备、解析三部分合称为连接
解析阶段位置不固定,可能在初始化之后开始,为了支持java语言运行时绑定。
有且仅有5中情况下必须对类进行初始化:
1.遇到new、getstatic、putstatic、invokestatic时,若类没有初始化,则进行初始化。
场景:使用new关键字实例化对象的时候、读取或设置一个类的静态字段的时候(final修饰)、调用类的静态方法的时候
2.使用java.lang.reflect包的方法对类反射调用的时候,类没有初始化,则进行初始化
3.初始化一个类时,若其父类没有初始化,则先初始化其父类
4.启动虚拟机,指定一个主类,先初始化主类
5.使用JDK1.7动态语言支持时,一个java.lang.invoke.MethodHandle实例最后解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic方法的句柄,方法句柄对应的类没有被初始化过,触
发初始化
除了以上5点,其余引用类的方法都不会触发初始化,称为被动引用。
1.通过子类调用父类的静态字段,不会导致子类初始化。对于静态字段,只有直接定义这个字段的类才会被初始化,通过子类调用父类里的静态字段,只会触发父类的初始化而不会触发子类初始化
2.例如调用com.brucekun.classloading.SuperClass类生成数组,类中定义静态字段,这回触发[Lcom.brucekun.classloading.SuperClass的类初始化阶段
该类是由虚拟机自动生成、直接继承与java.lang.object的子类,创建动作可由字节码指令newarray触发。该类代表一个元素类型为com.brucekun.classloading.SuperClass的一维数组,数组
中所有属性和方法都是现在这个类中。java对于数组的访问比C/C++相对安全是因为该类封装了数组元素的访问方法,而C/C++直接翻译对数组指针的引用。
3.常量在编译阶段会存入调用类的常量池中(常量传播优化),本质上并没与直接引用到定义常量的类,所以不会触发定义常量类的初始化。
接口不能使用static{}代码块,编译器会为其生成<clinit>()类构造器,用于初始化接口中管所定义的成员变量。
接口在初始化时与类初始化的唯一不同是,接口在初始化时并不需要父接口全部完成初始化,只有用到父接口的时候才初始化。
2.类加载过程
1.加载
加载阶段完成三件事情:
(1)通过类的全限定名来获取定义此类的二进制字节流
(2)将字节流所代表的静态存储结构转化为方法区运行时数据结构
(3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
---------> 类的二进制字节流 -----------------------------> 方法区运行时数据结构 --------->在内存中生成一个代表这个类的java.lang.Class对象
类的全限定名 字节流代表的静态存储结构 作为该类的各种数据访问入口
加载阶段既可以用系统提供的类引导类,也可以由用户自定义类加载器去完成。
数组本身不通过类加载器创建,由Java虚拟机直接创建,但是数组类的元素类型要靠类加载器创建
2.验证
验证是为了确保Class文件的字节流中包含的信息符合当前虚拟机的要求,并且不会威胁自身安全。
验证阶段大致分为四个检验动作:
1.文件格式验证:主要目的是保证输入的字节流正确的解析并存储于方法区内,格式上符合描述一个Java类型信息的要求。
基于二进制字节流,下面三个阶段基于存储结构,不直接操作字节流
2.元数据验证:对字节码描述的信息进行语义分析,对元数据信息进行校验,保证符合Java语言规范
3.字节码验证:通过数据流和控制流分析,确定程序语义是合法的、符合逻辑的
如果一个类方法体的字节码没有通过字节码验证,那一定是有问题的;如果一个方法体通过了字节码验证,不一定安全。
通过程序去校验程序逻辑无法做到绝对准确
4.符号引用验证:发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作在解析阶段中发生。
符号引用对类自身以外的信息进行匹配校验。
如果代码已经被使用和校验过,不必重新校验。
3.准备
为类变量分配内存并设置变量初始值,变量使用的内存在方法区中分配。
这时候的变量分配只包括类变量(static修饰的变量),不包括实例变量,实例变量在对象实例化时在堆中进行分配。
初始值一般情况下是设置数据类型的零值;而如果类字段属性表存在ConstantValue属性,则准备变量就会被初始化为ConstantValue的指定值。
4.解析
解析是将常量池中符号引用转化为直接引用的过程。
符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要能无歧义的定位到目标即可。
直接引用:直接指向对象的指针、相对偏移量、简介定位到目标的句柄。
如果有了直接引用,那么引用目标一定已经在内存中存在。
5.初始化
初始化阶段是执行类构造器<clinit>()方法的过程
1.<clinit>()方法由编译器自动收集类中所有变量的赋值动作和静态语句块(static{}块)中的语句合并产生的。
静态语句快只能访问到定义在静态语句块之前的变量,定义在它之后的变量,静态语句块可以赋值,不能访问。
2.<clinit>()方法和类的构造函数不一样,它不需要显示调用父类构造器,虚拟机保证在子类<clinit>()方法执行前,父类的<clinit>()方法执行完毕
那么第一个执行<clinit>()方法的是java.lang.Object
3.父类<clinit>()方法先执行,说明父类的静态语句块优先于子类静态语句块给变量赋值
4.若类中没有静态语句快也没有赋值操作,可以没有<clinit>()方法
5.对于接口,执行接口<clinit>()方法时,不需要先执行父接口的<clinit>()方法,当父接口中定义的变量被使用时,才执行父接口的<clinit>()方法
接口的实现类在初始化时不执行接口的<clinit>()方法
6.虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确加锁、同步
多个线程同时初始化一个类,只有一个线程会执行<clinit>()方法,其他线程进入阻塞状态
同一个类加载器下,一个类型只会初始化一次
3.类加载器
1.类和类加载器
类加载阶段----根据一个类的全限定名获取二进制字节流 这个动作放在虚拟机外部实现,实现这个动作的模块成为类加载器
对于任意一个类,这个类在虚拟机中的唯一性都需要该类本身和它的类加载器一同确立。
(比较两个类是否相等,只有在两个类由同一个类加载器加载的前提下才有意义)
2.双亲委派模型
存在两种加载器:
1.启动类加载器:C++语言实现,虚拟机自身一部分
2.其他的类加载器:Java语言实现,独立于虚拟机外部,全部继承自java.lang.ClassLoader
启动类加载器:
将存放在<JAVA_HOME>/lib目录中或者被-Xbootclasspath参数指定路径的可以被虚拟机识别的类库加载到虚拟机内存中
Java程序无法直接引用启动类加载器,编写自定义加载器时,如果需要把加载请求委派给引导类加载器,直接返回null
扩展类加载器:
加载存放在<JAVA_HOME>/lib/ext目录中或被java.ext.dirs系统变量指定的路径中的类库
应用程序类加载器:
加载用户路径(ClassPath)上指定的类库
若没有指定类加载器,应用程序类加载器就是默认的类加载器
类加载器的双亲委派模型:
启动类加载器
(Bootstrap ClassLoader)
↑
丨
扩展类加载器
(Extension ClassLoader)
↑
丨
应用程序类加载器
(Application ClassLoader)
↗ ↖
自定义类加载器 自定义类加载器
(User ClassLoader) (User ClassLoader)
除了顶层的启动类加载器之外,其余的类加载器都应该有自己的父类加载器
类加载器的父子关系以组合关系来复用父类加载器代码,不以继承关系实现
双亲委派模型过程:
一个类加载器收到了加载请求,自己不会去尝试加载这个类,而是把这个请求委派给父类加载器去完成,因此所有加载请求都被传送到启动类加载
器中,当父加载器反馈自己无法加载这个请求时,子加载器才会尝试自己去加载。
实现双亲委派的代码都集中在java.lang.ClassLoader的loadClass()方法中
1.首先检查请求的类是否被加载过
2.若没有被加载过,则调用父类加载器的loadClass()方法,父类加载器为空则使用启动类加载器加载
3.若父类加载器不能加载,则调用自己的findClass()方法进行加载
一般将自己的类加载逻辑写入findClass()中