重学系列---类加载机制(Java&Android)

2,181 阅读12分钟
还记得以前面试的时候
面试官随口问了一句,你知道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默认值为0booleanfalse等等

假如说

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 N的运行方式

类加载器

在我看来,Android中的类加载机制和Java的类加载机制差不多,不同的是他们加载的文件不同,那么现在我们来看看Android的吧

在Android中,大体分类两类 BootClassLoaderBaseDexClassLoader,基于BaseDexClassLoader又衍生出了PathClassLoaderDexClassLoader

  • 类的继承关系图

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

PathClassLoaderDexClassLoader的父类都是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执行的安全

总结

通过比对的方式学习对于类加载机制我有了更深刻的认识,关于学习不能只学,更要疯狂输出,疯狂~输出