类加载机制
Posted wbxk
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了类加载机制相关的知识,希望对你有一定的参考价值。
虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型。
Java类型的加载、连接和初始化过程都是在程序运行期间完成的。这种策略虽然会令类加载时稍微增加一些性能开销,但是会为Java应用程序提供高度的灵活性,Java语言的动态扩展特性就是依赖运行期动态加载和动态连接实现的。
类加载时机
类从被加载到虚拟机内存中开始,直到卸载出内存为止,它的整个生命周期包括了这7个阶段:加载、验证、准备、解析、初始化、使用、卸载。
其中,验证、准备和解析这三个部分统称为连接(linking)。
其中,加载、验证、准备、初始化和卸载这五个阶段的顺序是确定的,类的加载过程必须按照这种顺序按部就班的“开始”(仅仅指的是开始,而非执行或者结束,因为这些阶段通常都是互相交叉的混合进行,通常会在一个阶段执行的过程中调用或者激活另一个阶段),而解析阶段则不一定,它在某些情况下可以在初始化阶段之后再开始,这是为了支持Java语言的运行时绑定(也称动态绑定或晚期绑定)。
主动引用
主动引用的情况下必须对类进行初始化。
Java虚拟机规范没有强制约束执行“加载”的时机,但是对于“初始化”,严格规定了5中情况下必须立即对类进行“初始化”(加载、验证、准备当然需要在此之前开始):
(1)遇到new、getstatic、putstatic、invokestatic这四条字节码指令时,如果类没有进行过初始化,则需要先出触发其初始化。
常见Java代码场景:使用new关键字实例化对象、读取或设置一个类的静态字段(被final修饰的静态字段除外)、调用一个类的静态方法。
(2)使用java.lang.reflect包的方法对类进行反射调用的时候,如果类没有进行过初始化,则需要先出触发其初始化。
(3)当初始化一个类的时候,发现其父类没有初始化,则需要先触发其父类的初始化。
注意,接口比较特殊,一个接口在初始化时,并不要求其父接口全部完成初始化,只有在真正使用到父接口的时候(如引用接口中定义的常量)才会初始化。
(4)当虚拟机启动时,用户需要指定一个要执行的主类(包含main方法的那个类),虚拟机会先初始化这个主类。
(5)当使用JDK1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后解析结果为REF_getStatic、REF_putStatic、REF_invokeStatic的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
被动引用
“有且仅有”上述五种场景才会触发类的初始化,这五种场景中的行为被称为对一个类进行主动引用,除此之外,其他所有引用类的方式都不会触发初始化,称为被动引用。
被动应用的场景:
(1)通过子类引用父类的静态字段,不会导致子类初始化(当访问一个静态变量时,只有真正声明这个静态变量的类才会被初始化)。
(2)通过数组定义来引用类,不会触发此类的初始化。
(3)常量在编译阶段会存入调用类的常量池中,本质上并没有直接引用到定义常量的类,因此不会触发该类的初始化。
类加载过程
一、加载
在加载阶段,虚拟机需要完成以下三件事情:
(1)通过一个类的全限定名来获取定义此类的二进制字节流。
(2)将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
(3)在内存中生成一个代表这个类的java.lang.Class对象,作为方法区这个类的各种数据的访问入口。
加载阶段与连接阶段的部分内容(如一部分字节码文件格式验证动作)是交叉进行的,加载阶段尚未完成,连接阶段可能已经开始,但这些夹在加载阶段之中进行的动作,仍然属于连接阶段的内容,这两个阶段的开始时间仍然保持这固定的先后顺序。
二、验证
验证是连接的第一步,目的是为了确保Class文件字节流中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。
验证阶段大致分为4个阶段的检验动作:
(1)文件格式验证
这个阶段的验证是基于二进制字节流进行的,只有通过了这个阶段的验证,字节流才会进入内存的方法区中进行存储,后面的3个阶段都是基于方法区的存储结构进行的,不会再直接操作字节流。
(2)元数据验证
对字节码描述的信息进行语义分析,保证其描述的信息符合Java语言规范的要求。
(3)字节码验证
对类的方法体进行校验分析,保证被校验类的方法在运行时不会做出危害虚拟机安全的事件。
(4)符号引用验证
发生在虚拟机将符号引用转化为直接引用的时候,这个转化动作将在“解析”阶段中发生。符号引用验证可以看做是对类自身以外(常量池中的各种符号引用)的信息进行匹配性校验。
三、准备
正式为类变量(被static修饰的变量)分配内存并设置类变量初始值的阶段,这些变量所使用的内存都将在方法区中进行分配。
初始值通常是数据类型的零值。
public static int value = 123;
变量value在准备阶段完成后的初始值是0,而不是123,把value赋值为123的动作将在初始化阶段才会执行。
public static final int value = 123;
常量value在准备阶段完成后的初始值是123,编译时javac将会为value生成常量属性,在准备阶段虚拟机会根据常量属性的设置将value赋值为123。
四、解析
虚拟机将常量池内的符号引用替换为直接引用的过程。
符号引用:以一组符号来描述所引用的目标,符号可以是任何形式的字面量,只要使用时能无歧义的定位到目标即可。
直接引用:可以是直接指向目标的指针、相对偏移量或是一个能间接定位到目标的句柄。如果有了直接引用,那么引用的目标必定已经在内存中存在。
五、初始化
初始化阶段是执行类构造器<clinit>()方法的过程。
类加载器
类加载阶段中“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,实现这个动作的代码模块称为“类加载器”。
对于任意一个类,都需要由加载它的类加载器和这个类本身一同确立其在Java虚拟机中的唯一性,每一个类加载器,都拥有一个独立的类名称空间。
1 package com.wangbo; 2 3 import java.io.InputStream; 4 5 /** 6 * 类加载器与instanceof关键字演示 7 * @author wangbo 8 * @date 2018-09-04 18:49:48 9 */ 10 public class ClassLoaderTest { 11 12 public static void main(String[] args) throws Exception { 13 14 ClassLoader myLoader = new ClassLoader() { 15 @Override 16 public Class<?> loadClass(String name) throws ClassNotFoundException { 17 try { 18 String fileName = name.substring(name.lastIndexOf(".") + 1) + ".class"; 19 InputStream is = getClass().getResourceAsStream(fileName); 20 if (is == null) { 21 return super.loadClass(name); 22 } 23 byte[] b = new byte[is.available()]; 24 is.read(b); 25 return defineClass(name, b, 0, b.length); 26 } catch (Exception e) { 27 throw new ClassNotFoundException(name); 28 } 29 } 30 }; 31 32 Object object = myLoader.loadClass("com.wangbo.ClassLoaderTest").newInstance(); 33 System.out.println(object); 34 System.out.println(object instanceof com.wangbo.ClassLoaderTest); 35 36 } 37 38 }
运行结果
尽管两个类一模一样,但是在用instanceof检查类型时返回了false,这是因为虚拟机中存在了两个ClassLoaderTest类,一个是由系统应用程序类加载器加载的,另一个是由自定义的类加载器加载的,虽然都来自同一个Class文件,但依然是两个独立的类。
双亲委派模型
从Java虚拟机角度来讲,只存在两种不同的类加载器:
一种是启动类加载器(Bootstrap ClassLoader),这个类加载器使用C++语言实现,是虚拟机自身的一部分。
另一种就是所有其他的类加载器,都由Java语言实现,独立于虚拟机外部,都继承自抽象类java.lang.ClassLoader。
从Java开发人员角度来讲,类加载器可以分为以下三种:
(1)启动类加载器(Bootstrap ClassLoader)
负责将存放在<JAVA_HOME>lib目录下的,或者被-Xbootclasspath参数所指定的路径中,并且被虚拟机识别的类库加载到虚拟机内存中(仅按照文件名识别,名称不符合的类库不会加载)。也可称为引导类加载器。
启动类加载器无法被Java程序直接引用,用户在编写自定义类加载器时,如果需要把加载请求委派给启动类加载器,直接使用null代替即可。
(2)扩展类加载器(Extensions ClassLoader)
这个类加载器由sun.misc.Launcher$ExtClassLoader实现,负责加载<JAVA_HOME>libext目录下的,或者被Java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。
(3)应用程序类加载器(Application ClassLoader)
这个类加载器由sun.misc.Launcher$AppClassLoader实现,由于这个类加载器是ClassLoader中的getSystemClassLoader()方法的返回值,所以一般称它为系统类加载器。
负责加载用户类路径(ClassPath)上所指定的类库,开发者可以直接使用这个类加载器。
如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序默认的类加载器。
上图中展示的类加载器中的这种层次关系,称为类加载器的双亲委派模型,双亲委派模型要求除了顶层的启动类加载器外其余的类加载器都应有自己的父类加载器。
这里类加载器之间的父子关系一般不会以继承的关系来实现,而是使用组合关系来复用父加载器的代码。
双亲委派模型的工作过程:
如果一个类加载器收到了类加载的请求,它首先会把这个请求委派给父类加载器去完成,每个层次的类加载器都是如此,因此所有的加载请求最终都传送到了顶层的启动类加载器中,只有当父类加载器反馈自己无法完成这个加载请求时,子类加载器才会尝试自己去加载。
双亲委派机制是为了保证java核心库的类型安全,不会出现加载了用户自己定义的java.lang.Object类的情况,从而保证了Java程序的稳定运行。
不符合双亲委派模型的情况:
双亲委派模型不是一个强制性的约束模型,是Java设计者推荐给开发者的类加载器实现方式。大部分类加载器都遵循这个模型,但也有例外,目前为止,有三次较大规模的“模型被破坏”。
(1)JDK1.2才引入的双亲委派模型,而类加载器和抽象类在在JDK1.0时代就已经存在,所以Java设计者在设计双亲委派模型时做出了一些妥协。
(2)双亲委派模型存在缺陷,虽然很好的解决了各个类加载器基础类的统一问题,但是却无法解决基础类又要调用回用户的代码的情况。所以Java中所有涉及到SPI的加载动作都采用打通双亲委派模型的层次结构来逆向使用类加载器,例如JNDI、JDBC、JCE、JAXB、JBI等。
(3)由于Sun在和JCP组织在模块化规范之争中落败,导致OSGi成为业界事实上的Java模块化标准,而OSGi实现模块化热部署的关键在于它自定义的类加载器机制的实现,在OSGi环境下,类加载器不再是双亲委派模型中的树状结构,而是进一步发展为网状结构。OSGi的类加载顺序除了开头两点符合双亲委派规则,其余的类查找都在平级的类加载器中进行的。
以上是关于类加载机制的主要内容,如果未能解决你的问题,请参考以下文章
Android 逆向ART 脱壳 ( DexClassLoader 脱壳 | DexClassLoader 构造函数 | 参考 Dalvik 的 DexClassLoader 类加载流程 )(代码片段
Android 逆向ART 脱壳 ( DexClassLoader 脱壳 | DexClassLoader 构造函数 | 参考 Dalvik 的 DexClassLoader 类加载流程 )(代码片段