策略算法

1. 标记-清除算法:

第一种策略,应该也是最基础的策略,具体做法就是把不再使用的内存通过标记,然后在同一时间进行清除的算法。这种算法的问题在于多次垃圾回收后会产生大量的内存碎片,无法对大对象进行有效的内存分配。

2. 复制算法:

这是一种牺牲空间提升效率的算法。具体做法是将还要活着的内存进行复制,然后将整片内存清除,然后再把复制片区的内存复制回工作区间。这样一来就不会产生内存碎片,也不用一个个清除,代价就是牺牲了大量的空间。而新生代的做法就是使用了默认的10%作为复制区间,牺牲了10%的空间换取高效的内存管理。

3. 标记-整理算法

对标记清除算法进行优化之后提出的另一种算法。具体做法是将不再使用的内存进行标记,然后将使用的内存一个个挪过来,最后将末端的内存清理,这样也可以解决内存碎片的问题,而且不需要使用更多内存解决效率问题。

4. 分代收集算法

这种算法实际上就是通过策略筛选不同的内存进行不同的垃圾回收策略。新生代采用了复制算法,而老生代采用了标记整理算法。两个代之间采用存活时间进行划分。

垃圾判断算法

1. 引用计数算法:

给一个对象中添加一个引用计数器,每当有一个地方引用它时候计数器+1,当引用失效时候计数器减一,当计数器为0时候就是不能在使用,也就是可以回收的对象,但如果存在相互引用的话,这种方法就容易失效。

2. 可达性算法:

主流的虚拟机都是通过可达性算法判断对象是否存活的,通过一些列GC ROOTS的对象为起点,从这些起点向下搜索,搜索过的路为引用连,当一个对象到GC ROOTS没有任何引用链时候,则表示这个对象是不可用的
在JAVA中作为GCROOTS对象有:
a.虚拟机栈中引用的对象
b.方法区中静态属性引用的对象
c.方法区中常量引用的对象
d.Native方法中JNI引用的对象

算法实现

1. 枚举根节点

由于GC需要从GC Roots节点找出引用链,所以需要在某一个瞬间确保能查到所有已存在的内存分配,所以才需要所有Java线程的暂停。hotspot中使用OopMap获取对象引用信息,该数据结构能够通过计算得知对象内的某个偏移量是什么类型的数据。
例子中使用了String.hashCode()方法去解读OopMap的使用方式:

  1. OopMap{ebx=Oop [16]= Oop off=142}

在ebx寄存器和栈中偏移量为16的内存区里存着一个普通对象指针(ordinary object pointer),总长度为142。

2. 安全点

OopMap的指令很多,导致如果所有操作都有一个的话,遍历GC roots节点将会导致性能降低。所以JVM只在部分安全点(safepoint)生成OopMap,所以上面说明的Java线程暂停就需要停在安全点。
(那么问题来了,GC的时候是在安全点执行前还是安全点执行后停止线程的呢?)

GC到来时,有两种方法让线程进入安全点

1) 抢先式中断

先中断所有线程,然后将不在安全点的线程重新启动,直到进入了安全点。

2) 主动式中断

设置一个标志,当线程执行到安全点查询到标志位暂停时中断。

3. 安全区域

如果线程处于sleep或blocked的状态下,就无法进入到安全点(我感觉如果是这种情况就算是安全区域也没用)。所以引入了安全点的扩展——安全区域。当线程执行到安全区域时,给自己一个标识,然后系统GC时检查到这个标识就会自动跳过这个线程。但当线程执行到安全区域边界,将会查询系统GC是否已完成,如果未完成将会等待。

回到一开始思考的问题中,我认为如果真的要解决sleep的问题,应该是有sleep的地方就一定有安全区域。

第二种情况,就是如果sleep或blocked了就直接跳过该线程。

垃圾收集器

1. serial收集器

2. ParNew收集器

3. Parallel Scavenge收集器

4. Serial Old收集器

5. CMS收集器

6. G1收集器

特点:

1)并行与并发
2)分代收集
3)空间整合
与我最开始构思的优化方向类似,基于标记的基础上进行整理而非清理。主要作用在于降低GC频率。但可能细节上有所不同。
4)可预测的停顿

G1使用Remembered Set用来代表对象之间的引用,避免全堆扫描。

流程:

1)初始标记
2)并发标记
3)最终标记
4)筛选回收

自我思考

这些算法没问题,触发算法的时间也没问题,如何选择虚拟机,以及虚拟机内部的工作原理与流程,是接下来需要去关注的。淘宝的JVM就是为了自身业务的特殊性而研发出来的虚拟机,除了新生代与老生代以外,还新增了一个代用作更长时间的垃圾回收,我对这个很感兴趣。