1 垃圾标记阶段

在堆里存放几乎所有的java对象实例,在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。

判断对象存活一般有两种方式:引用计数算法和可达性分析算法。

2 引用计数算法

对于一个对象A,只要有任何一个对象引用了A,A的引用计数器就加1,当引用失效就减一。

优点:实现简单,垃圾对象便于辨识;判断效率高,回收没有延迟。

缺点:需要单独的字段存储计数器,这样的做法增加了存储空间的开销。每次赋值都需要更新,伴随着加法和减法操作,这就增加了时间开销。而且无法处理循环引用的情况,可能会造成内存泄漏,所有java中没有采用这类算法。

python中采用了计数机制,采用手动解除(在合适的时机解除引用关系)和使用弱引用weakref(是python提供的标准库)解决循环引用。

3 可达性分析算法

基本思路:

1)可达性分析算法是以根对象集合为起始点,按照从上至下的方式搜索被根对象所连接的目标对象是否可达。

2)使用可达性分析算法后,内存中存活的对象都会被根对象集直接或间接连接着,搜索所走过的路径称为引用链。

3)如果目标对象没有任何引用链相连,则是不可达的,就意味着对象已经死亡。

GC roots:

1)虚拟机栈中引用的对象

2)本地方法栈内本地方法引用的对象

3)方法区中类静态属性引用的对象

4)方法区中常量引用的对象

5)所有被同步锁持有的对象

6)java虚拟机内部的引用

6)反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存。

7)除了这些固定的GC Roots意外,根据用户所选用的垃圾收集器以及当前回收的区域不同,还可以有其他对象“临时性:地加入,共同构成完整GC Roots集合。比如:分代收集和局部回收。

小技巧:由于Root采用栈方式存放变量和指针,它保存了堆内存里面的对象,但是自己又不存放在堆里,那它就是一个root。

4 Finalization机制

java语言提供了对象终止机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。

永远不要主动调用某个对象的finalize()方法,应该交给垃圾回收机制调用:

1)在finalize()时可能复活

2)finalize方法的执行时间是灭有保障的,完全由gc线程决定,极端情况下,若不发生gc,则finalize将没有执行机会。

3)一个糟糕的finalize会严重影响GC性能。

由于finalize方法的存在,虚拟机的对象一般处于三种可能的状态:

1)可触及的:从根节点开始,可以到达这个对象。

2)可复活的:对象的所有引用都被释放,但是对象有可能在finalize中复活

3)不可触及的:对象的finalize方法被调用,并且没有复活,不可触及的对象不可能被复活,因为finalize只会被调用一次。

 

具体过程:

判断一个对象objA是否可以回收,至少要经历两次标记过程。

1)如果对象objA到GC Roots没有引用链,则进行第一次标记。

2)进行筛选,判断对象是否有必要执行finalize方法。

1.如果objA没有重写finalize方法,或者finalize方法已经被虚拟机调用过,则虚拟机视为”没有必要“执行,objA被判定为不可触及的。

2.如果对象重写了,并且还没有执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、优先级的Finalizer线程触发其finalize方法执行。

3.finalize()是对象逃脱死亡的最后机会,稍后GC会对F-Queued队列中的对象进行第二次标记。如果在finalize中于任何一个对象建立了联系,那么第二次标记的时候就会被移出”即将回收“集合。之后对象再次出现没有引用存在的情况下,会被直接变为不可触及的。

5 垃圾清除阶段

当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占的内存空间,以便有足够的内存空间为新对象分配内存。

6 标记-清除算法

执行过程:

当堆空间的内存空间被耗尽的时候,就会停止整个程序,然后进行两项工作,一项是标记:从根节点开始 遍历,标记所有被引用的对象,一般是在对象的Header记录为可达对象。二是清除:对堆内存从头到尾进行线性的遍历,如果发现某个对象在header中没有标记为可达对象,则将其回收。

缺点:

1)效率不算高

2)在进行GC的时候,需要停止整个应用程序,导致用户体验很差

3)这种方式清理出来的空闲内存是不连续的,产生内存碎片。需要维护一个空闲列表

注意:何为清除?

这里所谓的清除并不是真的置空,而是把需要清除的对象地址保存在空间的地址列表里面。下次有新对象需要加载的时候,判断垃圾的位置空间是否足够,如果够,就存放。

7 复制算法

核心思想:

将活着的内存空间分为两块,每次只使用其中的一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存中的所有对象,交换两个内存的角色,最后完成来及回收。

优点:

没有标记和清除的过程,实现简单,运行高效。

复制过去以后保证空间的连续性,不会出现碎片问题。

缺点:

需要两倍的空间。

对于G1这种分拆成大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者是时间开销也不小。

应用场景:

在新生代,对常规应用的垃圾回收,性价比很高。现在的商用虚拟机都是用这种收集算法回收新生代。s1和s2区也符合其回收的特点。

8 标记-压缩算法

基于老年代的回收特性,执行过程中第一阶段和标记清除算法一样,从根节点开始标记所有被引用对象,第二阶段将所有存活对象压缩到内存的一段,按顺序存放。

优点:

可以使用指针碰撞的方式给新对象分配内存,简便,消除了复制算法中,内存减半的高额代价。

缺点:

效率低于复制算法,移动对象的同时,如果对象被其他对象引用,还需要调整引用的地址,移动过程中,需要全程暂停用户应用程序。

9 分代收集算法

不同对象的生命周期是不一样的。因此,不同生命周期的对象可以采取不同的收集方式,以便提高回收效率。

年轻代:

特点是区域相对于老年代较小,对象生命周期短、存活率低,回收频繁。很使用复制算法。

老年代:

区域大,对象生命周期长,存活率高,回收不及年轻代,一般由标记-清除或者标记-清除与标记-整理混合使用。

10 增量收集算法、分区算法(降低延迟)

增量收集算法

基本思想:让垃圾收集算法收集线程和应用程序线程交替执行。每次,垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。依次反复,直到垃圾收集完成。

缺点:使用这种方式,由于在垃圾回收过程种,间断性地还执行了应用程序代码,所以能减少系统的停顿时间。但是,因为线程的切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

分区算法

将对象按照生命周期长短划分为两个部分,分区算法将整个堆空间划分成连续不同的小空间,每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间