类加载机制
一、Java类加载机制
java中,每一个类或者接口,在编译后,都会生成一个.class文件。
类加载机制指的是将这些.class文件中的二进制数据读入到内存中并对数据进行校验,解析和初始化。最终,每一个类都会在方法去保存一份元数据,在堆中创建一个与之对应的Class对象。
类的生命周期,经历7个阶段,分别是加载,验证,准备,解析,初始化,使用,卸载。
类加载过程包括加载,验证,准备,解析,初始化
类加载时机
类加载时机也就是.class文件什么时候被读取到虚拟机的内存中,并且达到可用的状态。
大多数情况下,都遵循什么时候初始化来进行加载。
初始化时机:
- 使用new实例化对象时,读取或者设置一个类的静态字段或者方法时。
- 反射调用时,例如Class.forName(“com.xxx.ClassName”)
- 初始化一个类的子类,会首先初始化子类的父类
- Java虚拟机启动时标明的启动类
- JDK8之后,接口中存在default方法,这个接口的实现来初始化时,接口会在其之前进行初始化。
类的加载过程
类的加载过程分5个阶段,其中,验证,准备,解析可以归纳为”连接“
**注:**这五个阶段,并不是严格意义上的按顺序完成,在类加载过程中,这些阶段会互相混合,交叉运行,最终完成类的加载和初始化。
加载
加载是类加载过程的第一个阶段,在加载阶段,虚拟机需要完成三件事情:
- 通过类的全限定名去找到其对应的.class文件
- 将这个.class文件内的二进制数据读取出来,转化成方法区的运行时数据结构
- 在java堆中生成一个代表这个类的java.lang.Class对象,作为对方法去中这些数据的访问入口
验证
Class文件中的内容是字节码,这些内容可以由任何途径产出,验证阶段的目的是保证文件内容里边的字节流符合Java虚拟机规范,且这些内容信息运行后不会危害虚拟机的自身的安全。
验证阶段会完成以下校验:
文件格式校验:验证字节码是否符合Class文件格式的规范
**元数据验证:**对字节码描述的元数据信息进行语义分析,要符合java语言规范
字节码验证:对类的方法体进行校验,确保这些方法在运行时是合法的,符合逻辑的
符号引用验证:发生在解析阶段,符号引用转为直接引用的时候。
验证阶段是非常重要的,但不是必须的,对程序运行期没有影响,如果在保证引用的类经过验证的情况下可以考虑使用-Xverifynone参数来关闭大部分的类验证措施,以缩短虚拟机类加载的时间。
准备
准备阶段,类的静态字段信息会得到内存分配并且被设置为初始值
- 1.内存分配仅包括static修饰过的变量,而不包含实例变量,实例变量得等到对象实例化的时候分配内存
- 2.初始值指的是变量数据类型的默认值,而不是被在java代码中显式赋予的值,当字段被final修饰成常量时,这个初始值就是java代码中显式赋予的值。
例如:public static int value = 3
类变量 value 在准备阶段设置的初始值是 0,不是 3。把value赋值为3的 putstatic 指令是在程序编译后,存放于类构造器 () 方法中的,所以把 value 赋值为 3 的动作将在初始化阶段才会执行。
当使用 final 修饰后:public static final int value = 3
类变量 value 在准备阶段设置的初始值是 3,不是 0。
- 3.在JDK取消永久代后,方法区变成了一个逻辑上的区域,这些类变量的内存实际上是分配在java堆中的。
解析
这个阶段,虚拟机会把这个Class文件中,常量池的符号引用转换为直接引用。主要解析的是类或者接口,字段,类方法,接口方法,方法类型,方法句柄等符号引用。符号引用转换为直接应用的过程就是当前加载的这个类和它所引用的类正式进行连接的过程。
什么是符号引用?
java代码在编译期间,是不知道最终引用的类型,具体指向内存中的哪个位置的,此时会用一个符号引用,来表示具体引用的目标是谁,Java虚拟机规范中明确定义了符号引用的形式,符合这个规范的前提下,符号引用可以是任意值,只要能通过这个值能定位到目标
什么是直接引用?
直接引用就是可以直接或者间接指向目标内存位置的指针或句柄
引用的类型,还未加载初始化怎么办?
当出现这种情况,会触发这个引用对应类型的加载和初始化
初始化
类加载的最后一步,初始化的过程就是执行类构造器<clinit>
()方法的过程 当初始化完成之后,类中static修饰的变量会赋予程序员实际定义的值,同时类中如果存在static代码块,也会执行这个静态代码里边的代码
<clinit>()方法的作用是什么?
在准备阶段,已经对类中static修饰的变量赋予了初始值。<clinit>()方法的作用,就是给这些变量赋予程序员实际定义的值。同时类中如果存在static代码块,也会执行这个静态代码块里边的代码
<clinit>()方法是什么?
<clinit>()方法和<init>方法是不同的,它们一个是类构造器,一个是实例构造器,java虚拟机会保证子类<clinit>()方法在执行前,父类的<clinit>已经执行完毕。而<init>方法则需要显性的调用父类的构造器
<clinit>()方法由编译器自动生成,但不是必须生成的,只有这个类存在static修饰的变量,或者类中存在静态代码块的时候,才会自动生成<clinit>()方法
加载过程总结
当一个符合java虚拟机规范的字节流文件,经历加载,验证,准备,解析,初始化这些阶段相互协作完成之后,加载阶段读取到的Class字节流信息,会按虚拟机规定的格式,在方法区保存一份,然后会在java堆中,会创建一个java.lang.Class类的对象,这个对象描述了这个类所有信息,也提供了这个类在方法区的访问入口。
方法区中,使用同一加载器的情况下,每个类只会有一份Class字节流信息
Java堆中,使用同一加载器的情况下,每个类中只会有一份java.lang.Class类的对象
类加载器
类加载器就是在加载阶段,通过类的全限定名,获取该类字节流数据的动作。
三层类加载器介绍
- 启动类加载器(Bootstrap Class Loader):负责加载<JAVA_HOME>\lib目录,或者呗-Xbootclasspath参数指定的路径,例如jre/lib/rt.jar里所有的class文件。由c++实现,不是ClassLoader子类
- 拓展类加载器(Extension Class Loader):负责加载Java平台中扩展功能的一些jar包,包括<JAVA_HOME>\lib\ext目录中或者java.ext.dirs指定目录下的jar包,由java代码实现。
- 应用程序类加载器(Application Class Loader):程序开发者开发的应用程序,有他加载,负责加载ClassPath路径下的所有jar包
双亲委派模型
任何一个类加载器在接到一个类的加载请求时,都会先让其父类进行加载,只有父类无法加载(或者没有父类)的情况下,才尝试加载
protected Class<?> loadClass(String name, boolean resolve)
throws ClassNotFoundException
{
// First, check if the class has already been loaded
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
}
if (c == null) {
// If still not found, then invoke findClass in order
// to find the class.
c = findClass(name);
}
}
return c;
}
ClassLoader类中的加载示例
双亲委派模型的好处
使用双亲委派模型,可以保证,每一个类只会有一个类加载器,例如java最基础的Object类,它存放在rt.jar中,这是Bootstrap的职责范围,当向上委派到Bootstrap时就会被加载,但是如果没有双亲委派模型,可以任由自定义类加载器加载的话,Java的核心api就会被随意篡改
二、Android中的ClassLoader
1、类加载器类型
Android跟java有很大的渊源,基于jvm的java应用是通过classLoader来加载应用中的class的,Android对jvm优化过,使用的是dalvik虚拟机,且class文件会被打包进一个dex文件中,底层虚拟机有所不同,那么它们的类加载器也会有区别。
Andorid中最主要的类加载器有4个
- BootClassLoader: 加载Android Framework层的class字节码文件(类似java的BootStrapClassLoader)
- PathClassLoader: 加载已经安装到系统中的APK的class字节码文件(类似java的App ClassLoader)
- DexClassLoader:加载指定目录的class字节码文件(类似java中的Custom ClassLoader)
Android 中的类加载器和java类加载器一样使用的是双亲委派模型
2、PathClassLoader与DexClassLoader的区别
1)使用场景
- PathClassLoader: 只能加载已经安装到Android系统的apk文件(data/app目录),是Android默认使用的类加载器
- DexClassLoader:可以加载任意目录下的dex/jr/zip文件,比PathClassLoader更灵活
2)代码差异
// PathClassLoader
public class PathClassLoader extends BaseDexClassLoader {
public PathClassLoader(String dexPath, ClassLoader parent) {
super(dexPath, null, null, parent);
}
public PathClassLoader(String dexPath, String librarySearchPath, ClassLoader parent) {
super(dexPath, null, librarySearchPath, parent);
}
}
// DexClassLoader
public class DexClassLoader extends BaseDexClassLoader {
public DexClassLoader(String dexPath, String optimizedDirectory,
String librarySearchPath, ClassLoader parent) {
super(dexPath, new File(optimizedDirectory), librarySearchPath, parent);
}
}
- PathClassLoader与DexClassLoader都继承于BaseDexClassLoader
- PathClassLoader与DexClassLoader在构造函数中都调用了父类的构造函数,但DexClaccLoader多传了一个optimizeDirectory
3、BaseDexClassLoader
1) 构造函数
public class BaseDexClassLoader extends ClassLoader {
private final DexPathList pathList;
...
public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent){
super(parent);
this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
}
...
}
- dexPath:要加载的程序文件(一般是dex文件,也可以是jar/apk/zip文件)所在目录
- optimizedDirectory:dex文件的输出目录(因为在加载jar/apk/zip等压缩格式的文件时会解压出其中的dex文件,该目录就是专门用于存放这些被解压出来的dex文件)
- liraryPath:加载程序文件时需要用到的库路径
- parent:父加载器
pathClassLoader只会加载已安装包中的dex文件,而dexClassLoader不仅仅可以加载dex文件还可以加载jar,apk,zip中的dex。jar apk zip就是一些压缩格式,要拿到压缩包里边的dex文件就需要解压。所以,DexClassLoader在调用父构造函数时会指定一个解压目录
2) findClass()
类加载器会提供一个方法来供外界找到它所加载的class,该方法就是findClass()。
private final DexPathList pathList;
@Override
protected Class<?> findClass(String name) throws ClassNotFoundException {
List<Throwable> suppressedExceptions = new ArrayList<Throwable>();
// 实质是通过pathList的对象findClass()方法来获取class
Class c = pathList.findClass(name, suppressedExceptions);
if (c == null) {
ClassNotFoundException cnfe = new ClassNotFoundException("Didn't find class \"" + name + "\" on path: " + pathList);
for (Throwable t : suppressedExceptions) {
cnfe.addSuppressed(t);
}
throw cnfe;
}
return c;
}
可以看到,BaseDexClassLoader的findClass()方法实际上是通过DexPathList对象的findClass()方法来获取class,而这个DexPathLIst对象恰好在之前的BaseDexClassLoader构造函数中就已经被创建好了。
4.DexPathList
1) 构造函数
private final Element[] dexElements;
public DexPathList(ClassLoader definingContext, String dexPath,
String libraryPath, File optimizedDirectory) {
...
this.definingContext = definingContext;
this.dexElements = makeDexElements(splitDexPath(dexPath), optimizedDirectory,suppressedExceptions);
...
}
构造函数中,保存了当前类加载器definingContext,并调用的makeDexElements()得到Element集合
通过对splitDexPath(dexPath)的源码追溯,发现该方法的作用其实就是将dexPath目录下的所有程序文件转变成一个File集合。而且还发现,dexPath是一个用冒号(“:”)作为分隔符把多个程序文件目录拼接起来的字符串(如:/data/dexdir1:/data/dexdir2:…)。
makeDexElements()方法
private static Element[] makeDexElements(ArrayList<File> files, File optimizedDirectory, ArrayList<IOException> suppressedExceptions) {
// 1.创建Element集合
ArrayList<Element> elements = new ArrayList<Element>();
// 2.遍历所有dex文件(也可能是jar、apk或zip文件)
for (File file : files) {
ZipFile zip = null;
DexFile dex = null;
String name = file.getName();
...
// 如果是dex文件
if (name.endsWith(DEX_SUFFIX)) {
dex = loadDexFile(file, optimizedDirectory);
// 如果是apk、jar、zip文件(这部分在不同的Android版本中,处理方式有细微差别)
} else {
zip = file;
dex = loadDexFile(file, optimizedDirectory);
}
...
// 3.将dex文件或压缩文件包装成Element对象,并添加到Element集合中
if ((zip != null) || (dex != null)) {
elements.add(new Element(file, false, zip, dex));
}
}
// 4.将Element集合转成Element数组返回
return elements.toArray(new Element[elements.size()]);
}
总体来说,DexPathList的构造函数是将一个个的程序文件(可能是dex、apk、jar、zip)封装成一个个Element对象,最后添加到Element集合中。
Android的类加载器(不管是PathClassLoader,还是DexClassLoader),它们最后只认dex文件,而loadDexFile()是加载dex文件的核心方法,可以从jar、apk、zip中提取出dex,
2) findClass()
DexPathList的findClass()方法
public Class findClass(String name, List<Throwable> suppressed) {
for (Element element : dexElements) {
// 遍历出一个dex文件
DexFile dex = element.dexFile;
if (dex != null) {
// 在dex文件中查找类名与name相同的类
Class clazz = dex.loadClassBinaryName(name, definingContext, suppressed);
if (clazz != null) {
return clazz;
}
}
}
if (dexElementsSuppressedExceptions != null) {
suppressed.addAll(Arrays.asList(dexElementsSuppressedExceptions));
}
return null;
}
DexPathList的findClass()方法很简单,就只是对Element数组进行遍历,一旦找到类名与name相同的类时,就直接返回这个class,找不到则返回null。
调用DexFile的loadClassBinaryName()方法来加载class?这是因为一个Element对象对应一个dex文件,而一个dex文件则包含多个class。也就是说Element数组中存放的是一个个的dex文件,而不是class文件。这可以从Element这个类的源码和dex文件的内部结构看出。
android类加载器与java类加载器异同
根据前面的分析,我们总结下,android与java在类加载上的异同
相同:
- Android类加载器和Java的类加载器工作机制是类似的,使用双亲委托机制
不同:
- 加载的字节码不同 Android虚拟机运行的是dex字节码,Java虚拟机运行的class字节码。
- 类加载器不同以及类加载器的类体系结构不同 如上面的类加载器结构图
- BootClassLoader和Java的BootStrapClassLoader区别:Android虚拟机中BootClassLoader是ClassLoader内部类,由java代码实现而不是c++实现,是Android平台上所有ClassLoader的最终parent,这个内部类是包内可见,所以我们没法使用。 Java虚拟机中BootStrapClassLoader是由原生代码(C++)编写的,负责加载java核心类库(例如rt.jar等)
- ClassLoader类中的
findBootstrapClassOrNull
方法,android sdk直接返回null,jdk会去调用native方法findBootstrapClass,如下源码