第一节:Class类加载过程
- 概述:
- 本文描述,使用class代指类和接口
- class文件,并非特指磁盘中的文件,包括二进制字节流
一、类加载的时机
- 概述:class被加载到被卸载,生命周期的7个阶段
1、生命周期
- 加载
- 验证
- 准备
- 解析
- 初始化
- 使用
- 卸载
- 某些情况下,初始化之后才解析(动态绑定)

2、立即初始化
- 对于初始化阶段,虚拟机规定了有且只有5种情况必须立即初始化(主动引用):
new、getstatic、putstatic和invokestatic四个字节码指令。- java中new对象,读取或设置一个class的静态字段(被final修饰的话,是在常量池和类无关,故除外)以及调用一个class的静态方法。
- 使用反射(reflect),class没有初始化,立即初始化
- 初始化class,发现其superclass未初始化,先初始化其父类
- jvm启动,必须有主类(包含main()的类),jvm先初始化主类
- 使用jdk1.7动态语言支持时,MethodHandle实例最后的解析结果
REF_getstatic、REF_putstatic、REF_invokestatic句柄时,立即初始化 - 接口和类区别?
- 类初始化时,要求其父类初始化,但是接口只有使用其父类的时候,才会初始化父类
第二节:class加载过程
- 详细介绍class的生命周期,及其作用

一、加载阶段
- 通过class的全限定名(包名+类名),读取二进制流(从本地,网络,jar,zip,jsp等等)
- 将class文件中的static存储结构转换为方法区的运行时数据结构
- 内存中生成一个java.lang.Class对象,作为方法区这个类内容的访问入口(hotSpot是放在方法区)
非数组class加载:
- 准确的说,是加载阶段获取二进制流,可控性最强。可以使用系统提供的class加载器,也可以使用自定义的class加载器(重写loadClass())。
数组类加载:
- 数组类本身不通过class loader创建,是jvm直接创建
- 数组类的元素类型(Element-Type,去掉所有维度)靠类加载器去创建
- 引用类型(数组内元素是引用类型):数组的组件类型(component Type,数组去掉一个维度)是引用类型,那就递归处理,该数组会在加载该组件类型的类加载器上做标记(class和类加载器必须一起确定唯一性)
- 非引用类型(数组内元素是基本数据类型):jvm把数组标记为该引导类加载器关联
- 数组类的可见性和他的组件类型的可见性一致。如果组件类型不是应用类型,那么默认是public
- 加载和验证阶段同步进行
二、验证阶段
概述:
- 确保加载的class文件,符合要求,不会危害jvm安全
- 如果加载了不符合jvm规范的字节流,会抛出
java.lang.verifyError错误 - 文件格式验证、元数据验证、字节码验证、符号引用验证
1、文件格式验证:
- 是否以魔数开头(
x0cafebabe) - 主次版本号是否在jvm处理范围内
- 常量池的常量中,是否有不被支持的常量类型
- 指向常量池的索引中,是否有指向不存在的常量或者不符合类型的常量
CONSTANT_utf8_info是否有不符合utf8编码的数据- Class文件各个部分的是否有被删除的内容或附加的信息(文件的完整性)
- 这个阶段是基于文件的二进制流进行的,然后流进入内存的方法区存储,后面的验证,均是基于内存的方法区存储结构进行,不在操作流
2、元数据验证(字节码描述信息进行语义分析,针对class相关内容):
- 这个class是否有父类(除Object外,所有的类都有父类)
- 这个class继承是否符合规范(继承final)
- 如果不是抽象类,是否实现了其父类或接口要求实现的方法
- class中的字段是否和父类产生冲突
3、字节码验证阶段(class的方法体校验,就是code部分)
- 保证任意时刻操作数栈的数据类型与指令代码序列都能配合工作
- 保证跳转指令不会跳转到方法体以外的字节码指令上
- 保证方法体内的类型转换有效
- 为了节约验证时间,jdk6+给Code的属性表添加了StackMapTable属性:
- 用来描述本地变量表和操作数栈应用的状态。
- 这样字节码验证期间,无需推导状态合法性,只需要检查 StackMapTable是否合法即可
4、符号引用验证(jvm将符号引用转化为直接引用)
- 这个阶段发生在连接的第三个阶段(解析阶段)
- 对class自身以外的信息(常量池中各种符号引用)进行匹配性校验
- 符号引用中,通过字符串描述能否通过全限定名找到对应的类
- 指定class中是否符合方法的字段描述符及简单名称所描述的方法和字段
- 符号引用中的类,字段,方法的访问性(
private、public、default、protected)是否可以被当前类访问
三、准备阶段
概述:
- 正式为class变量分配内存并赋值的阶段。这些变量内存都在方法区中:
- 类变量(static)不包括obj变量(obj变量内存在堆中)
- 这时候,给到的static 变量是默认值,而不是static属性值
public static int value=123- 这时候的value值是0,而不是123,这是因为class没有被初始化,没有赋值
- 把value赋值为123的putstatic指令是程序编译后,存放在()类构造器中
- 如果修饰符中存在final(即constantValue属性),那么准备阶段就会进行赋值
- 因为编译阶段final直接进入到了常量池,和类本身没有关系
四、解析
概述:将常量池中的符号引用,替换成直接引用的过程
- 符号引用:以一组符号来描述所引用的目标。可以是任何形式的字面量,只要能无歧义的定位即可。这时引用的目标不一定被加载在内存中
- 直接引用:直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用一定被加载在内存中
1、类或接口解析(当前类D,D中的引用符号N指向类或者接口C)
- C不是数组:会把N的全限定名传给D的类加载去加载C
- C是一个数组:
- 元素对象:使用(C不是数组的规则)加载
- 基本数据类型:jvm生成一个代表此数组维度和元素的数组对象
- 权限验证:如果前面两步没有问题,那么验证D对C访问权限
2、字段解析(类或接口用C表示,已经解析成功)
- 直接匹配:如果C本身包含了简单名称和字段描述都与目标相匹配的字段,直接返回字段的引用(其实就是C中有字段的详细信息,直接能找到)
- C实现接口:无法直接匹配的情况,在从下向上递归搜索接口和父接口
- C不是obj:无法直接匹配的情况,在从下向上递归搜索父类
- 否则查找失败,抛出
NoSuchFieldError异常 - 权限校验,成功返回引用后,如果没有访问权限,抛出
IllegalAccessError
3、类方法解析(static方法,已经解析成功的类或接口用C表示)
- 类方法和接口方法符号引用的常量类型定义是分开的,如果在类方法表中的class_index中发现是C是接口,那么抛出
IncompatibleClassChangeError异常 - 如果是类C,查找是否有简单名称和描述符都与目标匹配的方法,找到返回方法的直接引用。查找结束
- 否则,在类C的父类中递归查找,是否有简单名称和描述符都与目标匹配的方法,找到返回直接引用,查找结束
- 否则,在类C实现的接口及其父接口中递归查找,是否有简单名称和描述符相匹配的方法。如果有匹配的方法说明类C是个抽象类,查找结束,抛出
AbstractMethodError - 否则,查找方法结束,抛出
NoSuchMethodError - 如果能找到该方法,最后验证权限,没有权限抛出
IllegalAccessError异常
4、接口方法解析(解析成功的接口C)
- 在接口方法中发现class_index中索引是类而不是接口, 直接抛出
IncompatibleClassChangeError异常 - 接口C中是否有简单名称和描述符相匹配的方法,有返回直接引用
- 否则,在接口C的父接口中递归查找简单名称和描述符相匹配的方法,如果找到返回引用
- 最后没有找到,抛出
NoSuchMethodError异常 - 接口中方法都是public,不存在权限问题
五、初始化阶段
概述:
- 前面阶段除在加载阶段用户可以自己定义class loader外,其他均由jvm自动完成
- 初始化阶段,才正式开始执行class字节码文件,准备阶段给static赋了默认值,到了初始化阶段才进行
<clinit>(),真正给static属性复制
1、<clinit>()方法(编译器自动收集创建):
- class中的
static变量和static{}语句块合并产生 - 收集顺序在class源文件中出现的先后顺序决定的,也就决定了他们的初始化顺序和访问顺序
- 定义在
static变量之前的static{}可以给static变量赋值,但是无法访问
2、类<clinit>()和实例构造器<init>()
- 子类
<clinit>()不要显示的调用父类的<clinit>(),jvm保证在调用子类之前先调用父类 - jvm中执行的第一个
<clinit>()肯定是java.lang.Object
3、父类的<clinit>()优先执行,也就是父类的static优先于子类执行
4、如果类和接口中不存在static相关内容,那么也就不会有<clinit>()
5、Node:接口的<clinit>():
- 接口中不存在
static{} - 接口的
<clinit>()只存在静态变量,执行<clinit>()不需要优先执行父类的<clinit>() - 接口的实现类在初始化时,也不会执行接口的
<clinit>()
6、类的<clinit>()加锁、同步
- jvm保证类的
<clinit>()在多线程的情况下被正确枷锁、同步 - 多线程并发初始化一个类,那么jvm只会让一个线程去初始化,其他线程阻塞等待,直到
<clinit>()方法执行结束
第三节:类加载器
- 概述:在class生命周期中的加载阶段。jvm规范,通过一个类的全限定名来获取类描述该类的二进制字节流。实现这个动作的就是类加载器
一、类与类加载器
1、类加载器用于实现类的加载动作
2、类和类加载器两者共同确定该类在jvm中的唯一性
- 同一个class文件被同一个jvm中的两个不同加载器加载,那么生成的这两个类也是不相等的
- 判断两个类相等
- Class对象的equals()、isAssignableFrom()方法、isInstance()
- instanceOf关键字做对象所属关系判定
二、双亲委派模式

1、jvm角度看类加载器
- 启动类加载器(bootstrap ClassLoader),这个类加载器使用C++实现(jvm自身的一部分)
- 其他加载器,使用java实现,独立于jvm,并且全部继承
java.lang.ClassLoader
2、开发使用细分:
- 启动类加载器(bootstrap ClassLoader)
- 负责将
<JAVA_HOME>\lib目录中,并且被jvm识别的(仅按照文件名识别)类库加载到jvm中 - 启动类加载器没有办法直接引用,只能在自定义ClassLoader时直接用null代替
- 负责将
- 拓展类加载器(Extension ClassLoader)
- 由
sun.misc.launcher$ExtClassLoader实现 - 负责加载
<JAVA_HOME>\lib\ext目录,或者被java.ext.dirs系统变量指定的路径中的所有类库,开发者可以直接使用拓展类加载器
- 由
- 应用程序类加载器(Application ClassLoader)
- 由
sun.misc.launcher$App-ClassLoader实现 - 这个类加载器是ClassLoader中的
getSystemClassLoader()方法的返回值,所以也叫系统类加载器 - 负责加载用户类路径上的所指定的类库,开发者可以直接使用该类库,如果没有自定义ClassLoader那么该类库也是默认类加载器
- 由
3、双亲委派模型
- 除启动类加载器外,其余所有ClassLoader都有自己的父类加载器
- 这里的父类加载器和子类加载器并不是继承关系,而是以组合关系复用父类加载器的代码
- 双亲委派模型的工作流程:
- 如果一个类加载器收到了类加载的请求,他首先不会自己尝试去加载这个类,而是把这个请求委派给他的父类加载器去完成。
- 每一个层次的类加载器都是如此,最终请求传递给最顶层的启动类加载器。
- 只有当父类加载器反馈自己无法这个请求(搜索范围内没有这个类)子加载器才会尝试去加载这个类。
- 双亲委派的意义:
- 类和类加载器两者共同确定该类在jvm中的唯一性
- 如果是不同的加载器去加载同一个class会在方法区中映射出不同的class对象,产生二义性,
- 除
java.lang.Object自身外,任何类都继承object,但实际方法区中只有唯一一个object这样的class对象存在
- 双亲委派代码的实现,都集中在
java.lang.ClassLoader的loadClass()方法中
protected Class<?> loadClass(String name, boolean resolve)throws ClassNotFoundException{
Class<?> c = findLoadedClass(name);
if (c == null) {
try {
if (parent != null) {
c = parent.loadClass(name, false);
} else {
c = findBootstrapClassOrNull(name);
}
} catch (ClassNotFoundException e) {
// ClassNotFoundException thrown if class not found
// from the non-null parent class loader
//如果父类抛出ClassNotFoundException,说明父类没有找到,调用子类
}
//如果上面走到了catch那么这个c就会是null,也就是要调用自己的classLoader
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}