总体上说,虚拟机把描述类的数据,从字节码文件加载到内存,然后进行数据校验、转换解析、初始化,最终形成可以被寻积极使用的Java类型,这就是虚拟机的类加载机制。
与C/C++在编译时,需要连接不同,Java语言里,类型的加载、连接和初始化都是在程序运行期间完成。虽然增加了性能开销,但有更高灵活性。
Java的动态扩展特性,依赖运行时的动态加载和动态连接。
例如,面向接口编程,可以在运行时再指定实际的实现类;用户可以通过自定义类加载器,在运行时,从网络或其他地方加载一个二进制流,作为程序代码的一部分。
类的生命周期
类从被加载到虚拟机内存中开始,到卸载出内存为止,其生命周期,如下图
加载、验证、准备、初始化、卸载,这5阶段的顺序是确定的。
解析阶段不一定,某些情况下,可以在初始化之后再开始。这是为了支持Java语言的运行时绑定
虚拟机规范严格规定,5种情况,必须立即类初始化:
遇到
new
,getstatic
,putstatic
,invokestatic
字节码指令,若类未曾初始化,则先初始化使用java.lang.reflect包的方法,反射调用类,若类未曾初始化,先初始化
- 初始化一个类时,若父类未曾初始化过,先初始化父类
- 虚拟机启动时,用户需要指定一个要执行的朱磊,虚拟机会先初始化主类
- 使用JDK 1.7动态语言支持时,若java.lang.invoke.MethodHandle实例最后的解析结果
REF_getStatic
,REF_putStatic
,REF_invokeStatic
的方法句柄,且句柄对应的类未曾初始化过,则先初始化
加载(Loading)
加载是类加载(Class Loading)过程的一个阶段。
加载完成3件事情
- 通过类的全限定名来获取定义此类的二进制字节流
- 将这个二进制字节流的静态存储结构转化为方法区的运行时数据结构
- 在内存生成代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口
HotSpot虚拟机的java.lang.Class对象,放在方法区Method Area,不是堆Heap
连接(Linking)
加载阶段与连接阶段的部分内容,交叉进行,加载阶段未完成,连接阶段可能已经开始
验证(Verification)
连接阶段的第一步,目的是为了确保class文件的字节流中包含的信息,符合当前虚拟机的要求,而且,不会危害虚拟机自身安全
大致完成4个阶段的检验
- 文件格式验证
- 是否符合字节码文件格式的规范,并能被当前版本的VM处理
- 基于二进制流进行,后面的验证基于方法区的存储结构,不再直接操作字节流
- 验证通过后才会将二进制流存入JVM内存的方法区
- 元数据验证
- 对字节码描述的信息进行语义分析,保证其符合规范
- 字节码验证
- 通过数据流和控制流分析,确定程序语义是合法、符合逻辑的
- 第二阶段,对元数据的数据类型做完校验后,这个阶段将校验分析类的方法体
- 符号引用验证
- 发生在虚拟机将要把符号引用转化为直接引用的时候,即解析阶段中发生
- 对类自身以外的信息进行匹配性校验
- 目的是保证解析动作能正常执行
准备(Preparation)
正式为类变量分配内存,并设置类变量初始值的阶段,这些变量所使用的内存,都将在方法区中分配
即static修饰的变量,初始值为各个类型的0值
如果是final修饰的类变量,会直接生成ConstantValue属性。在准备阶段,虚拟机会根据ConstantValue为变量赋值
解析(Resolution)
虚拟机将常量池内的符号引用,转换为直接引用的过程
符号引用(Symbolic References)
:class文件结构规范定义了的引用
-
直接引用(Direct References)
- 直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。 与虚拟机的内存布局相关
分为:
- 类或接口解析
- 字段解析
- 类方法解析
- 接口方法解析
初始化(Initialization)
类加载过程的最后一步,这一阶段,才真正开始执行类中定义的Java代码(或者说字节码)
准备阶段,变量已经被清零,有了初始值;初始化阶段,则根据我们定义的Java代码逻辑去初始化类变量和其他资源
从另外的角度表达,初始化阶段,是执行类构造器的
<clinit>
方法的过程
- 虚拟机会保证一个类的方法在多线程环境中被正确的加锁、同步;如果多个线程同时去初始化一个类,那么只有会有一个线程执行方法,其他线程阻塞
类加载器
-
作用
- 类加载器实现 类的加载动作,同时用于确定一个类。 确定类的唯一性
- 对于任意一个类,都需要由加载它的 类加载器和这个 类本身一同确立其在Java虚拟机中的 唯一性。即使两个类来源于同一个Class文件,只要加载它们的类加载器不同,这两个类就不相等
类加载器
从虚拟机的角度来讲,只存在2种不同的类加载器
- 启动类加载器(Bootstrap ClassLoader)
- C++实现(HotSpot而言),是虚拟机自身一部分
- 所有其他加载器
- Java实现,独立于虚拟机外
- 全继承自java.lang.ClassLoader
从程序员角度讲
- 启动类加载器(Bootstrap ClassLoader)
- 负责将存放在\lib目录(或者-Xbootclasspath参数指定的路径)中的类库,加载到虚拟机中。其无法被Java程序直接引用。
- 扩展类加载器(Extention ClassLoader)
- sun.misc.Launcher$ExtClassLoader实现,负责加载\lib\ext目录(或者java.ext.dirs系统变量指定的路径)中的所有类库,开发者可以直接使用。
- 应用程序类加载器(Application ClassLoader)
- 由sun.misc.Launcher$APPClassLoader实现。负责加载用户类路径(ClassPath)上所指定的类库。
- 由getSystemClassLoader()方法返回,所以又叫系统类加载器
类加载机制
双亲委派机制(Parents Delegation Model)
- 除了顶层的Bootstrap ClassLoader外,其他加载器都应当有自己的父类加载器
- 加载器之间的父子关系,通过组合关系复用
双亲委派工作过程
如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器完成。
每个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到顶层的启动类加载器中
只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围没有找到所需的类)时,子加载器才会尝试自己去加载。
代码实现集中在java.lang.ClassLoader的loadClass()方法中
//JDK 1.8 中的loadClass()方法
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);
// 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;
}
}
为什么要使用双亲委派模型,组织类加载器之间的关系?
Java类随着它的类加载器一起具备了一种带优先级的层次关系。比如java.lang.Object,它存放在rt.jar中,无论哪个类加载器要加载这个类,最终都是委派给启动类加载器进行加载,因此Object类在程序的各个类加载器环境中,都是同一个类。
如果没有使用双亲委派模型,让各个类加载器自己去加载,那么Java类型体系中最基础的行为也得不到保障,应用程序会变得一片混乱。