内存管理属于基础知识组织下语言便可,内存模型放在 Java 并发相关;虚拟机执行系统是重点,包括,类加载机制(类的加载、校验阶段,与热补丁原理相关)java
学习这一块的内容能够参考:android
System.gc()
区别System.gc()
函数的做用只是提醒虚拟机:程序员但愿进行一次垃圾回收。可是它不能保证垃圾回收必定会进行,并且具体何时进行是取决于具体的虚拟机的,不一样的虚拟机有不一样的对策。git
一个类从被加载到虚拟机内存到卸载的整个生命周期包括:加载-验证-准备-解析-初始化-使用-卸载
7 个阶段。其中 验证-准备-解析
3 个阶段称为链接。程序员
加载发生在类被使用的时候,若是一个类以前没有被加载,那么就会执行加载逻辑,好比当使用new 建立类、调用静态类对象和使用反射的时候等。加载过程主要工做包括:1). 从磁盘或者网络中获取类的二进制字节流;2). 将该字节流的静态存储结构转换为方法取的运行时数据结构;3). 在内存中生成表示这个类的 Class 对象,做为方法区访问该类的各类数据结构的入口。github
验证阶段会对加载的字节流中的信息进行各类校验以确保它符合JVM的要求。面试
准备阶段会正式为类变量分配内存并设置类变量的初始值。注意这里分配内存的只包括类变量,也就是静态的变量(实例变量会在对象实例化的时候分配在堆上),而且这里的设置初始值是指‘零值’,好比int类型的会被初始化为 0,引用类型的会被初始化为 null,即便你在代码中为其赋了值。算法
解析阶段是将常量池中的符号引用替换为直接引用的过程。符号引用与虚拟机实现的布局无关,引用的目标并不必定要已经加载到内存中。各类虚拟机实现的内存布局能够各不相同,可是它们能接受的符号引用必须是一致的,只要能正肯定位到它们在内存中的位置就行。直接引用能够是指向目标的指针,相对偏移量或是一个能间接定位到目标的句柄。若是有了直接引用,那引用的目标一定已经在内存中存在。编程
初始化是执行类构造器 <client>
方法的过程。<client>
方法是由编译器自动收集类中的类变量的赋值操做和静态语句块中的语句合并而成的。虚拟机会保证 <client>
方法执行以前,父类的 <client>
方法已经执行完毕。数组
内存区域大体的分布图,与线程对应以后的分布图缓存
图中由浅蓝色标识的部分是全部线程共享的数据区;淡紫色标识的部分是每一个线程私有的数据区域
。
线程私有
,用来指示当前线程所执行的字节码的行号,就是用来标记线程如今执行的代码的位置;对 Java 方法,它存储的是字节码指令的地址;对于 Native 方法,该计数器的值为空。线程私有
,与线程同时建立,总数与线程关联,表明Java方法执行的内存模型。每一个方法执行时都会建立一个栈桢来存储方法的的变量表、操做数栈、动态连接方法、返回值、返回地址等信息。一个方法的执行和退出就是用一个栈帧的入栈和出栈表示的。一般咱们不容许你使用递归就是由于,方法就是一个栈,太多的方法只执行而没有退出就会致使栈溢出,不过能够经过尾递归优化。栈又分为虚拟机栈和本地方法栈,一个对应 Java 方法,一个对应 Native 方法。几乎全部的对象实例(包括数组)都在上面分配
。它是垃圾收集器的主要管理区域,所以也叫 GC 堆。它其实是一块内存区域,因为一些收集算法的缘由,又将其细化分为新生代和老年代等。若是在堆中没有内存完成实例分配,而且堆也没法再扩展时,将会抛出 OutOfMemoryError 异常。运行时常量池
是方法区的一部分,它用于存放编译器生成的各类字面量和符号引用,好比字符串常量等。根据 Java 虚拟机规范的规定,当方法区没法知足内存分配需求时,将抛出 OutOfMemoryError 异常。四种引用类型:强引用、软引用、弱引用和虚引用。
new
关键字建立一个对象的时候,这个对象就是强引用的,它绝对不会被回收,即便内存耗尽。你能够经过将其置为 null
来弱化对其的引用,但何时被回收还要取决于 GC 算法。SoftReference<T>
和 WeakReference<T>
来使用它们,它们的区别在于后者更弱一些。当 JVM 进行垃圾回收时,不管内存是否充足,都会回收被弱引用关联的对象;而软引用关联着的对象,只有在内存不足的时候 JVM 才会回收该对象。软引用能够用来作缓存,由于当 JVM 内存不足的时候才会被回收;而弱引用适合 Android 上面引用 Activity 等的时候使用,由于 Activity 被销毁不必定是由于内存不足,多是正常的生命周期结束。若是此时使用软引用,而 JVM 内存仍然足够,则仍然会持有 Activity 的引用而形成内存泄漏。当一个对象再也不被引用的时候,该对象也不必定被回收,理论上它还有一次救赎的机会,即经过覆写 finilize()
方法把对本身的引用从弱变强,即把本身赋值给全局的对象等。由于当对象不可达的时候,只有当 finilize()
没被覆写,或者 finilize()
已经被调用过,则该对象会被回收。不然,它会被放在一个队列中,并在稍后由一个低优先级的 Finilizer 线程执行它。
实际虚拟机的内存区域就是一整块内存,不区分新生代与老年代。新生代与老年代是垃圾收集器为了使用不一样收集策略而定义的名称。
内存分配的策略是: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 异常。
至于老年代究竟使用哪一种垃圾收集算法其实是由垃圾收集器来决定的。老年代、新生代以及新生代的各个内存区域之间的比例并非固定的,咱们可使用参数来配置。
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 一种用来实现 Java 模块化的方式,在 2010 年左右的时候比较火,如今用得比较少了。
梳理下内存模型,组织一下语言
Java 内存模型,即 Java Memory Model,简称 JMM
,它是一种抽象的概念,或者是一种协议,用来解决在并发编程过程当中内存访问的问题,同时又能够兼容不一样的硬件和操做系统。
在 Java 内存模型中,全部的变量都存储在主内存。每一个 Java 线程都存在着本身的工做内存,工做内存中保存了该线程用获得的变量的副本,线程对变量的读写都在工做内存中完成,没法直接操做主内存,也没法直接访问其余线程的工做内存。当一个线程之间的变量的值的传递必须通过主内存。
当两个线程 A 和线程 B 之间要完成通讯的话,要经历以下两步:首先,线程 A 从主内存中将共享变量读入线程 A 的工做内存后并进行操做,以后将数据从新写回到主内存中;而后,线程 B 从主存中读取最新的共享变量。
此外,内存模型还规定了
ART 4.4 时发布,5.0 以后默认使用 ART.
寄存器
,相比于 JVM(基于栈),执行速度更快(由于无需到栈中读取数据)。init 进程启动 Zygote 时会调用 app_main.cpp
,它会调用 AndroidRuntime 的 start()
函数,在其中经过 startVM()
方法启动虚拟机。在启动虚拟机以前会经过读取系统的属性,判断使用 DVM 仍是 ART 虚拟机实例。
Android 高级面试系列文章,关注做者及时获取更多面试资料,
本系列以及其余系列的文章均维护在 Github 上面:Github / Android-notes,欢迎 Star & Fork. 若是你喜欢这篇文章,愿意支持做者的工做,请为这篇文章点个赞👍!