还记得以前面试的时候
面试官随口问了一句,你知道Android中的类加载机制吗?
我:记得,吧啦吧啦吧啦……
面试官:好了,今天的面试先到这里了
后面回家查资料才知道当时说的是Java相关的,并非是Android的,现在重新梳理两者的内容整理此文
Java
我们知道Java通过.java的源文件通过javac编译成.class的字节码,然后交由JVM进行处理
在聊Java的类加载之前我们首先了解下一下字节码,它和类加载机制息息相关。
字节码
计算机只认识0和1,所以任何语言都需要编译成机器码才能被计算机理解和执行,Java也不例外,这也就是为什么我们要通过javac编译成.class文件的原因
我们简单看下Java字节码是什么鸟样?
public class Test{
public static void main(String[] args){
System.out.println("i am just test");
}
}
代码通过编译后,会生成一个Test.class的字节码文件,然后我们使用xxd Test.class
命令就可以查看这个文件了
发现这个文件看不懂,一脸懵逼,懵逼就对了,我们不需要过多的关心其他的东西,重点看字节码中cafe base就好了,这东西叫做“魔数”,是JVM识别.class文件的标志,是类加载过程中的重要环节
什么是类的加载
了解完字节码后,我们再看下什么是类的加载?
所谓类的加载就是指将类的.class文件文件中的二进制数据读入到内存中,将其放到运行时数据区的方法区内,然后在堆区中创建出一个java.lang.Class的对象,用来封装类在加载方法区中的数据结构。并且为开发者提供了方区内的数据结构的接口
类加载的过程
类从被加载到虚拟机内存开始到卸载为止,他的生命周期包括加载->验证->准备->解析->初始化->使用->卸载
7个阶段,其中验证->准备->解析
统称为连接,顺序如图所示
注意:
加载、验证、准备、解析和初始化过五个阶段中,加载、验证、准备和初始化的顺序是确定的
解析阶段不一定,它有时候可能在初始化阶段后开始的
也就是说这些是按照顺序开始的,但是不按照顺序进行和完成
因为它们通常是相互交叉进行的
下面简单聊聊这几个过程
载入
载入(加载):载入是类加载过程中的第一步,也就是我们上面说的通过javac编译字节码后,将字节码中的二进制数据读入到内存中的操作,也就是上文中什么是类的加载的内容。
总结下,虚拟机主要完成三件事:
①类的.class文件文件中的二进制数据读入到内存中
②将这个字节流的静态存储结构转换为方法区内运行时数据结构
③在堆区中创建出一个java.lang.Class的对象,并为方区内的数据结构提供接口
验证
验证是连接阶段的第一步
验证的作用就是确保被加载类的正确性,不会对JVM产生危害行为。
主要完成四个阶段的验证:
1.文件格式的验证:
验证.class文件字节流是否符合class文件的规范,并且能被虚拟机进行处理。
验证包括魔数、版本号、常量池等校验(魔数和版本号包含在.class文件中)
2.元数据校验:
主要对字节码描的信息进行语义分析,保证符合Java语言规范
3.字节码校验:
是验证过程中最复杂的阶段,主要是通过数据流和控制流分析,确定程序语法是否合法,
是否符合逻辑性,也是为了避嫌在运行时对JVM产生危害
4.符号引用校验:
是验证过程中的最后一个阶段,主要是在JVM将符号引用转为直接引用的时候进行校验。
主要是对类自身以外的信息进行校验,保证解析动作完成
准备
准备阶段主要是为类变量分配内存并设置初始值
类变量(static)会分配内存,但是实例不会,实例变量要在对象被实例化的时候分配到Java堆中
初始值是指数据类型的默认值,如int默认值为0,boolean为false等等
假如说
public static int value = 10
在此处int的值是0,而不是10。被赋值为10的操作是在初始化阶段
如果说代码变成这样的
public static final int value = 10
注意了,同时被static和final修饰的的时候,在此刻的值就是10
因为同时被static和final修饰在编译期就已经将结果放到了类的常量池中了
解析
解析阶段就是将符号引用转变为直接引用的过程
符号引用:用一组符号,任何字面量来描述引用的目标
直接引用:对符号引用进行解析,找到对应的实际内存地址
解析主要是针对类、接口、字段、类方法、接口方法、方法类型、方法句柄和调用点限定符7类符号引用进行
初始化
初始化是类加载过程中的最后一步,对类的静态变量和静态代码块执行初始化操作
在这个阶段,Java程序才会正式开始执行,我们知道在准备阶段的时候已经为类变量赋过一次值了,在这个阶段可以根据需要进行赋值操作
这个阶段就是执行类构造器的<clinit>()方法的过程
,简单说就是执行了new操作
类什么时候被初始化
1) 创建类的时候,也就是new操作
2) 访问类或接口中的静态变量或者对该静态变量赋值时
3) 调用类的静态方法
4) 反射(Class.forName("xxxx"))
5) 初始化一个类的子类
6) JVM启动时标明的启动类,也就是文件名和类名相同的那个类
类初始化步骤
1) 如果这个类还没有被加载和链接,那么会先执行加载和链接
2) 如果这个类存在直接父类,并且这个类还没有初始化(注意:在一个类加载器中,类只能被初始化一次),那就初始化直接的父类(不适用于接口)
3) 假如类中存在static变量和static块,那么就依次执行这些初始化操作
加载类的方式
1) 从本地系统直接加载
2) 从网络下载.class文件
3) 从zip、jar等压缩文件中加载.class文件
4) 从专门数据库中提取.class文件
5) 编译Java文件得到.class文件
6) 通过Class.forName()方法加载
7) 通过ClassLoader.loadClass()方法加载
类加载器
JVM的类加载是通过ClassLoader及其子类来完成的,类的层级关系和加载顺序如下面所示:
此处运用到了双亲委派模型,优先从顶级父类加载,这样可以保证Java程序的稳定运行
我们可以通过使用对象.getClassLoader()
方法查看当前是属于哪个加载器类型
双亲委派机制
- 概念 当某个类加载器加载某个.class文件的时候,它首先会把这个任务交给上级类加载器,递归这个操作,如果最后没有找到,则自己加载这个类
- 代码逻辑(伪代码)
public void loadClass(String name){
//1.查找是否被加载
Class c=findLoadedClass(name);
if(parent!=null){
//2.如果父类存在,则交给父类处理
c=parent.loadClass(name);
}else{
//3.不存在的话就交给启动器处理
c=findBoostrapClass0(name);
}
//4.否则自己处理
c=findClass(name);
}
Java的类加载机制我们就了解到这里了,现在来看看Android相关知识
Android
在说Android的类加载机制前,我们首先了解下Android虚拟机相关知识
JVM vs Dalvik
Android应用程序运行在Dalvik/ART虚拟机上,并且每一个应用程序都对应有一个单独的Dalvik虚拟机实例。
Dalvik虚拟机其实也算是一个Java虚拟机,只不过它执行的是.dex文件
,并非Java中的.class文件
Dalvik虚拟机和Java虚拟机共享有差不多的特性,区别是在于两者执行的指令集不同,Java虚拟机是基于堆栈,而Dalvik是基于寄存器执行的
那么问题来了,什么是基于栈的虚拟机?什么是基于寄存器的虚拟机呢?
- 基于栈的虚拟机 对于栈的虚拟机而言,每一个运行时的线程,都有一个独立的栈。栈中记录了方法调用的历史,每一次方法调用,栈中就会多一个栈帧。最顶部的栈帧被称为当前栈帧,代表当前执行的方法。
基于栈的虚拟机是通过操作数据进行所有的操作
-
基于寄存器的虚拟机 基于寄存器的虚拟机没有操作数栈,但是有很多虚拟寄存器,和操作数栈相同,这些寄存器存放在运行时的栈中,本质上就是一个数组。与JVM相似,在Dalvik VM中每个线程都有自己的PC(程序计数器)和调用栈,方法调用的活动记录以
帧
为单位保存在调用栈上 -
栈的虚拟机 vs 寄存器的虚拟机 与JVM相比,发现Dalvik调用程序的指令减少了,数据移动的次数也减少了
两者对比如下:
对比内容 | 栈式调用 | 寄存器式调用 |
---|---|---|
指令条数 | 多 | 少 |
代码尺寸 | 小 | 多 |
移植性 | 优 | 劣 |
指令优化 | 不易 | 容易 |
解释执行速度 | 慢 | 快 |
代码生成难度 | 容易 | 复杂 |
数据移动次数 | 多 | 少 |
ART & Dalvik
Dalvik虚拟机执行的是dex字节码,解释执行。从Android2.2版本开始,支持JIT(Just In Time)即时编译
,在程序运行的过程中选择热点(经常执行的)代码进行编译或者优化
ART(Android Runtime)
是在Android4.4中引入的一个开发者选项,也是Android5.0及更版本的默认Android运行式。
ART虚拟机执行的是本地机器码,Android运行从Dalvik虚拟机替换成ART虚拟机,开发者无感知的操作,APK中包含的仍然是dex字节码的文件
你知道ART虚拟机执行的本地机器码是哪里来的吗?(提示:dex2aot)
Android N的运行方式
在Android N上, ART在混合使用预先AOT
编译,解释执行和JIN
这种方式的好处:
1.最初安装应用的时候不进行任何的AOT编译操作(安装变快了)
,运行过程中解释执行,对进程执行的方法进行JIT操作,经过JIT操作编译的方法将会被记录到Profile配置文件中
2.当设备闲置和充电时,编译守护进程会运行,根据Profile文件对常用代码进行AOT编译,等待下次运行时直接使用
借用一张热修复图表示这个过程
类加载器
在我看来,Android中的类加载机制和Java的类加载机制差不多,不同的是他们加载的文件不同,那么现在我们来看看Android的吧
在Android中,大体分类两类 BootClassLoader
和BaseDexClassLoader
,基于BaseDexClassLoader又衍生出了PathClassLoader
和DexClassLoader
- 类的继承关系图
ClassLoader是一个抽象类,它具体的实现类有:
1. BootClassLoader:用于加载Android Framework层的class
2. BaseDexClassLoader:用于Android程序类加载器。可以加载指定的dex,以及jar、zip、apk中的classed.dex
3.DexClassLoader:加载指定的dex,以及jar、zip、apk中的classed.dex
4.PathClassLoader:加载指定的dex,以及jar、zip、apk中的classed.dex
PathClassLoader
和 DexClassLoader
的父类都是BaseDexClassLoader,那么他们之间有什么区别呢?
可以看到两者的唯一区别在于:创建DexClassLoader的时候需要传递一个optimizesDirectory参数,并且需要创建一个File对象传递给super,而PathClassLoader可以直接传递null
注意了,在API26(android 9.0)之后两者并没有什么区别了
- 双亲委托流程
注意:
c=findBootstrapClassLoaderOrNull(name)
按照方法名理解当parent为null的时候,也能够加载BootStrapClassLoader加载的类
如果new PathClassLoader("/sdcard/xxx.dex",null)
,能否加载Activity.class?
实际上:
private Class findBootstrapClassLoaderOrNull(String name){
return null;
}
此处Android和Java不同
- Activty是如何被加载
我们在MainActivity中通过getClassLoader()
方法获取到当前为PathClassLoader
加载器,那么我们可以来看下PathClassLoader中的方法
public class PathClassLoader extends BaseDexClassLoader{}
由于PathClassLoader是继承的BaseDexClassLoader,我们可以直接查看BaseDexClassLoader中的findClass
方法,看看是如何实现的
实现很简单,从pathList中查找class,我们继续查看DexPathList
查看
makeDexElements()
方法最终发现实现的是去List<File>.add(dexPath)中使用DexFile加载dex文件返回 Element数组
最后查看findClass方法发现,从element中获得代表Dex的DexFile,通过dex.loadBinaryName
查找对应的class文件
上面简单的描述了一个类被加载的过程,最后用流程图简单总结下:
顺便说一句,热修复其实就是将补丁包插入到classex.dex文件的最前面,让它优先执行,这样就实现了修复的功能
我认为其实Java和Android的类加载机制大同小异,都是通过双亲委派机制进行加载类的操作,只是加载的文件不同而已
双亲委派机制的好处
1.防止重复加载同一个class文件,保证数据安全
2.保证了核心class不会被篡改,通过委托的方式查找,即使篡改了也无法被加载,保证了class执行的安全
总结
通过比对的方式学习对于类加载机制我有了更深刻的认识,关于学习不能只学,更要疯狂输出,疯狂~输出