1. 概述
GC需要完成的三件事:
哪些内存需要回收?
什么时候回收?
如何回收?
其中 程序计算器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭,不需要过多考虑回收的问题。
而java 堆和方法区则不一样,一个接口中的多个实现类需要的内存可能不一样,一个方法中的多个分支需要的内存也可能不一样,我们只有在运行期间才能知道会创建哪些对象,这部分内存的分配和回收都是动态的。
2. 判断对象死亡or生存?(重要)
heap里面存放着几乎所有的java对象,GC在对堆进行回收前,首先要判断哪些对象是存活着,哪些已经死去。
2.1 引用计数算法(Reference Counting)
描述:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用时效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
COM、Python语言使用这种算法,但是,主流java 虚拟机并没有选用这种算法来管理内存, 最主要的原因是:它很难解决对象之间相互循环引用的问题。
2.2 可达性分析算法(Reachability Analysis)
基本思路:通过一系列的称为“GC Roots“的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots 没有任何引用链相连(用图论,即从GC Roots到这个对象不可达)时,则证明此对象是不可用的。
主流商用语言,如java、C#等都是使用这种算法。
2.3 谈引用
java将引用分为:强引用、软引用、弱引用、虚引用4种,这4种引用强度依次逐渐减弱。
强引用:类似 Object obj = new Object();垃圾收集器永远不会回收引用所指对象;
软引用:描述一些还有用但并非必需的对象;在系统内存吃紧,要发生OOM之前,会把这些对象列入第二次垃圾回收的范围中,如果回收之后内存还是不够,才会发生OOM。
弱引用:也是用来描述有用但并非必需的对象;被弱引用关联的对象只能生存到下一次GG发生之前。当GC工作时,无论当前内存是否足够,都会回收被弱引用关联的对象;
虚引用:无法通过虚引用取得一个对象的实例。设置虚引用的唯一目的是:能在这个对象被收集器回收时收到一个系统通知。
2.4 生存还是死亡
要真正宣告一个对象死亡,至少要经历两次标记过程:如果对象在进行可达性分析时发现没有与GC Roots 相连接的引用链,那它会被第一次标记并且进行一个筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法以及被虚拟机调用过,虚拟机将这两种情况都视为”没有必要执行“。
如果被判定为有必要执行,则这个对象将会放置在F-Queue的队列之中,并在稍后由一个虚拟机自动建立的、低优先级的Finalize线程去执行它。finalize()是对象逃脱死亡命运的最后一次机会,稍后GC将对整个F-Queue中 的对象进行第二次小规模的标记。
2.5 回收方法区(也称永久代)
在方法区中进行GC性价比一般比较低;
永久代的垃圾回收主要包括两部分内容:废弃常量和无用的类;
判断一个类是否是“无用的类”,需同时满足以下三个条件:
a)该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例;
b)加载该类的ClasLoader已经被回收;
c)该类对应的java.lang.Class 对象没有在任何地方被引用,无法在任何地方通过反射访问该类的方法;
3. 垃圾收集算法(重要)
3.1 标记-清除算法(Mark-Sweep)
缺点:a)效率问题:标记和清除两个过程的效率都不高;
b)空间问题:产生大量碎片;
3.2 复制算法(Coping)(适用新生代)
优点:实现简单,运行高效;
缺点:内存浪费了一半;
现在的商业虚拟机都采用这种算法来回收新生代;将新生代内存划分为一块较大的Eden和两块较小的Survivor空间(默认是8:1:1),当回收时将Eden和Survivor中还存活的对象一次性复制到另外的一块Survivor空间上,最后清理掉Eden和刚那块Survivor空间。
HotSpot默认Eden和Survivor比例为8:1,只有10%的内存会被浪费。有时我们没办法保证每次回收都只有不多余10%的对象存活,当survivor不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。
3.3 标记-整理算法(Mark-Compact)(适用老年代)
复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。
标记-整理算法中标记仍然与标记-清除算法一样,但“整理”是让所有存活的对象都向一端移动,然后直接清理掉边界以外的内存。
3.4 总结
当前商业jvm的GC都采用”分代手机(Generational Collection)算法。
新生代中:每次GC都有大批对象死去,只有少量存活,选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
老年代:对象存活率高,没有额外空间对它进行分配担保,采用“标记-整理”算法。