先来个思惟导图预览一下本文结构。java

一图带你看完本文面试
1、运行时数据区域
首先来看看Java虚拟机所管理的内存包括哪些区域,就像咱们要了解一个房子,咱们得先知道这个房子大致构造。根据《Java虚拟机规范(Java SE 7 版)》的规定,请看下图:算法

Java 虚拟机运行时数据区编程
1.1 程序计数器数组
程序计数器是一块较小的内存空间,它能够看做是当前线程所执行的字节码的行号指示器。缓存
- 因为 Java 虚拟机的多线程是经过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个肯定的时刻,一个处理器(对于多核处理器来讲是一个内核)都只会执行一条线程中的指令。
- 为了线程切换后能恢复到正确的执行位置,每条线程都须要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,咱们称这类内存区域为“线程私有”的内存。
- 此内存区域是惟一一个在 Java 虚拟机规范中没有规定任何 OutOfMemoryError 状况的区域。
1.2 Java 虚拟机栈安全
与程序计数器同样,Java 虚拟机栈也是线程私有的,它的生命周期与线程相同。虚拟机栈描述的是 Java 方法执行的内存模型:每一个方法在执行的同时都会建立一个栈帧用于存储局部变量表、操做数栈、动态连接、方法出口等信息。每个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。请看下图:性能优化

Java 虚拟机栈数据结构
- 有人把 Java 内存区分为堆内存和栈内存,而所指的“栈”就是这里的虚拟机栈,或者说是虚拟机栈中局部变量表部分。
- 局部变量表存放了编译期可知的各类基本数据类型(boolean、byte、char、short、int、float、long、double)、对象引用和 returnAddress 类型(指向了一条字节码指令的地址),其中64位长度的 long 和 double 类型的数据占用2个局部变量空间,其他数据类型只占用1个。
- 操做数栈也常被称为操做栈,它是一个后入先出栈。当一个方法刚刚执行的时候,这个方法的操做数栈是空的,在方法执行的过程当中,会有各类字节码指向操做数栈中写入和提取值,也就是入栈与出栈操做。
- 每一个栈帧都包含一个指向运行时常量池中该栈帧所属方法的引用,持有这个引用是为了支持方法调用过程当中的动态链接。在Class文件的常量池中存有大量的符号引用,字节码中的方法调用指令就以常量池中指向方法的符号引用为参数。这些符号引用一部分会在类加载阶段或第一次使用的时候转化为直接引用,这种转化称为静态解析。另一部分将在每一次的运行期期间转化为直接引用,这部分称为动态链接。
- 当一个方法执行完毕以后,要返回以前调用它的地方,所以在栈帧中必须保存一个方法返回地址。方法退出的过程实际上等同于把当前栈帧出栈,所以退出时可能执行的操做有:恢复上层方法的局部变量表和操做数栈,把返回值(若是有的话)压入调用都栈帧的操做数栈中,调用PC计数器的值以指向方法调用指令后面的一条指令等。
- 虚拟机规范容许具体的虚拟机实现增长一些规范里没有描述的信息到栈帧中,例如与高度相关的信息,这部分信息彻底取决于具体的虚拟机实现。在实际开发中,通常会把动态链接,方法返回地址与其它附加信息所有归为一类,称为栈帧信息。
- 在 Java 虚拟机规范中,规定了两种异常情况:若是线程请求的栈深度大于虚拟机所容许的深度,将抛出 StackOverflowError 异常;若是虚拟机栈能够动态扩展,当扩展时没法申请到足够的内存,就会抛出 OutOfMemoryError 异常。
1.2.1 虚拟机栈溢出多线程
- 若是线程请求的栈深度大于虚拟机所容许的最大深度,将抛出 StackOverflowError 异常。
- 若是虚拟机在扩展栈时没法申请到足够的内存空间,则抛出 OutOfMemoryError 异常。
- 当栈空间没法继续分配时,究竟是内存过小,仍是已使用的栈空间太大,其本质上只是对同一件事情的两种描述而已。
- 系统分配给每一个进程的内存是有限制的,除去 Java 堆、方法区、程序计数器,若是虚拟机进程自己耗费的内存不计算在内,剩下内存就由虚拟机栈和本地方法栈“瓜分”了。每一个线程分配到的栈容量越大,能够创建的线程数量天然就越少,创建线程时就越容易把剩下的内存耗尽。
- 出现 StackOverflowError 异常时有错误栈能够阅读,栈深度在大多数状况下达到1000~2000彻底没有问题,对于正常的方法调用(包括递归),这个深度应该彻底够用了。
- 可是,若是是创建过多线程致使的内存溢出,在不能减小线程数或者更换 64 位虚拟机的状况下,就只能经过减小最大堆和减小栈容量来换取更多的线程。
1.3 本地方法栈
- 本地方法栈与虚拟机栈所发挥的做用很是类似,它们之间的区别是虚拟机栈为虚拟机执行 Java 方法服务,而本地方法栈则为虚拟机栈使用到的 Native 方法服务。
- 与虚拟机栈同样,本地方法栈区域也会抛出 StackOverflowError 和 OutOfMemoryError 异常。
1.4 Java 堆
Java 堆是被全部线程共享的一块内存区域,在虚拟机启动时建立。此内存区域的惟一目的就是存放对象实例,几乎全部的对象实例都在这里分配内存(可是,随着技术发展,全部对象都分配在堆上也渐渐变得不是那么“绝对”了)。请看下图:

Generational Heap Memory 模型
- 对于大多数应用来讲,Java 堆是 Java 虚拟机所管理的内存中最大的一块。
- Java 堆是垃圾收集器管理的主要区域,也被称为“GC堆”。
- Java 堆能够细分为新生代、老年代、永久代;再细致一点能够分为 Eden、From Survivor、To Survivor、Tenured、Permanent 。
- Java 堆能够处于物理上不连续的内存空间中,只要逻辑上是连续的便可,就像磁盘空间同样。
- 从内存分配的角度来看,线程共享的 Java 堆中可能划分出多个线程私有的分配缓冲区(TLAB)。
- 若是在堆中没有内存完成实例分配,而且堆也没法再扩展时,将会抛出 OutOfMemoryError 异常。
1.4.1 Java 堆溢出
- Java 堆用于存储对象实例,只要不断地建立对象,而且保证 GC Roots 到对象之间有可达路径来避免垃圾回收机制清除这些对象,那么在对象数量到达最大堆的容量限制后就会产生内存溢出异常。
- Java 堆内存的 OOM 异常是实际应用中常见的内存溢出异常状况。当出现 Java 堆内存溢出时,异常堆栈信息 “java.lang.OutOfMemoryError” 会跟着进一步提示 “Java heap space” 。
- 一般是先经过内存映像分析工具对 Dump 出来的堆转储快照进行分析,重点是确认内存中的对象是不是必要的,也就是要先分清楚究竟是出现了内存泄漏仍是内存溢出。
- 若是是内存泄漏,可进一步经过工具查看泄露对象到 GC Roots 的引用链。因而就能找到泄露对象的类型信息及 GC Roots 引用链的信息,就能够比较准确地定位出泄露代码的位置。
- 若是不存在泄露,就是内存中的对象确实都还必须存活着,那就应当检查虚拟机的堆参数(-Xmx 与 -Xms),与机器物理内存对比看是否还能够调大,从代码上检查是否存在某些对象生命周期过长、持有状态时间过长的状况,尝试减小程序运行期的内存消耗。
1.5 方法区
方法区与 Java 堆同样,是各个线程共享的内存区域,它用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
- Java 虚拟机规范对方法区的限制很是宽松,除了和 Java 堆同样不须要连续的内存和能够选择固定大小或者可扩展外,还能够选择不实现垃圾收集。
- 这区域的内存回收目标主要是针对常量池的回收和对类型的卸载。
- 当方法区没法知足内存分配需求时,将抛出 OutOfMemoryError 异常。
1.5.1 运行时常量池
- 运行时常量池是方法区的一部分。
- 常量池用于存放编译期生成的各类字面量和符号引用,这部份内容将在类加载后进入方法区的运行时常量池中存放。
- 运行时常量池相对于 Class 文件常量池的一个重要特征是具有动态性,Java 语言并不要求常量必定只有编译期才能产生,也就是并不是预置入 Class 文件中常量池的内容才能进入方法区运行时常量池,运行期间也可能将新的常量放入池中,这种特性被开发人员利用得比较多的即是 String 类的 intern() 方法。
- 当常量池没法再申请到内存时会抛出 OutOfMemoryError 异常。在 OutOfMemoryError 后面跟随的提示信息时 “PermGen space” 。
1.6 直接内存
- 直接内存并非虚拟机运行时数据区的一部分,也不是 Java 虚拟机规范中定义的内存区域。可是这部份内存也被频繁地使用,并且也可能致使 OutOfMemoryError 异常出现。
- NIO 类,一种基于通道与缓冲区的 I/O 方式,它可使用 Native 函数库直接分配堆外内存,而后经过一个存储在 Java 堆中的 DirectByteBuffer 对象做为这块内存的引用进行操做。这样能在一些场景中显著提升性能,由于避免了在 Java 堆和 Native 堆中来回复制数据。
- 本机直接内存的分配不会受到 Java 堆大小的限制,可是,既然是内存,确定仍是会受到本机总内存(包括 RAM 以及 SWAP 区或者分页文件)大小以及处理器寻址空间的限制。
- 由 DirectMemory 致使的内存溢出,一个明显的特征是在 Heap Dump 文件中不会看见明显的异常,若是咱们发现 OOM 以后 Dump 文件很小,而程序中有直接或间接使用了 NIO ,那就能够考虑检查一下是否是这方面的缘由。
2、内存分配策略
对象的内存分配,往大方向讲,就是在堆上分配(但也可能通过 JIT 编译后被拆散为标量类型并间接地栈上分配),对象主要分配在新生代的 Eden 区上,若是启动了本地线程分配缓冲,将按线程优先在 TLAB 上分配。少数状况下也可能会直接分配在老年代中,分配的规则并非固定的,其细节取决于当前使用的是哪种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。
2.1 对象优先在 Eden 分配
大多数状况下,对象在新生代 Eden 区中分配。当 Eden 区没有足够的空间进行分配时,虚拟机将发起一次 Minor GC 。举个例子,看下面的代码:

执行上面的testAllocation() 代码,当分配 allocation4 对象的语句时会发生一次 Minor GC ,此次 GC 的结果是新生代 6651KB 变为 148KB ,而总内存占用量则几乎没有减小(由于 allocation一、allocation二、allocation3 三个对象都是存活的,虚拟机几乎没有找到可回收的对象)。此次 GC 发生的缘由是给 allocation4 分配内存时,发现 Eden 已经被占用了 6MB ,剩余空间已不足以分配 allocation4 所需的 4MB 内存,所以发生 Minor GC 。GC 期间虚拟机又发现已有的 3 个 2MB 大小的对象所有没法放入 Survivor 空间(从上图中可看出 Survivor 空间只有 1MB 大小),因此只好经过分配担保机制提早转移到老年代去。
2.2 大对象直接进入老年代
- 所谓的对象是指,须要大量连续内存空间的 Java 对象,最典型的大对象就是那种很长的字符串以及数组。常常出现大对象容易致使内存还有很多空间时就提早触发垃圾收集以获取足够的连续空间来“安置”它们。
- 虚拟机提供了一个 -XX:PretenureSizeThreshold 参数,令大于这个设置值的对象直接在老年代分配。这样作的目的是避免在 Eden 区及两个 Survivor 区之间发生大量的内存复制(新生代采用复制算法收集内存)。
2.3 长期存活的对象将进入老年代
既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别到哪些对象应放在新生代,哪些对象应放在老年代中。为了作到这点,虚拟机给每一个对象定义了一个对象年龄计数器。若是对象在 Eden 出生并通过第一次 Minor GC 后仍然存活,而且能被 Survivor 容纳的话,将被移动到 Survivor 空间中,而且对象年龄设为 1 。对象在 Survivor 区中每“熬过”一次 Minor GC,年龄就增长1岁,当它的年龄增长到必定程度(默认15岁),就会被晋升到老年代中。对象晋升老年代的年龄阈值,能够经过参数 -XX:MaxTenuringThreshold 设置。
2.4 动态对象年龄断定
为了能更好地适应不一样程序的内存情况,虚拟机并非永远地要求对象的年龄必须达到了 MaxTenuringThreshold 才能晋升老年代,若是在 Survivor 空间中相同年龄全部对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就能够直接进入老年代,无须等到 MaxTenuringThreshold 中的要求的年龄。
2.5 空间分配担保机制
- 在发生 Minor GC 以前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代全部对象总空间,若是这个条件成立,那么 Minor GC 能够确保是安全的。若是不成立,则虚拟机会查看 HandlePromotionFailure 设置值是否容许担保失败。若是容许,那么会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,若是大于,将尝试着进行一次 Minor GC ,尽管此次 Minor GC 是有风险的;若是小于,或者 HandlePromotionFailure 设置不容许冒险,那此次也要改成进行一次 Full GC。
- 上面提到的“冒险”指的是,因为新生代使用复制收集算法,但为了内存利用率,只使用其中一个 Survivor 空间来做为轮换备份,所以当出现大量对象在 Minor GC 后仍然存活的状况,把 Survivor 没法容纳的对象直接进入老年代。老年代要进行这样的担保,前提是老年代自己还有容纳这些对象的剩余空间,一共有多少对象会活下来在实际完成内存回收以前是没法明确知道的,因此只好取以前每一次回收晋升到老年代对象容量的平均大小值做为经验值,与老年代的剩余空间进行比较,决定是否进行 Full GC 来让老年代腾出更多空间。
- 取平均值进行比较其实仍然是一种动态几率的手段,也就是说,若是某次 Minor GC 存活后的对象突增,远远高于平均值的话,依然会致使担保失败。
- 若是出现了HandlePromotionFailure 失败,那就只好在失败后从新发起一次 Full GC。虽然担保失败时绕的圈子是最大的,但大部分状况下都仍是会将 HandlePromotionFailure 开关打开,避免 Full GC 过于频繁。
- 但在 JDK 6 Update 24 以后,HandlePromotionFailure 参数不会再影响到虚拟机的控件分配担保策略,只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行 Minor GC ,不然将进行 Full GC。
3、内存回收策略
- 新生代 GC(Minor GC) :指发生在新生代的垃圾收集动做,由于 Java 对象大多都具有朝生夕灭的特性,因此 Minor GC 很是频繁,通常回收速度也比较快。
- 老年代 GC(Major GC / Full GC):值发生在老年代的 GC,出现了 Major GC,常常会伴随至少一次的 Minor GC(但非绝对)。Major GC 的速度通常会比 Minor GC 慢 10 倍以上。
3.1 内存回收关注的区域
- 上面已经介绍 Java 内存运行时区域的各个部分,其中程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭。
- 栈中的栈帧随着方法的进入和退出而有条不紊地执行者出栈和入栈操做。每个栈帧中分配多少内存基本上是在类结构肯定下来时就已知的。
- 所以这几个区域的内存分配和回收都具有肯定性,在这几个区域内就不须要过多考虑回收的问题,由于方法结束或者线程结束时,内存天然就跟随着回收了。
- 而 Java 堆和方法区则不同,一个接口中的多个实现类须要的内存可能不同,一个方法中的多个分支须要的内存也可能不同,咱们只有在程序处于运行期间时才能知道会建立哪些对象,这部份内存的分配和回收都是动态的,垃圾收集器所关注的是这部份内存。
3.2 对象存活判断
3.2.1 引用计数算法
- 给对象添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任什么时候刻计数器为 0 的对象就是不可能再被使用的。
- 这种算法的实现简单,断定效率也很高,在大部分状况下它都是一个不错的算法,但它很难解决对象之间相互循环引用的问题。
- 举个例子,对象 objA 和 objB 都有字段 instance,赋值令 objA.instance = objB 及 objB.instance = objA ,除此以外,这两个对象再无任何引用,实际上,这两个对象已经不可能再被访问,可是它们由于相互引用着对方,致使它们的引用计数都不为 0,因而引用计数算法没法通知 GC 收集器回收它们。
3.2.2 可达性分析算法
- 这个算法的基本思路就是经过一系列额称为“GC Roots” 的对象做为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到 GC Roots 没有任何引用链相连或者说这个对象不可达时,则证实此对象是不可用的。
- 在 Java 语言中,可做为 GC Roots 的对象包括如下:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中 JNI 引用的对象
请看下图:

可达性分析算法
3.3 方法区的回收
- 方法区(HotSpot 虚拟机中的永久代)的垃圾收集主要回收两部份内容:废弃常量和无用的类。回收废弃常量与回收 Java 堆的对象很是相似。
- 断定一个类是不是“无用的类”须要同时知足下面3个条件:
- 该类的全部的实例都已经被回收,也就是 Java 堆中不存在该类的任何实例。
- 加载该类的 ClassLoader 已经被回收。
- 该类对应的 java.lang.Class 对象没有在任何地方被引用,没法在任何地方经过反射访问该类的方法。
- 虚拟机能够对知足上述3个条件的无用类进行回收,这里说的仅仅是“能够”,而并非和对象同样,不使用了就必然回收。
3.4 垃圾收集算法
3.4.1 标记—清除算法
- 算法分为 “标记” 和 “清除” 两个阶段:首先标记出全部须要回收的对象,在标记完成后统一回收全部被标记的对象。
- 它主要有两个不足的地方:一个是效率问题,标记和清除两个过程的效率都不高;另外一个是空间问题,标记清除以后会产生大量不连续的内存碎片,空间碎片太多可能会致使之后在程序运行过程当中须要分配较大对象时,没法找到足够的连续内存而得不到提早触发另外一次垃圾收集动做。
- 这是最基础的收集算法,后续的收集算法都是基于这种思路并对其不足进行改进而获得的。

“标记—清除”算法示意图
3.4.2 复制算法
- 为了解决效率问题,“复制”算法应运而生,它将可用内存按容量划分为大小相等的两块,每次只使用其中一块。
- 当这一块的内存用完了,就将还存活着的对象复制到另一块上面,而后再把已使用过的内存空间一次清理掉。
- 这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂状况,只要移动堆顶指针,按顺序分配内存便可,实现简单,运行高效。
- 不足之处是,将内存缩小为原来的一半,代价过高。

复制算法示意图
举个优化例子:新生代中的对象98%是“朝生夕死”的,因此并不须要按照 1:1 的比例来划份内存空间,而是将内存分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 和其中一块 Survivor。当回收时,将 Eden 和 Survivor 中还存活着的对象一次性地复制到另外一块 Survivor 空间上,最后清理掉 Eden 和刚才用过的 Survivor 空间。
再举个优化例子:将 Eden 和 Survivor 的大小比例设为 8:1 ,也就是每次新生代中可用内存空间为整个新生代容器的 90%,只有10% 的内存做为保留区域。固然 98% 的对象可回收只是通常场景下的数据,咱们没有办法保证每次回收都只有很少于 10% 的对象存活,当 Survivor 空间不够用时,须要依赖其余内存(这里指老年代)进行分配担保(空间分配担保机制在上面,了解一下)。
3.4.3 标记—整理算法
复制收集算法在对象存活率较高时就要进行较多的复制操做,效率将会变低。因此在老年代通常不能直接选用复制收集算法。
- 根据老年代的特色,“标记—整理” 算法应运而生。
- 标记过程仍然与 “标记—清除” 算法同样,但后续步骤不是直接对可回收对象进行清理,而是让全部存活的对象都向一端移动,而后直接清理掉端边界之外的内存。

“标记—整理”算法示意图
3.4.4 分代收集算法
- 根据对象存活周期的不一样将内存划分为几块,通常是把 Java 堆分为新生代和老年代,这样就能够根据各个年代的特色采用最适当的收集算法。
- 在新生代中,每次垃圾收集时都发现有大批对象死去,只有少许存活,那就选用复制算法,只须要付出少许存活对象的复制成本就能够完成收集。
- 而老年代中由于对象存活率高、没有额外空间对它进行分配担保,就必须使用 “标记—清除” 或者 “标记—整理” 算法来进行回收。
- 当前商业虚拟机的垃圾收集都采用 “分代收集” 算法。
4、编程中的内存优化
相信你们在编程中都会注意到内存使用的问题,下面我就简单列一下在实际操做当中须要注意的地方。
4.1 减少对象的内存占用
咱们能够考虑使用 ArrayMap / SparseArray 而不是 HashMap 等传统数据结构。(我在老项目中,根据 Lint 提示,将 HashMap 替换成 ArrayMap / SparseArray 以后,在 Android Profiler 中显示运行时内存比以前直接少了几M,仍是挺可观的。)
- 避免使用 Enum
- 减少 Bitmap 对象的内存占用
- inSampleSize :缩放比例,在把图片载入内存以前,咱们须要先计算出一个合适的缩放比例,避免没必要要的大图载入。
- decode format:解码格式,选择 ARGB_8888 / RBG_565 / ARGB_4444 / ALPHA_8,存在很大差别。
- 使用更小的图片:尽可能使用更小的图片不只仅能够减小内存的使用,还能够避免出现大量的 InflationException。
4.2 内存对象的重复利用
- 复用系统自带的资源:Android系统自己内置了不少的资源,例如字符串/颜色/图片/动画/样式以及简单布局等等,这些资源均可以在应用程序中直接引用。
- 注意在 ListView / GridView 等出现大量重复子组件的视图里面对 ConvertView 的复用
- Bitmap 对象的复用
- 避免在 onDraw 方法里面执行对象的建立:相似 onDraw() 等频繁调用的方法,必定须要注意避免在这里作建立对象的操做,由于他会迅速增长内存的使用,并且很容易引发频繁的 GC,甚至是内存抖动。
- StringBuilder:在有些时候,代码中会须要使用到大量的字符串拼接的操做,这种时候有必要考虑使用 StringBuilder 来替代频繁的 “+” 。
4.3 避免对象的内存泄露
- 注意 Activity 的泄漏
- 内部类引用致使 Activity 的泄漏
- Activity Context 被传递到其余实例中,这可能致使自身被引用而发生泄漏。
- 考虑使用 Application Context 而不是 Activity Context :对于大部分非必须使用 Activity Context 的状况(Dialog 的 Context 就必须是 Activity Context),咱们均可以考虑使用 Application Context 而不是 Activity 的 Context,这样能够避免不经意的 Activity 泄露。
- 注意临时 Bitmap 对象的及时回收:例如临时建立的某个相对比较大的 bitmap 对象,在通过变换获得新的 bitmap 对象以后,应该尽快回收原始的 bitmap,这样可以更快释放原始 bitmap 所占用的空间。
- 注意监听器的注销:在 Android 程序里面存在不少须要 register 与 unregister 的监听器,咱们须要确保在合适的时候及时 unregister 那些监听器。本身手动 add 的 listener,须要记得及时 remove 这个 listener。
- 注意缓存容器中的对象泄漏:咱们为了提升对象的复用性把某些对象放到缓存容器中,但是若是这些对象没有及时从容器中清除,也是有可能致使内存泄漏的。
- 注意 WebView 的泄漏:一般根治这个问题的办法是为 WebView 开启另一个进程,经过 AIDL 与主进程进行通讯,WebView 所在的进程能够根据业务的须要选择合适的时机进行销毁,从而达到内存的完整释放。
- 注意 Cursor 对象是否及时关闭
4.4 内存使用策略优化
- 资源文件须要选择合适的文件夹进行存放
- Try catch 某些大内存分配的操做:在某些状况下,咱们须要事先评估那些可能发生 OOM 的代码,对于这些可能发生 OOM 的代码,加入 catch 机制,能够考虑在 catch 里面尝试一次降级的内存分配操做。例如 decode bitmap 的时候,catch 到 OOM,能够尝试把采样比例再增长一倍以后,再次尝试 decode。
- 谨慎使用 static 对象:由于static的生命周期过长,和应用的进程保持一致,使用不当极可能致使对象泄漏。
- 特别留意单例对象中不合理的持有:由于单例的生命周期和应用保持一致,使用不合理很容易出现持有对象的泄漏。
- 珍惜Services资源:建议使用 IntentService
- 优化布局层次,减小内存消耗:越扁平化的视图布局,占用的内存就越少,效率越高。咱们须要尽可能保证布局足够扁平化,当使用系统提供的 View 没法实现足够扁平的时候考虑使用自定义 View 来达到目的。
- 谨慎使用 “抽象” 编程
- 使用 nano protobufs 序列化数据
- 谨慎使用依赖注入框架
- 谨慎使用多进程
- 使用 ProGuard 来剔除不须要的代码
- 谨慎使用第三方 libraries
- 考虑不一样的实现方式来优化内存占用
5、内存检测工具
最后给推荐几个内存检测的工具,具体使用方法,能够自行搜索。固然除了下面这些工具,应该还有更多更好用的工具,只是我尚未发现,若有建议,能够在文章下面评论留言,你们一块儿学习分享一下。
- Systrace
- Traceview
- Android Studio 3.0 的 Android Profiler 分析器
- LeakCanary
后续
学习资料