APP下载

十分良心 全网最详细的Java 自动内存管理机制及效能优化教程

消息来源:baojiabao.com 作者: 发布时间:2026-05-23

报价宝综合消息十分良心 全网最详细的Java 自动内存管理机制及效能优化教程

专注于Java领域优质技术,欢迎关注

作者:涤生_Woo

同样的,先来个思维导图预览一下本文结构。

一图带你看完本文

一、执行时资料区域

首先来看看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 ,那就可以考虑检查一下是不是这方面的原因。

二、内存分配策略

物件的内存分配,往大方向讲,就是在堆上分配(但也可能经过 JIT 编译后被拆散为标量型别并间接地栈上分配),物件主要分配在新生代的 Eden 区上,如果启动了本地执行绪分配缓冲,将按执行绪优先在 TLAB 上分配。少数情况下也可能会直接分配在老年代中,分配的规则并不是固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机器中与内存相关的引数的设定。

2.1 物件优先在 Eden 分配

大多数情况下,物件在新生代 Eden 区中分配。当 Eden 区没有足够的空间进行分配时,虚拟机器将发起一次 Minor GC 。举个例子,看下面的程式码:

执行上面的testAllocation() 程式码,当分配 allocation4 物件的语句时会发生一次 Minor GC ,这次 GC 的结果是新生代 6651KB 变为 148KB ,而总内存占用量则几乎没有减少(因为 allocation1、allocation2、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。

三、内存回收策略

新生代 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.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考虑不同的实现方式来优化内存占用

五、内存检测工具

最后给推荐几个内存检测的工具,具体使用方法,可以自行搜寻。当然除了下面这些工具,应该还有更多更好用的工具,只是我还没有发现,如有建议,可以在文章下面评论留言,大家一起学习分享一下。

SystraceTraceviewAndroid Studio 3.0 的 Android Profiler 分析器LeakCanary

后续

学习资料

《深入理解Java虚拟机器:JVM高阶特性与最佳实践》Android效能优化典范来自:https://www.jianshu.com/p/1086ee575ec8

2019-12-15 05:51:00

相关文章