在学会内存性能优化之前我们得先了解内存如何回收,在《Android性能:内存篇之虚拟机概论》我们已经了解了虚拟机的概念及JVM结构体系与内存空间,在《Android性能:内存篇之进程内存管理》中已了解到系统如何通过管理进程处理内存,接下来我们详细了解进程内部如果管理内存。
一般来说,程序使用内存的方式遵循先向操作系统申请一块内存,使用内存,使用完毕之后释放内存归还给操作系统。然而在传统的C/C++等要求显式释放内存的编程语言中,记得在合适的时候释放内存是一个很有难度的工作,因此Java等编程语言都提供了基于垃圾回收算法的内存管理机制。
Garbage Collection(GC)
Garbage Collection(垃圾回收机制)主要是处理 Java堆Heap ,和部分方法区内的对象。而栈区的数据,在超出作用域后会被JVM自动释放掉,所以其不在JVM GC的管理范围内。
Stop-the-world
在学习Java GC 之前,我们需要记住一个单词:stop-the-world 。它会在任何一种GC算法中发生。Stop-the-world 意味着JVM因为需要执行GC而停止了应用程序的执行。当stop-the-world 发生时,除GC所需的线程外,所有的线程都进入等待状态,直到GC任务完成。GC优化很多时候就是减少stop-the-world 的发生。
在JVM规范中并没有明确GC的运作方式,各个厂商可以采用不同的方式去实现垃圾回收器。这里讨论几种常见的GC算法。常见的垃圾回收算法有引用计数法(Reference Counting)、标注并清理(Mark and Sweep GC)、拷贝(Copying GC)、标注并整理回收法(Mark and COMPACT GC)和逐代回收(Generational GC)等算法,其中Android系统采用的是标注并删除和拷贝GC,并不是大多数JVM实现里采用的逐代回收算法。由于几个算法各有优缺点,所以在很多垃圾回收实现中,常常可以看到将几种算法合并使用的场景,本节将一一讲解这几个算法。
1. 引用计数回收法(Reference Counting GC)
引用计数法的原理很简单,即记录每个对象被引用的次数。每当创建一个新的对象,或者将其它指针指向该对象时,引用计数都会累加一次;而每当将指向对象的指针移除时,引用计数都会递减一次,当引用次数降为0时,删除对象并回收内存(采用这种算法的较出名的框架有微软的COM框架)
然而引用计数回收算法有一个很大的弱点,就是无法有效处理循环引用的问题。
2. 可达性分析算法(Rearchability Analysis)
主流的商业语言(Java,C#)都是通过可达性分析来判定对象是否存活的。
算法思路:通过一系列称为GC Roots的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何的引用链,(也就是从GC Roots到这个对象不可达),那么证明此对象是不可用的。
所谓“GC roots”,或者说tracing GC的“根集合”,就是一组必须活跃的引用。
可达状态:在一个对象创建后,有一个以上的引用变量引用它,那它就处于可达状态。
可恢复状态:对象不再有任何的引用变量引用它,它将先进入可恢复状态,系统会调用finalize()方法进行资源整理,发现有一个以上引用变量引用该对象,则这个对象又再次变为可达状态,否则会变成不可达状态。
不可达状态:当对象的所有引用都被切断,且系统调用 finalize() 方法进行资源整理后该对象依旧没变为可达状态,则这个对象将永久性失去引用并且变成不可达状态,系统才会真正的去回收该对象所占用的资源。
要注意的是,不可达对象不等价于可回收对象,不可达对象变为可回收对象至少要经过两次标记过程。两次标记后仍然是可回收对象,则将面临回收。
Java中可作为GC Roots的对象:
2. 标注并清理回收法(Mark and Sweep GC)
标注并清理回收法中,程序在运行的过程中不停的创建新的对象并消耗内存,直到内存用光,这时再要创建新对象时,系统暂停其它组件的运行,触发GC线程启动垃圾回收过程。内存回收的原理很简单,就是从所谓的"GC Roots"集合开始,将内存整个遍历一次,保留所有可以被GC Roots直接或间接引用到的对象,而剩下的对象都当作垃圾对待并回收。
算法通常分为两个主要的步骤:
标注(Mark)阶段:针对GC Roots中的每一个对象,采用递归调用的方式处理其直接和间接引用到的所有对象;
清理(SWEEP)阶段:即执行垃圾回收过程,留下有用的对象,在这个阶段,GC线程遍历整个内存,将所有没有标注的对象(即垃圾)全部回收,并将保留下来的对象的标志清除掉,以便下次GC过程中使用。
这个方法的优点是很好地处理了引用计数中的循环引用问题,而且在内存足够的前提下,对程序几乎没有任何额外的性能开支(如不需要维护引用计数的代码等),然而它的一个很大的缺点就是在执行垃圾回收过程中,需要中断进程内其它组件的执行。
3. 拷贝回收法(Copying GC)
为了解决Mark-Sweep算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉
回收算法的优点在于内存分配速度快,而且还有可能实现低中断,因为在垃圾回收过程中,从一块内存拷贝存活对象到另一块内存的同时,还可以满足新的内存分配请求,但其缺点是需要有额外的一个内存空间。不过对于回收算法的缺点,也可以通过操作系统地虚拟内存提供的地址空间申请和提交分布操作的方式实现优化,因此在一些JVM实现中,其Eden区域内的垃圾回收采用此算法。
4. 标注并整理回收法(Mark and COMPACT GC)
结合了以上两个算法,为了避免缺陷而提出。标记阶段和Mark-Sweep算法相同,标记后不是清理对象,而是将存活对象移向内存的一端,然后清除端边界外的对象。
在清理(SWEEP)过程中,会执行内存中移动存活的对象,使其排列的更紧凑。在这种算法中,虚拟机在内存中依次排列和保存对象,可以想象GC组件在内部保存了一个虚拟的指针 – 下个对象分配的起始位置 ,其GC内存堆中已经分配有3个对象,因此"下个对象分配的起始位置"指向已分配对象的末尾,新的对象的起始位置将从这里开始。这个算法减少内存碎片化问题。
5. 逐代回收法(Generational GC)
也是标注法的一个变种,标注法最大的问题就是中断的时间过长,此算法是对标注法的优化基于下面几个发现:
大部分对象创建完很快就没用了 – 即变成垃圾;
每次GC收集的90%的对象都是上次GC后创建的;
如果对象可以活过一个GC周期,那么它在后续几次GC中变成垃圾的几率很小,因此每次在GC过程中反复标注和处理它是浪费时间。
可以将逐代回收法看成拷贝GC算法的一个扩展,一开始所有的对象都是分配在"Young对象池" 中。
第一次垃圾回收过后,垃圾回收算法一般采用标注并清理算法,存活的对象会移动到"Old对象池"中– 在JVM中其被称Tenured,而后面新创建的对象仍然在"Young对象池"中创建,这样进程不停地重复前面两个步骤。等到"Old对象池"也快要被填满时,虚拟机此时再在"老一代对象池"中执行垃圾回收过程释放内存。
在逐代GC算法中,由于"Young对象池"中的回收过程很快 – 只有很少的对象会存活,而执行时间较长的"Old对象池"中的垃圾回收过程执行不频繁,实现了很好的平衡,因此大部分虚拟机,如JVM、.NET的CLR都采用这种算法。
另外,不要忘记在Java基础:Java虚拟机(JVM)中提到过的处于方法区的永生代(Permanet Generation),就是“方法区”,它用来存储class类,常量,方法描述等。对永生代的回收主要包括废弃常量和无用的类。
强引用 :创建一个对象并把这个对象直接赋给一个变量,不管系统资源多么紧张,强引用的对象都不会被回收,即使他以后不会再用到。
软引用 :通过SoftReference修饰的类,内存非常紧张的时候会被回收,其他时候不会被回收,在使用之前要判断是否为null从而判断他是否已经被回收了。
弱引用 :通过WeakReference修饰的类,不管内存是否足够,系统垃圾回收时必定会回收。
虚引用 :不能单独使用,主要是用于追踪对象被垃圾回收的状态。通过PhantomReference修饰和引用队列ReferenceQueue类联合使用实现。
Android内存回收算法
在Android Delvik中 ,实现了标注与清理(Mark and Sweep)和拷贝GC,但是具体使用什么算法是在编译期决定的,无法在运行的时候动态更换。关于Android ART 垃圾回收机制,将在下篇文章详聊。