JVM - 类加载
概述
Java虚拟机(JVM)不与包括Java语言在内的任何程序语言绑定,只与“Class文件”(字节码)这种特定的二进制文件格式关联。而Java虚拟机可以运行在各种不同硬件平台和操作系统上,这也是说Java语言跨平台的原因。虚拟机与程序语言的关系如下图:
Class文件结构定义
ClassFile {
u4 magic; //魔数,代表这是一个可以被虚拟机接收的Class文件,而不是靠文件扩展名确定:0xCAFEBABE
u2 minor_version;//次版本号
u2 major_version;//主版本号;高版本JDK可以向下兼容以前版本的class文件,大于虚拟机版本号的class文件(高版本编译器生成的class文件)将执行不了
u2 constant_pool_count;//常量池的数量
cp_info constant_pool[constant_pool_count-1];//常量池,常量池里面放的是字面量和符号引用,字面量就是一些如文本字符串,声明为final的常量值等,符号引用主要包括package,类和接口名,字段名,方法类型等等
u2 access_flags;//Class 的访问标记
u2 this_class;//当前类
u2 super_class;//父类
u2 interfaces_count;//接口
u2 interfaces[interfaces_count];//一个类可以实现多个接口
u2 fields_count;//Class 文件的字段属性
field_info fields[fields_count];//一个类可以有多个字段
u2 methods_count;//Class 文件的方法数量
method_info methods[methods_count];//一个类可以有个多个方法
u2 attributes_count;//此类的属性表中的属性数
attribute_info attributes[attributes_count];//属性表集合
}
//字段表,方法表结构:
field_info/method_info{
u2 access_flags;//作用域,如private
u2 name_index;//常量池引用,表示字段/方法名
u2 descriptor_index;//常量池引用,字段和方法描述
u2 attributes_count;//字段/方法的额外属性
attribute_info attributes[attributes_count];
}
类加载
类的生命周期
加载
在加载阶段,虚拟机要完成三件事:
- 通过一个类的全限定名来获取定义此类的二进制字节流(有多种方式,例如从zip包读取,如jar,war,jsp文件生成,运行时计算生成等)
- 将这个字节流所代表的静态存储结构转化为方法区运行时数据结构
- 在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
验证
- 文件格式验证,例如魔数验证,版本号大小验证等等
- 元数据验证
- 是否有父类,是否继承了不允许继承的类如final类等等
- 不是抽象类,是否实现了父类中要求实现的方法
- 是否与父类矛盾
...
- 字节码验证,通过数据流,控制流分析,确定程序语义合法。
- 符号引用验证,如符号引用通过全限定名能否找到对应类等等
准备
为类中定义的变量(即静态变量,被static修饰的变量)分配内存并设置类变量初始值的阶段。
注意此时的变量初始值是数据变量类型的“零值”,例如:
public static int value = 123;
此时value的值为0,因为目前还没有执行任何java方法(赋值为123是执行类构造器<clinit>()
方法做的),但是如果是被final
修饰,在准备阶段就会被赋值为ConstantValue属性指定的初始值。
解析
Java虚拟机将常量池内的符号引用,替换为直接引用(指针,相对偏移量等)的过程。
初始化
执行类构造器<clinit>()
方法。<clinit>()
方法是Javac编译器的自动生成物和实例构造器<init>()
不同。
- 初始化顺序是由源文件初始化顺序决定
- Java虚拟机保证父类的
<clinit>()
会比子类的<clinit>()
先执行 - 如果一个类没有静态语句块或者变量的赋值,编译器可以不为该类生成
<clinit>()
方法 - 线程安全的,只有一个线程能执行这个类的
<clinit>()
方法
卸载
卸载类,即该类Class对象能成功被GC
卸载类需要满足三个要求:
- 该类的所有实例对象都被GC
- 该类没有在任何地方被引用,即其Class对象不可达
- 该类的类加载器的实例不可达,即该类的ClassLoader不可达
类加载器
比较两个类是否相等
任何一个类,都是由该类本身和它的类加载器一起确立其在JVM中的唯一性。 比较两个类是否相等(equals()
, isInstance()
,isAssignableFrom()
),只有他们在同一类加载器下加载的前提才有意义,否则必然不相等。
Java的类加载器
- 启动类加载器 Bootstrap ClassLoader
使用C++开发,加载<JAVA_HOME>/lib目录中的jar 和-Xbootclasspath
参数指定的类 - 扩展类加载器 Extension ClassLoader
加载<JAVA_HOME>/lib/ext目录的jar 和使用-Djava.ext.dir
指定的类 - 应用程序加载器 App ClassLoader
加载用户类路径下的类 - 用户自定义加载器 User/Custom ClassLoader
双亲委派模型 Parent Delegation Model
双亲委派模型是类加载的过程,总结两句话就是自底向上检查类是否被加载,自顶向下尝试加载类。如下图:
源码:
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class<?> c = findLoadedClass(name);
if (c == null) {
long t0 = System.nanoTime();
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
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
long t1 = System.nanoTime();
c = findClass(name); // You will probably want to override this method when you instantiate a class loader subclass in your application -- oracle
// this is the defining class loader; record the stats
sun.misc.PerfCounter.getParentDelegationTime().addTime(t1 - t0);
sun.misc.PerfCounter.getFindClassTime().addElapsedTimeFrom(t1);
sun.misc.PerfCounter.getFindClasses().increment();
}
}
if (resolve) {
resolveClass(c);
}
return c;
}
}
双亲委派小结
结论1 任何一个类的加载最终都委托到启动类加载器
结论2 每个加载器都有自己加载的类范围,如启动类加载器只加载JDK核心库,因此并不是父类加载器都会加载成功,父类加载器无法加载时,此时会自己加载。
双亲委派的好处
保证所有的类都尽可能地由顶层类加载器加载,保证了加载类的唯一性和类的实现关系;也保证了Java的核心API不被窜改。