Android 插件化基础——虚拟机

来源:互联网 发布:sodu小说源码 编辑:程序博客网 时间:2024/05/22 00:29

其他有关插件化的文章欢迎大家观阅
插件化踩坑之路——Small和Atlas方案对比
Android插件化基础篇—— class 文件
Android插件化基础篇 — dex 文件

我们之前讲解了 class 文件和 dex 文件,但如果没有虚拟机,这些文件都将没有任何意义,所以我们这篇文章将给大家讲解下 JVM 和 Dalvik 的一些知识。

这里提醒一下大家,虚拟机内容非常多,这里只会讲解一些必要的概念和知识,如果需要深入了解,可以通过购买一些书籍来学习,我这里推荐给大家一本国人大牛写的一本 Java 虚拟机书籍 「深入理解 Java 虚拟机」(不是托,因为我自己看的是这本),这本书算是把 Java 虚拟机讲述的非常清楚了,感兴趣的童鞋可以自行购买。

一 Java 虚拟机结构解析

我们学习一个知识点的时候,要先从整体去看一下它如何组成,再拆分开来,各个击破,从简单的部分入手,再慢慢深入,有针对性的学习,这样才能提升我们的学习效率,并且能更好的理解它。

下图是 JVM 的整体结构图。我们先简单概要的说下各个结构的作用。
JVM 整体结构

1. class 文件

Class 文件我们之前的文章已经说的比较清楚了,它是通过 Javac 命令将 java 文件生成为 class 文件的,这里就不再细说了。下图是 java 源代码的编译流程
编译流程
这一系列的步骤实际上就是由 Javac 内部执行的,把 Java 源代码生成了字节码,Javac 就是一个编译器程序。其实整个编译流程就是编译器程序如何分析源文件,核心就是对源文件的词法语法分析,如果大家学过「编译原理」这门课,就一定知道,任何一门语言的核心都是编译器,如果你可以写一个编译器,那么你就可以创造一门语言。

2. 类加载器子系统

这部分的作用就是将 class 字节码加载到 JVM 虚拟机内存中,而类加载器的核心就是 ClassLoader ,平时我们说的类加载器实际上就是指 ClassLoader,而 ClassLoader 又是我们动态加载,动态更新的核心,我们将在下篇文章中详细讲解 ClassLoader 的相关知识,现在先简单介绍下 ClassLoader。
JVM 的 ClassLoader

Bootstrap ClassLoader 用来加载 rt.jar,rt 意为 runtime ,是 JDK 为我们提供的核心 jar 包。
Extension ClassLoader 用来加载 ext*.jar 或 Djava.ext.dirs 指定目录的 jar 包,它和 Bootstrap ClassLoader 实际上都是为了加载 JDK 中指定的特定 jar 包的。
App ClassLoader 用来加载应用程序的 ClassLoader。
Custom ClassLoader 通过重写 ClassLoader 来加载指定目录下的 class 文件。

Eclipse 中可以动态加载指定 jar 包中的内容就是通过 Custom ClassLoader 来实现动态加载,来拓展应用功能。Android 的虚拟机也继承了 JVM 的这种特性,所以 Android 也可以实现动态加载,所以* ClassLoader 是实现动态加载的理论依据和技术手段。*

下图是 ClassLoader 加载 class 字节码的流程:
加载流程

  • Loading:类的信息从文件中获取并且载入到 JVM 的内存里
  • Verifying:检查读入的结构是否符合 JVM 规范的描述
  • Preparing:分配一个结构用来存储类信息
  • Resolving:把这个类的常量池的所有的符号引用改变成直接引用
  • Initializing:执行静态初始化程序,把静态变量初始化成指定的值
    所以大家平时定义的 public static final 这样的静态变量都不会立马初始化,都要经过以上几个步骤,到最后才会执行 Initializing 初始化过程。

3. JVM 内存管理

class 字节码被 ClassLoader 加载到虚拟机内存空间以后,JVM 虚拟机还会把内存分为方法区Java堆区Java 栈本地方法栈。我们来看看这几个区域有什么作用

Java 栈区是我们内存管理中最重要的模块。它存放的是 Java 方法执行时的所有的数据。栈区是由栈帧组成,一个栈帧代表一个方法的执行,栈帧就是我们栈区的关键内容,有了栈帧我们就可以完成方法的嵌套调用。

每个方法从调用到执行完成就对应一个栈帧在虚拟机栈中入栈到出栈的过程。这里给大家举一个最简单的例子。

A 方法调用开始的时候,将会把 A 方法对应的栈帧压入到栈中,然后当 A 方法内部调用到 B 方法的时候,虚拟机将会创建一个栈帧保存 B 方法,并把这个栈帧压入到栈区中,当 B 方法执行完成后,B 方法的栈帧将会出栈,从而回到 A 方法中,等 A 方法执行完,A 方法的栈帧也会随之出栈,这就是一个栈帧的作用和描述方法调用的完整过程。

栈帧中包含局部变量表栈操作数动态链接方法出口,它存储了方法调用过程中的所有内容。

我们通常见到的 StackOverFlow 异常就是当栈的深度大于 JVM 所允许的最大深度的时候,就会抛出该异常来告知我们开发者我们的方法调用深度已经超过限制了。要模拟出这个异常也很容易,只要写一个没有退出条件的递归函数即可

//运行该方法将会抛出 StackOverFlowError 异常public void stackOverFlowTest(){    stackOverFlowTest();}

本地方法栈是专门为 native 方法服务的。也是通过栈帧控制调用的。

方法区存储被虚拟机加载的类信息、常量、静态变量、即时编译器编译后等数据,方法区是始终会占用内存的。

堆区也是一块比较复杂的存储空间。所有通过 new 创建的对象的内存都在堆中分配,堆区是虚拟机中最大的一块内存,是 GC 要回收的部分,下面一张图将展示 JVM 如何对堆内存进行分配。

堆区内存

Young Generation 代表新生代区,Old Generation 代表的是老年代区。我们刚刚创建的对象都将放在新生代区,当新生代区的内存不足的时候,JVM 会通过一定的算法规则把新生代区的对象移到老年代区,这样新生代区就又可以为后面新创建的对象分配内存了。当新生代和老年代区的内存都不足的时候,JVM 就会抛出 OOM 异常,同时 GC 也会重点回收这两个区域。

当然,新生代何时转移到老年代,转移算法又是怎样的,不同的虚拟机有不同的处理方式,有兴趣的朋友可以自行去研究一下。虚拟机将堆内存分为新生代和老年代而不是一整块内存区域,主要是方便我们调整两个内存的大小。

比如我们要开发一个信息通信系统,那么一些 message 对象将会频繁的创建,这时候我们就可以将新生代的内存区域多分一点,这样就可以便于内存分配,加快内存的创建。相反,如果开发一种服务类程序,不需要有这种频繁创建对象的情况,那我们就可以把老年代多分配一点,从而帮助对象常驻内存,提升服务的稳定性。调整新老代空间这块的知识点在服务端用的比较多,而且也相对比较深入了,移动端的话只要了解这两个区域概念就好了,因为我们很少会去改动这两个空间的大小的。

4. 垃圾回收

垃圾回收就是我们常说的 GC 。

既然我们要讲解垃圾回收,我们就要先确定哪些是垃圾,哪些对象会被我们标记为垃圾对象,先来看一下 JVM 有哪些垃圾收集的算法。

第一个算法是引用计数算法。这个算法有点老了,是第一个使用的垃圾收集算法,JDK 1.2 之前都是用的这种算法。每当我们在内存中创建一个对象的时候,都会为这个对象分配一个引用计数器,每当有新的引用连接到此对象的时候,计数器都会 +1 ,当有引用销毁的时候,计数器就会 -1,当计数器为 0 的时候,就表示这个对象可以被 GC 回收了。

但这个算法是有问题的,当对象 A 引用对象 B ,对象 B 引用对象 A 的时候,它们俩的引用计数都是 1, 但实际上它们都是不可达对象,没有路径能够指向它们,实际上它们已经是垃圾了,但在引用计数算法下,它们就无法被回收。这就是引用计数器的最大缺陷,也是我们写代码的时候尽量不要有这种相互引用的最早来源。

第二种也是我们一直用的算法就是可达性算法,也叫做根搜索算法,它的原理是根据我们离散数学中图论相关概念引入的,把程序中的所有引用关系看作图,从一个 GC Root 节点开始寻找我们所有的引用节点,找到引用节点后再继续寻找该节点的引用,直到寻找完所有的引用节点,剩下没找到的节点就是不可达节点,就会被标记成为垃圾对象,供 GC 回收,我们用一张图来帮助我们理解。

可达性算法

这里的 ObjD 和 ObjE 就是之前引用计数器中说的相互引用的情况,在可达性算法里面,没有一条路径可以从 GC Root 节点到达到它们,所以它们会被标记为可回收对象。

那么,说了这么多引用,引用又是什么呢?

引用有四种类型:强引用弱引用软引用虚引用,平时我们开发用的比较多的就是强引用和弱引用,其他两个平时开发中不太常用。

强引用的创建很简单:Object obj = new Object();

弱引用的创建需要引用强引用参数:WeakReference<Object> wf = new WeakReference<Object>(obj);

当我们通过 obj = null把 obj 置为 null,这个时候将只有 wf 这个弱引用指向 Object 对象。可以通过 wf.get(); 能够获得真正的引用,使用的时候一定要注意获取到的引用是否为 null,因为弱引用并不会阻碍对象的回收,get() 是可能拿到 null 的。

上面我们讲的主要是垃圾收集算法,我们通过算法来确定哪些是垃圾,拿到了垃圾,我们就要通过垃圾回收算法来真正的清理垃圾了

第一个算法是标记-清除算法,从根节点开始遍历所有的引用,遍历到的对象将会被标记,当遍历结束后,没有被标记的对象将会被回收。优点不需要对对象进行移动处理,只需要对不存活的对象进行处理,当内存中存活对象很多的情况下,非常高效,但因为算法会直接回收不存活的对象,这样就会造成非常严重的内存碎片问题,后面对象的内存分配将会非常不利。如图所示。
标记-清除算法

第二个算法是复制算法,逻辑很简单,遍历时发现某个引用是可达的,将会把该引用复制到空闲内存中,如果不可达则直接跳过,当遍历完毕后,将会回收原来的内存空间,只保留复制之后的内存空间。它的优点是,当存活对象比较少的时候很高效,但需要一块内存作为交换空间来进行对象移动。如图所示。
复制算法

基于以上两种算法的优缺点,JVM 开发人员开发出了第三种算法:标记-整理算法。实际上该算法是在标记-清除算法基础上演变而来的,在清理结束后,进行内存的整理,提高了成本,但解决了内存碎片的问题。

标记-整理算法

上面三种算法各有优劣,不能说某一种算法能完全替代另外两种,在 JVM 中这三种算法也是结合使用,在不同条件和情况下使用不同的算法。

垃圾回收的触发时机有以下几种情况:

  • Java 虚拟机无法再为新的对象分配内存空间了。
  • 手动调用 System.gc() 方法,但不推荐开发人员调用该方法,即使你调用了,JVM 也不会立即进行垃圾回收,而且该方法也会加大虚拟机的压力。
  • 低优先级的 GC 县城,被运行时会执行 GC 。

JVM 其他的结构都是和底层有点关系的,我们程序员实际上只要对上面四个模块了解了,就可以算是对 JVM 比较了解了。

二 Dalvik 与 JVM 的区别

Dalvik VM 和 JVM 最大的不同就是执行的文件不同,JVM 执行的是 class 文件,Dalvik VM 执行的是 dex。同时,类加载机制系统和 JVM 区别也很大,这方面我们下一篇文章将会细致讲解。再有就是,JVM 只能存在一个,而 DVM 可以存在多个,当某一个虚拟机挂了的时候,不会影响其他应用程序,保证了我们系统的稳定性。

Dalvik 是基于寄存器的, JVM 是基于栈的。寄存器会让 DVM 运行的更快,寄存器是比内存更快的存储介质。

三 ART 比 Dalvik 有哪些优势

DVM 虽然已经相比 JVM 有了一定优化,但速度还是被诟病,所以谷歌工程师又研发出了 ART 虚拟机。

DVM 使用 JIT 来将字节码转化成机器码,效率低。JIT 技术在应用每次运行的时候,都会将字节码转化为本地机器码,再去执行,如果退出应用,再次进入的时候就要重新进行一次转化流程,所以效率较低。

ART 采用了 AOT 预编译技术 (ahead of time),执行速度更快。AOT 在应用程序安装的时候就将字节码转化为了本地机器码并存储到了存储介质中,不管何时启动应用程序,都将会直接执行本地机器码,不需要再每次进行一次转化操作。

但使用了 ART 会占用更多的安装时间和存储空间,这种思想是我们软件工程的一个常见思想—以空间换时间,以不太稀缺的资源,换取比较稀缺的时间。


下一篇文章我们将讲解 ClassLoader,ClassLoader 是动态加载的理论依据,所以比较重要。

本文部分内容参考于慕课网实战课程「Android 应用发展趋势必备武器 热修复与插件化」,有兴趣的朋友可以付费学习。
插件化实战课程