内存抖动
内存抖动是指内存频繁地分配和回收,而频繁的 GC 会导致卡顿,严重时和内存泄漏一样会导致 OOM。
回收算法
标记-清除算法 Mark-Sweep
标记-清除算法分为两个阶段,标记(mark)和清除(sweep).
在标记阶段,collector从mutator根对象开始进行遍历,对从mutator根对象可以访问到的对象都打上一个标识,一般是在对象的header中,将其记录为可达对象。
在清除阶段,collector对堆内存(heap memory)从头到尾进行线性的遍历,如果发现某个对象没有标记为可达对象-通过读取对象的header信息,则就将其回收。
接着我们先看下一般垃圾收集动作是怎么被触发的,下面是mutator进行NEW操作的伪代码:
New():
ref <- allocate() //分配新的内存到ref指针
if ref == null
collect() //内存不足,则触发垃圾收集
ref <- allocate()
if ref == null
throw "Out of Memory" //垃圾收集后仍然内存不足,则抛出Out of Memory错误
return ref
atomic collect():
markFromRoots()
sweep(HeapStart,HeapEnd)
而下面是对应的mark算法:
markFromRoots():
worklist <- empty
for each fld in Roots //遍历所有mutator根对象
ref <- *fld
if ref != null && isNotMarked(ref) //如果它是可达的而且没有被标记的,直接标记该对象并将其加到worklist中
setMarked(ref)
add(worklist,ref)
mark()
mark():
while not isEmpty(worklist)
ref <- remove(worklist) //将worklist的最后一个元素弹出,赋值给ref
for each fld in Pointers(ref) //遍历ref对象的所有指针域,如果其指针域(child)是可达的,直接标记其为可达对象并且将其加入worklist中
//通过这样的方式来实现深度遍历,直到将该对象下面所有可以访问到的对象都标记为可达对象。
child <- *fld
if child != null && isNotMarked(child)
setMarked(child)
add(worklist,child)
缺点
Mark-Sweep 算法的标记和清除过程效率都不高。另外, Mark-Sweep 算法没有进行对象的移动,只是单纯的进行对象回收,这样很容易造成内存碎片。
复制算法 Copying
Copying 算法是将内存分成相等的两块区域,然后使用其中的一块。当这块内存不足时候,将存货的对象复制到另一块上,然后清除已使用的这块上所有对象。
缺点:
这样虽然不会产生内存碎片这种问题,但是他的缺点也很明显:内存只有原来的一半。特别是当被使用的一半内存中,对象存活率较高的时候,需要复制的对象,以及复制的次数增加,导致效率低下。
标记压缩算法 Mark-Compact
考虑到 Copying 算法会浪费一半的内存,出现了 Mark-Compact 算法。用 Mark-Sweep 算法中的标记过程,然后让所有存活的对象移到一端,然后直接清理掉端边界以外的内存。
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。
根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,“标记-整理”算法的示意图如下图所示。
分代收集算法
把 Java 堆分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或“标记-整理”算法来进行回收。
注意:Permanent和垃圾回收没什么关系,主要用来存放类,方法信息,也能作为常量沲使用,不同VM不同实现,有些没有这个区
新生代 Young Generation
新生代内存按照 8:1:1 的比例分为一个 Eden Space (亚丹)区和两个 Survivor(夏娃)区,From Space 和 To Space。
新建的对象都是在新生代分配内存。
首先,每次使用 Eden 区和一块 Survivor 区 From Space。(即每次新生代可用内存为 90%)
当 Eden 和 From Space 满时,触发新生代回收 Minor GC ,将 Eden 和 From Space 中还存活着的对象一次性地复制到 To Space 空间上,最后清理掉 Eden 和刚 From Space 空间。然后交换 From Space 和 To Space。
每一次 Minor GC,把存活下来的对象年龄 +1,当某个对象的年龄达到老年的标准,就移到老年代中。
老年代 Old Generation
用于存放新生代中经过 N 次垃圾回收仍然存活的对象,以及 To From 不足时候,担保的对象。
内存比新生代也大很多(大概比例是1:2)。
当老年代内存满时触发回收 Major GC 。
老年代采用 Mark-Compact 算法。
永久代 Permanent Generation
主要存放所有已加载的类信息,方法信息,常量池等等。
并不等同于方法区,只不过是主流的 Sun 公司的 Hotspot JVM 用永久代来实现方法区而已,有些虚拟机没有永久带而用其他机制来实现方法区。
这个区域存放的内容与垃圾回收要回收的Java对象关系并不大。
垃圾收集器
垃圾收集器就是内存回收的具体实现。 Java 虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、 不同版本的虚拟机所提供的垃圾收集器都可能会有很大差别,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。
注: 虚拟机所处的区域,则表示它是属于新生代收集器还是老年代收集器。如果两个收集器之间存在连线,就说明它们可以搭配使用。
预防内存泄漏
数据类型,不要使用比需求更占空间的基本数据类型
循环尽量用foreach,少用iterator, 自动装箱尽量少用
数据结构与算法的解度处理
数据量千级以内可以使用
Sparse数组(key为整数),ArrayMap(key为对象)
性能不如HashMap但节约内存枚举优化
缺点:
每一个枚举值都是一个单例对象,在使用它时会增加额外的内存消耗,所以枚举相比与 Integer 和 String 会占用更多的内存
较多的使用 Enum 会增加 DEX 文件的大小,会造成运行时更多的IO开销,使我们的应用需要更多的空间
特别是分dex多的大型APP,枚举的初始化很容易导致ANR
枚举可以进行改进
public enum SHAPE{ RECTANGLE, TRIANGLE, SQUARE, CIRCLE } public class SHAPE{ public static final int RECTANGLE=0; public static final int TRIANGLE=1; public static final int SQUARE=2; public static final int CIRCLE=3; }
static staticfinal的问题
static会由编译器调用clinit方法进行初始化
static final不需要进行初始化工作,打包在dex文件中可以直接调用,并不会在类初始化申请内存
所以基本数据类型的成员,可以全写成static final字符串的连接尽量少用加号(+)
重复申请内存的问题
同一个方法多次调用,如递归函数 ,回调函数中new对象,读流直接在循环中new对象等
不要在onMeause() onLayout() onDraw() 中去刷新UI(requestLayout)避免GC回收将来要重用的对象
内存设计模式对象沲+LRU算法
Activity组件泄漏
尽量使用IntentService,而不是Service