Android 高级面试-4:虚拟机相关

内存管理属于基础知识组织下语言便可,内存模型放在 Java 并发相关;虚拟机执行系统是重点,包括,类加载机制(类的加载、校验阶段,与热补丁原理相关)java

学习这一块的内容能够参考:android

一、内存管理

  • GC 回收策略
  • Java 中内存区域与垃圾回收机制
  • 垃圾回收机制与调用 System.gc() 区别
  1. 标记-清除算法:这种算法直接在内存中把须要回收的对象“抠”出来。效率不高,清除以后会产生内容碎片,形成内存不连续,当分配较大内存对象时可能会因内存不足而触发垃圾收集动做。
  2. 标记-整理算法:相似于标记-清除算法,只是回收了以后,它要对内存空间进行整理,以使得剩余的对象占用连续的存储空间。
  3. 复制算法:将内存分红两块,一次只在一块内存中进行分配,垃圾回收一次以后,就将该内存中的未被回收的对象移动到另外一块内存中,而后将该内存一次清理掉。
  4. 分代收集算法:根据对象存活周期的不一样将内存划分红几块,而后根据其特色采用不一样的回收算法。

System.gc() 函数的做用只是提醒虚拟机:程序员但愿进行一次垃圾回收。可是它不能保证垃圾回收必定会进行,并且具体何时进行是取决于具体的虚拟机的,不一样的虚拟机有不一样的对策。git

  • Java 中对象的生命周期

一个类从被加载到虚拟机内存到卸载的整个生命周期包括:加载-验证-准备-解析-初始化-使用-卸载 7 个阶段。其中 验证-准备-解析 3 个阶段称为链接。程序员

加载发生在类被使用的时候,若是一个类以前没有被加载,那么就会执行加载逻辑,好比当使用new 建立类、调用静态类对象和使用反射的时候等。加载过程主要工做包括:1). 从磁盘或者网络中获取类的二进制字节流;2). 将该字节流的静态存储结构转换为方法取的运行时数据结构;3). 在内存中生成表示这个类的 Class 对象,做为方法区访问该类的各类数据结构的入口。github

验证阶段会对加载的字节流中的信息进行各类校验以确保它符合JVM的要求。面试

准备阶段会正式为类变量分配内存并设置类变量的初始值。注意这里分配内存的只包括类变量,也就是静态的变量(实例变量会在对象实例化的时候分配在堆上),而且这里的设置初始值是指‘零值’,好比int类型的会被初始化为 0,引用类型的会被初始化为 null,即便你在代码中为其赋了值。算法

解析阶段是将常量池中的符号引用替换为直接引用的过程。符号引用与虚拟机实现的布局无关,引用的目标并不必定要已经加载到内存中。各类虚拟机实现的内存布局能够各不相同,可是它们能接受的符号引用必须是一致的,只要能正肯定位到它们在内存中的位置就行。直接引用能够是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。若是有了直接引用,那引用的目标一定已经在内存中存在。编程

初始化是执行类构造器 <client> 方法的过程。<client> 方法是由编译器自动收集类中的类变量的赋值操做和静态语句块中的语句合并而成的。虚拟机会保证 <client> 方法执行以前,父类的 <client> 方法已经执行完毕。数组

  • JVM 内存区域,开线程影响哪块内存

内存区域大体的分布图,与线程对应以后的分布图缓存

JVM内存区域

图中由浅蓝色标识的部分是全部线程共享的数据区;淡紫色标识的部分是每一个线程私有的数据区域

  1. 程序计数器线程私有,用来指示当前线程所执行的字节码的行号,就是用来标记线程如今执行的代码的位置;对 Java 方法,它存储的是字节码指令的地址;对于 Native 方法,该计数器的值为空。
  2. 线程私有,与线程同时建立,总数与线程关联,表明Java方法执行的内存模型。每一个方法执行时都会建立一个栈桢来存储方法的的变量表、操做数栈、动态连接方法、返回值、返回地址等信息。一个方法的执行和退出就是用一个栈帧的入栈和出栈表示的。一般咱们不容许你使用递归就是由于,方法就是一个栈,太多的方法只执行而没有退出就会致使栈溢出,不过能够经过尾递归优化。栈又分为虚拟机栈和本地方法栈,一个对应 Java 方法,一个对应 Native 方法。
  3. :用来给对象分配内存的,几乎全部的对象实例(包括数组)都在上面分配。它是垃圾收集器的主要管理区域,所以也叫 GC 堆。它其实是一块内存区域,因为一些收集算法的缘由,又将其细化分为新生代和老年代等。若是在堆中没有内存完成实例分配,而且堆也没法再扩展时,将会抛出 OutOfMemoryError 异常。
  4. 方法区:方法区由多线程共享,用来存储类信息、常量、静态变量、即便编译后的代码等数据。运行时常量池是方法区的一部分,它用于存放编译器生成的各类字面量和符号引用,好比字符串常量等。根据 Java 虚拟机规范的规定,当方法区没法知足内存分配需求时,将抛出 OutOfMemoryError 异常。
  • 软引用、弱引用区别
  • Java 中的四种引用
  • 强引用置为 null,会不会被回收?

四种引用类型:强引用、软引用、弱引用和虚引用。

  1. 当使用 new 关键字建立一个对象的时候,这个对象就是强引用的,它绝对不会被回收,即便内存耗尽。你能够经过将其置为 null 来弱化对其的引用,但何时被回收还要取决于 GC 算法。
  2. 软引用和弱引用类似,你能够分别经过 SoftReference<T>WeakReference<T> 来使用它们,它们的区别在于后者更弱一些。当 JVM 进行垃圾回收时,不管内存是否充足,都会回收被弱引用关联的对象;而软引用关联着的对象,只有在内存不足的时候 JVM 才会回收该对象。软引用能够用来作缓存,由于当 JVM 内存不足的时候才会被回收;而弱引用适合 Android 上面引用 Activity 等的时候使用,由于 Activity 被销毁不必定是由于内存不足,多是正常的生命周期结束。若是此时使用软引用,而 JVM 内存仍然足够,则仍然会持有 Activity 的引用而形成内存泄漏。
  3. 虚引用在任什么时候候均可能被垃圾回收器回收。

当一个对象再也不被引用的时候,该对象也不必定被回收,理论上它还有一次救赎的机会,即经过覆写 finilize() 方法把对本身的引用从弱变强,即把本身赋值给全局的对象等。由于当对象不可达的时候,只有当 finilize() 没被覆写,或者 finilize() 已经被调用过,则该对象会被回收。不然,它会被放在一个队列中,并在稍后由一个低优先级的 Finilizer 线程执行它。

  • 垃圾收集机制 对象建立,新生代与老年代

实际虚拟机的内存区域就是一整块内存,不区分新生代与老年代。新生代与老年代是垃圾收集器为了使用不一样收集策略而定义的名称。

JVM 内存各个区域的名称

内存分配的策略是:1). 对象优先在Eden分配;2). 大对象直接进入老年代;3). 长期存活对象将进入老年代。

咱们以前有一次线上的问题就是代码中查询了太多的数据,致使大对象直接进入了老年代,查询频繁,致使虚拟机 GC 频繁,进入假死状态(停顿)。

新生代:主要是用来存放新生的对象。通常占据堆的 1/3 空间。因为频繁建立对象,因此新生代会频繁触发 MinorGC 进行垃圾回收。

新生代又分为 Eden 区、ServivorFrom、ServivorTo 三个区。

Eden 区:Java 新对象的出生地(若是新建立的对象占用内存很大,则直接分配到老年代)。当Eden 区内存不够的时候就会触发 MinorGC,对新生代区进行一次垃圾回收。
ServivorTo:保留了一次 MinorGC 过程当中的幸存者。
ServivorFrom:上一次 GC 的幸存者,做为这一次 GC 的被扫描者。

MinorGC 的过程:MinorGC 采用复制算法。首先,把 Eden 和 ServivorFrom 区域中存活的对象复制到 ServicorTo 区域(若是有对象的年龄以及达到了老年的标准,则赋值到老年代区),同时把这些对象的年龄+1(若是 ServicorTo 不够位置了就放到老年区);而后,清空 Eden 和 ServicorFrom 中的对象;最后,ServicorTo 和 ServicorFrom 互换,原 ServicorTo 成为下一次 GC 时的 ServicorFrom 区。

老年代:主要存放应用程序中生命周期长的内存对象。老年代的对象比较稳定,因此 MajorGC 不会频繁执行。MajorGC 前通常都先进行了一次 MinorGC,使得有新生代的对象进入老年代,致使空间不够用时才触发。当没法找到足够大的连续空间分配给新建立的较大对象时也会提早触发一次 MajorGC 进行垃圾回收腾出空间。

当老年代也满了装不下的时候,就会抛出 OOM 异常。

至于老年代究竟使用哪一种垃圾收集算法其实是由垃圾收集器来决定的。老年代、新生代以及新生代的各个内存区域之间的比例并非固定的,咱们可使用参数来配置。

二、虚拟机执行系统

  • 谈谈类加载器 classloader
  • 类加载机制,双亲委派模型

Android 中的类加载器与 Java 中的类加载器基本一致,都分红系统类加载器和用户自定义类加载器两种类型。Java 中的系统类加载器包括,Bootstrap 类加载器,主要用来加载 java 运行时下面的 lib 目录;拓展类加载器 ExtClassLoader,用来加载 Java 运行时的 lib 中的拓展目录;应用程序类加载器,用来加载当前程序的 ClassPath 目录下面的类。其中,引导类加载器与其余两个不一样,它是在 C++ 层实现的,没有继承 ClassLoader 类,也没法获取到。

Android 中的系统类加载器包括,BootClassLoader, 用来加载经常使用的启动类;DexClassLoader 用来加载 dex 及包含 dex 的压缩文件;PathClassLoader 用来加载系统类和应用程序的类。三种类加载器都是在系统启动的过程当中建立的。DexClassLoader 和 PathClassLoader 都继承于 BaseDexClassLoader。区别在于调用父类构造器时,DexClassLoader 多传了一个 optimizedDirectory 参数,这个目录必须是内部存储路径,用来缓存系统建立的 Dex 文件。而 PathClassLoader 该参数为 null,只能加载内部存储目录的 Dex 文件。因此咱们能够用 DexClassLoader 去加载外部的 Apk.

public DexClassLoader(String dexPath, String optimizedDirectory, String libraryPath, ClassLoader parent) {
    super(dexPath, new File(optimizedDirectory), libraryPath, parent);
}
复制代码

DexClassLoader 重载了 findClass() 方法,在加载类时会调用其内部的 DexPathList 去加载。DexPathList 是在构造 DexClassLoader 时生成的,其内部包含了 DexFile。腾讯的 qq 空间热修复技术正是利用了 DexClassLoader 的加载机制,将须要替换的类添加到 dexElements 的前面,这样系统会使用先找到的修复过的类。

private final DexPathList pathList;
    public BaseDexClassLoader(String dexPath, File optimizedDirectory, String libraryPath, ClassLoader parent) {
        super(parent);
        this.originalPath = dexPath;
        this.pathList = new DexPathList(this, dexPath, libraryPath, optimizedDirectory);
    }
    @Override
    protected Class<?> findClass(String name) throws ClassNotFoundException {
        Class clazz = pathList.findClass(name);
        return clazz;
    }
复制代码

本质上不论哪一种加载器加载类的时候的都是分红两个步骤进行的,第一是使用双亲委派模型的规则从资源当中加载类的数据到内存中。一般类都被存储在各类文件中,因此这无非就是一些文件的读写操做。当将数据读取到内存当中以后会调用 defineClass() 方法,并返回类的类型 Class 对象。这个方法底层会调用一个 native 的方法来最终完成类的加载工做。

至于双亲委派模型,这是 Java 中类加载的一种规则,比较容易理解。前提是类加载器之间存在着继承关系,那么当一个类进行加载以前会先判断这个类是否已经存在。若是已经存在,则直接返回对应的 Class 便可。若是不存在则先交给本身的父类进行加载,父类加载不到,而后本身再进行加载。这样一层层地传递下去,一个类的加载将是从父类开始到子类的过程,因此叫双亲委派模型。这种加载机制的好处是:第一,它能够避免重复加载,已经加载一次的类就无需再次加载;第二,更加安全,由于类优先交给父类进行加载,按照传递规则,也就是先交给系统的类进行加载。那么若是有人想要伪造一个 Object 类型,想要蒙混过关的话,显然是逃不过虚拟机的法眼了。

Android 的 ClassLoader 定义在 Dalivk 目录下面,这里是它在 AOSP 中的位置:dalvik-system

  • 动态加载
  • 对动态加载(OSGI)的了解?

OSGI 一种用来实现 Java 模块化的方式,在 2010 年左右的时候比较火,如今用得比较少了。

三、内存模型

梳理下内存模型,组织一下语言

  • JVM 内存模型,内存区域
  • JVM 内存模型

Java 内存模型,即 Java Memory Model,简称 JMM,它是一种抽象的概念,或者是一种协议,用来解决在并发编程过程当中内存访问的问题,同时又能够兼容不一样的硬件和操做系统。

在 Java 内存模型中,全部的变量都存储在主内存。每一个 Java 线程都存在着本身的工做内存,工做内存中保存了该线程用获得的变量的副本,线程对变量的读写都在工做内存中完成,没法直接操做主内存,也没法直接访问其余线程的工做内存。当一个线程之间的变量的值的传递必须通过主内存。

当两个线程 A 和线程 B 之间要完成通讯的话,要经历以下两步:首先,线程 A 从主内存中将共享变量读入线程 A 的工做内存后并进行操做,以后将数据从新写回到主内存中;而后,线程 B 从主存中读取最新的共享变量。

此外,内存模型还规定了

  1. 主内存和工做内存交互的 8 种操做及其规则;
  2. 提供了 voliate 关键字用来,保证变量的可见性,和屏蔽指令重排序;
  3. 对 long 及 double 的特殊规定:读写操做分红两个 32 位操做;
  4. 先行发生原则 (happens-before) 和 as-if-serial 语义(无论怎么重排序,程序的执行结果不能被改变)。

四、Android 虚拟机

  • ART 和 Dalvik (DVM) 的区别

ART 4.4 时发布,5.0 以后默认使用 ART.

  1. ART 在应用安装时会进行预编译 (ahead of time compilation, AOT),将字节码编译成机器码并存储在本地,这样每次运行程序时就无需编译了,提高了效率。缺点是:1).安装耗时更长了;2).占用更多存储空间。7.0 以后,ART 引入 JIT,安装时不会将字节码所有编译成机器码,而是运行时将热点代码编译成机器码。
  2. DVM 是为 32 位 CPU 设计的,ART 支持 64 位且兼容 32 位 CPU.
  3. ART 对垃圾收集机制进行了改进,将 GC 暂停由 2 次改为了 1 次等。
  4. ART 的运行时堆空间划分与 DVM 不一样
  • DVM 与 JVM 的区别
  1. 基于的架构不一样:DVM 基于寄存器,相比于 JVM(基于栈),执行速度更快(由于无需到栈中读取数据)。
  2. 执行的字节码不一样:DVM 在执行的是 dex 文件,通过 class 经 dx 转换以后的。dex 会对 class 进行优化,整个 class,取出冗余信息,加快加载方式。
  3. DVM 容许在有限的空间内同时运行多个进程
  4. DVM 由 Zygote 建立和初始化 Zygote 是一个 DVM 进程,当须要建立一个应用程序时,Zygote 经过 fork 自身来建立新的 DVM 实例。
  5. DVM 有共享机制,不一样应用在运行时能够共享相同的类。
  6. DVM 早期没有使用 JIT 编译器,JIT 就是即时编译器,早期的 DVM 须要通过解释器将 dex 码编译成机器码,效率不高。2.2 以后使用了 JIT,会对热点代码进行编译,生成本地机器码,下次执行到相同的逻辑时,能够直接执行本地机器码,无需每次编译。
  • DVM 与 ART 的诞生

init 进程启动 Zygote 时会调用 app_main.cpp,它会调用 AndroidRuntime 的 start() 函数,在其中经过 startVM() 方法启动虚拟机。在启动虚拟机以前会经过读取系统的属性,判断使用 DVM 仍是 ART 虚拟机实例。


Android 高级面试系列文章,关注做者及时获取更多面试资料

本系列以及其余系列的文章均维护在 Github 上面:Github / Android-notes,欢迎 Star & Fork. 若是你喜欢这篇文章,愿意支持做者的工做,请为这篇文章点个赞👍!

相关文章
相关标签/搜索