Skip to content

Latest commit

 

History

History
163 lines (90 loc) · 8.22 KB

Android_GC.md

File metadata and controls

163 lines (90 loc) · 8.22 KB

内存模型

JAVA是在JVM所虚拟出的内存环境中运行的,JVM的内存可分为三个区:堆(heap)、栈(stack)和方法区(method)。

  • 栈(stack):是一种简单的数据结构,其最显著的特征是:LIFO(Last In, First Out, 后进先出)。栈中只存放基本类型和对象的引用(不是对象)

  • 堆(heap):堆内存用于存放由new创建的对象和数组。在堆中分配的内存,由java虚拟机自动垃圾回收器来管理。JVM只有一个堆区(heap)被所有线程共享, 堆中不存放基本类型和对象引用,只存放对象本身。

  • 方法区(method):又叫静态区,跟堆一样,被所有的线程共享。方法区包含所有的static变量

PS:static 也是被线程所共享的,所以如果 Acticity 被一个static变量所引用,很可能会造成内存泄露。

所以关于GC,我们主要关注的是堆内存。

gc_heap.png

  • 堆内存分为两大部分:新生代和老年代,比例为1:2

  • 老年代主要存放应用程序中生命周期长的存活对象

  • 新生代又分为三个部分:一个Eden区和两个Survivor区,比例为8:1:1

  • Eden区存放新生的对象,Survivor存放每次垃圾回收后存活的对象

可回收对象的判定

目前有两种算法用来判定一个对象是否为垃圾。

1. 引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

gc_recycled_object.png

优点是简单,高效。缺点是很难处理循环引用,比如图中相互引用的两个对象则无法释放。

2. 可达性分析算法(根搜索算法)

为了解决上面的循环引用问题,Java采用了一种新的算法:可达性分析算法。从GC Roots(每种具体实现对GC Roots有不同的定义)作为起点,向下搜索它们引用的对象,可以生成一棵引用树, 树的节点视为可达对象,反之视为不可达。

gc_root_object.png

OK,即使循环引用了,只要没有被GC Roots引用就会被回收,完美!

Stop The World

在垃圾回收的时候,需要整个的引用状态保持不变,否则现在判定是垃圾,等我稍后回收的时候它又被引用了,这就全乱套了。在GC的时候,其他所有的程序执行处于暂停状态,卡住了。 幸运的是,这个卡顿是非常短(尤其是新生代)。

PS:频繁的垃圾回收可能也会造成用户界面的不流畅。

几种垃圾回收算法

复制算法 (Copying)

复制算法将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用的内存空间一次清理掉,这样一来就不容易出现内存碎片的问题。 优缺点是,实现简单,运行高效且不容易产生内存碎片,但是却对内存空间的使用做出了高昂的代价,因为能够使用的内存缩减到原来的一半。从算法原理我们可以看出,Copying算法的效率跟存活对象的数目多少有很大的关系, 如果存活对象很多,那么 Copying 算法的效率将会大大降低。

gc_copying.png

标记整理算法 (Mark-Compact)

标记-清除算法分为两个阶段:标记阶段和整理阶段。标记阶段的任务是标记出所有需要被回收的对象,整理阶段就是将存活对象都向一端移动,然后清理掉端边界以外的内存。

所以,特别适用于存活对象多,回收对象少的情况下。

gc_mark_compact.png

分代回收算法

分代回收算法其实不算一种新的算法,而是根据复制算法和标记整理算法的的特点综合而成。这种综合是考虑到java的语言特性的。这里重复一下两种老算法的适用场景:

  • 复制算法:适用于存活对象很少,回收对象多

  • 标记整理算法: 适用用于存活对象多,回收对象少

刚好互补!不同类型的对象生命周期决定了更适合采用哪种算法。于是,我们根据对象存活的生命周期将内存划分为若干个不同的区域。一般情况下将堆区划分为老年代(Old Generation)和 新生代(Young Generation),老年代的特点是每次垃圾收集时只有少量对象需要被回收,而新生代的特点是每次垃圾回收时都有大量的对象需要被回收,那么就可以根据不同代的特点采取最适合的 收集算法,这就是分代回收算法。

  • 对于新生代采取Copying算法,因为新生代中每次垃圾回收都要回收大部分对象,也就是说需要复制的操作次数较少,采用Copying算法效率最高。实际中并不是按照上面算法中说的1:1的比例来 划分新生代的空间的,而是将新生代划分为一块较大的Eden空间和两块较小的Survivor空间,比例为8:1:1。

  • 由于老年代的特点是每次回收都只回收少量对象,一般使用的是Mark-Compact算法。

深入理解分代回收算法

为什么不是一块Survivor空间而是两块?

这里涉及到一个新生代和老年代的存活周期的问题,比如一个对象在新生代经历15次GC,就可以移到老年代了。问题来了,当我们第一次GC的时候,我们可以把Eden区的存活对象放到 Survivor A空间,但是第二次GC的时候, Survivor A 空间的存活对象也需要再次用Copying算法,放到Survivor B空间上,而把刚刚的Survivor A空间和Eden空间清除。第三次GC时,又把Survivor B空间的存活对象复制到Survivor A空间,如此反复。 所以,这里就需要两块Survivor空间来回倒腾。

为什么Eden空间这么大而 Survivor 空间要分的少一点?

新创建的对象都是放在Eden空间,这是很频繁的,尤其是大量的局部变量产生的临时对象,这些对象绝大部分都应该马上被回收,能存活下来被转移到survivor空间的往往不多。所以,设置较大的Eden空间和较小的Survivor空 间是合理的,大大提高了内存的使用率,缓解了Copying算法的缺点。

新的问题又来了,从Eden空间往Survivor空间转移的时候Survivor空间不够了怎么办?直接放到老年代去。

Eden空间和两块Survivor空间的工作流程

现在假定有新生代Eden,Survivor A, Survivor B三块空间和老生代Old一块空间。

// 分配了一个又一个对象
放到Eden区
// 不好,Eden区满了,只能GC(新生代GC:Minor GC)了
把Eden区的存活对象copy到Survivor A区,然后清空Eden区(本来Survivor B区也需要清空的,不过本来就是空的)
// 又分配了一个又一个对象
放到Eden区
// 不好,Eden区又满了,只能GC(新生代GC:Minor GC)了
把Eden区和Survivor A区的存活对象copy到Survivor B区,然后清空Eden区和Survivor A区
// 又分配了一个又一个对象
放到Eden区
// 不好,Eden区又满了,只能GC(新生代GC:Minor GC)了
把Eden区和Survivor B区的存活对象copy到Survivor A区,然后清空Eden区和Survivor B区
// ...
// 有的对象来回在Survivor A区或者B区呆了比如15次,就被分配到老年代Old区
// 有的对象太大,超过了Eden区,直接被分配在Old区
// 有的存活对象,放不下Survivor区,也被分配到Old区
// ...
// 在某次Minor GC的过程中突然发现:
// 不好,老年代Old区也满了,这是一次大GC(老年代GC:Major GC)
Old区慢慢的整理一番,空间又够了
// 继续Minor GC
// ...
// ...

触发GC的类型

了解这些是为了解决实际问题,Java虚拟机会把每次触发GC的信息打印出来来帮助我们分析问题,所以掌握触发GC的类型是分析日志的基础。

  • GC_FOR_MALLOC: 表示是在堆上分配对象时内存不足触发的GC

  • GC_CONCURRENT: 当我们应用程序的堆内存达到一定量,或者可以理解为快要满的时候,系统会自动触发GC操作来释放内存

  • GC_EXPLICIT: 表示是应用程序调用System.gc、VMRuntime.gc接口或者收到SIGUSR1信号时触发的GC

  • GC_BEFORE_OOM: 表示是在准备抛OOM异常之前进行的最后努力而触发的GC。