1、垃圾收集算法
分代垃圾收集理论、标记清除算法、标记整理算法、标记复制算法
分代垃圾收集理论:根据对象的存活时间将内存划分为不同的块,一般将java堆分成新生代和老年代,不同的分代使用不同的垃圾收集算法。年轻代:每次收集都会有大量的对象死去,对象朝生夕死,所以可以选择标记复制算法,每次付出少量的复制成本就可以完成年轻代的垃圾收集。老年代的对象存活几率比较高,而且老年代没有额外的空间进行分配担保。所以老年代一般使用标记整理或者标记清除算法进行回收。
标记复制算法:将内存分成大小相等的两块空间,每次分配对象的时候只是使用其中一块,当一块使用完毕之后,就将存活的对象复制到另外一块内存空间去,然后将之前使用的内存全部清理掉。这样使得每次回收都是对内存中一半的区域进行回收。

优点:不会产生内存碎片
缺点:每次只是使用一半的内存空间进行对象分配,浪费内存空间
标记清除算法:算法分为标记跟清除两个阶段,标记:标记处存活的对象,清除:清除掉所有未标记的对象。标记清除算法会产生大量的内存碎片,但是效率相比标记整理跟标记复制两种算法要高。

标记整理算法:首先标记处存活的对象,将存活的对象向一端移动(修改对象的内存地址),清理掉存活对象边界以外的对象。标记整理算法不会产生大量的内存碎片内存空间比较规整。

2、垃圾收集器

Serial垃圾收集器(-XX:+UseSerialGC -XX:+UserSerialOldGC):
Serial是一款单线程垃圾收集器,只会启动一个线程进行垃圾收集并且在垃圾收集的时候需要暂停其他所有的线程(STW),直到垃圾收集结束。Serial垃圾收集器年轻代使用标记复制的算法,老年代使用的是标记整理的算法。

Serial收集的优点:简单高效,由于Serial收集器是单线程的垃圾收集所有没有多个线程之间的交互开销,所以可以获得很高效的单线程收集效率。
Serial Old收集器是Serial收集的老年代版本,同样是一个单线程的垃圾收集器,主要有两大用途:a、在JDK1.5以及以前版本作为Parallel Scavenge收集器搭配使用 b、作为CMS垃圾收集器的后备方案。
Parallel Scavenge(-XX:+UseParallelGC -XX:+UserParallelOldGC):Parallel垃圾收集器就是Serial垃圾收集器的多线程版本,除了使用多线程进行垃圾收集外,其余行为与Serial收集器类似。默认的垃圾收集线程数与CPU核数相同,可以通过-XX:ParallelGCThreads来指定。Parallel Scavenge收集器的关注点是吞吐量,即用户运行代码的时间与CPU消耗总时间的比值。CMS等垃圾收集器更多的是用户线程的停顿时间,提高用户的体验度。Parallel Scavenge年轻代使用的是标记复制算法,老年代使用的标记整理算法。

Parallel Old 是Parallel Scavenge收集器的老年代版本,使用多线程和标记整理算法。Parallel Scavenge 和 Parallel Old 是JDK1.8 默认的年轻代和老年代的垃圾收集器。
ParNew收集器(-XX:UseParNewGC):ParNew 与 Parallel Scavenge很类似,区别在与ParNew可以与CMS收集器配合使用,新生代采用的是标记复制算法,老年代采用的是标记整理算法。

它是许多运行在Server模式下的虚拟机的首选,除了Serial收集器外,只有它能与CMS收集器配合工作。
CMS收集器(-XX:UseConcMarkSweepGC):CMS垃圾收集器是一种以获取最短相应时间为目的的收集器。它是hotspot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户收集线程同时工作。
CMS垃圾收集器主要使用标记清除算法,可以分为以下几个步骤:

1、初始标记:暂停所有的其它线程(STW),找到GC ROOTS直接引用对象,速度很快。
2、并发标记:并发标记阶段就是从GC ROOTS直接关联的对象开始遍历整个对象的过程,过程时间较长,但是不需要STW,标记线程与应用线程同时进行处理。因为用户线程继续进行,有可能导致已经标记过得对象状态发生改变。
3、重新标记:重新标记是为了修正并发标记阶段因为用户线程继续而导致标记产生变动的那一部分对象的标记记录,这个阶段要比初始标记的时间要长,要比并发标记的时间要短,也会发生STW,主要是用三色标记中的增量更新来进行标记。
4、并发清理:开启用户线程,同时GC线程开始对没有标记的区域进行清理。这个阶段如果有新增对象会被标记为黑色不做任何处理。
5、并发重置:重置本次GC过程中的标记对象。
CMS垃圾收集器的优点:并发收集、低停顿。缺点:对CPU资源敏感(与用户线程同时进行),由于CMS使用标记清除算法因此会产生大量的内存碎片(可以通过参数-XX:+UseCMSCompactAtFullCollection在经过Full GC之后对内存进行压缩),无法处理浮动垃圾,在并发标记、并发清理这两个阶段产生的垃圾只能等到下一次垃圾回收进行处理,concurrent model failure(并发失败),在并发标记与并发清理的过程中,由于程序还在运行因此也会对对象进行内存空间的分配,那么由于老年代没有足够的内存空间对对象进行分配,那么CMS垃圾回收器将自动转换成serial垃圾收集器进行回收,此时会触发STW。
CMS相关参数:
1、-XX:+UseConcMarkSweepGC:启用CMS
2、-XX:+ConcGCThreads:GC并发线程数
3、-XX:+UseCMSCompactAtFullCollection:FullGC之后做整理(对碎片进行压缩)
4、-XX:CMSFullGCsBeforeCompaction:多少次full GC之后进行压缩默认是 0,代表每次full GC都会进行压缩。
5、-XX:CMSInitiatingOccupancyFraction:当老年代使用到该比例的时候进行full GC(默认92%)
6、-XX:+UseCMSInitiatingOccupancyOnly:只是用设定的固定回收阀值,如果不指定,JVM在第一次垃圾回收会使用阀值,后续则会自动调整。
7、-XX:+CMSScavengeBeforeRemark:在CMS进行full GC之前进行一次minor GC主要目的是如果有跨代引用可以减少,并发标记的时间。
8、-XX:+CMSParallelInitialMarkEnabled:表示在初始标记的时候并发执行,减少STW时间。
9、-XX:+CMSParallelRemarkEnabled:表示在重新标记的时候进行并发执行,减少STW时间。
3、三色标记算法
在并发标记过程中由于用户线程还在继续运行,对象间的引用可能发生变化,所以就有可能出现漏标或者多标的情况,通过三色标记以及写屏障解决这个问题。可达性分析中使用三色标记以及写屏障解决漏标的问题。
黑色对象:表示对象已经被垃圾收集器访问过并且这个对象所有的引用对象都已经被扫描过,黑色对象代表已经扫描过,它是安全存活的,如果有其他对象引用指向了黑色对象,无需重新扫描一次。黑色对象不可能直接指向一个白色对象。
灰色对象:表示对象已经被垃圾收集器访问过,但是这个对象至少存在一个引用还没有被扫描过。
白色对象:表示这个对象还没有被垃圾收集器访问过,在可达性分析刚刚开始的阶段,所有的对象都是白色的,若在分析结束阶段对象还是白色的,则表示这个对象不可达。

public class ThreeColorRemark {
public static void main(String[] args) {
A a = new A();
//开始做并发标记
D d = a.b.d; // 1.读
a.b.d = null; // 2.写
a.d = d; // 3.写
}
}
class A {
B b = new B();
D d = null;
}
class B {
C c = new C();
D d = new D();
}
class C {
}
class D {
}对象D被对象B引用,但是在并发标记的时候由于代码不断的运行,B对D的引用置为null,此时D还是为白色,但是在并发标记结束后A对象对D对象进行了引用,但是此时D对象还是白色,所以产生了漏标。
多标-浮动垃圾:在并发标记过程中程序不断的运行,如果方法运行结束导致局部变量被销毁,GCRoot被销毁,这个GCRoot引用的对象又被非垃圾对象,那么本轮GC不会回收这些对象。这部分本应该被回收的内存没有被回收,被称为浮动垃圾。浮动垃圾不影响垃圾回收的正确性,这部分垃圾会在下一次GC进行回收。在并发标记过程中新创建的对象都会被标记为黑色对象,本轮不会进行清除,这部分对象也会被标记为黑色对象,也算浮动垃圾的一部分。
漏标-读写屏障:漏标会导致正在被引用的对象被标记为垃圾对象,通过增量更新跟原始快照解决。增量更新:当黑色对象插入新的指向白色对象的引用关系时,将引用记录下来,等待并发标记结束之后再将这些记录过的引用关系中的黑色对象为根重新扫描一次,这样就会扫描到白色对象。原始快照:当灰色对象要删除指向白色对象的引用的时候,就将这个引用关系记录下来,在并发标记结束之后,再将记录下来的引用对应的白色对象标记为黑色对象,等待下次GC的时候进行回收。目的就是让这种对象在本轮GC存活下来,等待下一轮GC重新扫描,这个对象也有可能是浮动垃圾。增量跟新与原始快照都是通过写屏障来实现的。
写屏障:在给某个成员变量赋值的时候,会在成员变量赋值前后加入相应的代码。
写屏障代码:
为对象赋值:
/** * @param field 某对象的成
* @param field 某对象的成员变量,如 a.b.d
* @param new_value 新值,如 null
*/
void oop_field_store(oop*field,oop new_value){
*field=new_value; // 赋值操作
} 员变量,如 a.b.d * @param new_value 新值,如 null */void oop_field_store(oop*field,oop new_value){ *field=new_value; // 赋值操作 } 写屏障代码:
void oop_field_stovoid oop_field_store(oop*field, oop new_value) {
pre_write_barrier(field); // 写屏障-写前操作
*field = new_value;
post_write_barrier(field, value); // 写屏障-写后操作
}re(oop*field, oop new_value) { pre_write_barrier(field); // 写屏障-写前操作*field = new_value; post_write_barrier(field, value); // 写屏障-写后操作}写屏障实现SATB:在写操作之前,对之前的引用关系进行记录。
void pre_write_barrier(oop* field) {
oop old_value = *field; // 获取旧值
remark_set.add(old_value); // 记录原来的引用对象
}写屏障实现增量更新:在写操作之后,记录新增对象与原对象的关系引用。
void post_write_barriervoid (oop* field, oop new_value) {
remark_set.add(new_value); // 记录新引用的对象
}读屏障:在读取对象之前进行相应的操作,将对象地址记录到集合中去
oop oop_field_load(oop* field) {
pre_load_barrier(field); // 读屏障-读取前操作
return *field;
} void pre_loadvoid pre_load_barrier(oop* field) {
oop old_value = *field;
remark_set.add(old_value); // 记录读取到的对象
}CMS通过写屏障(增量更新) + 三色标记解决漏标
G1通过写屏障(原始快照)+ 三色标记解决漏标
ZGC通过读屏障 + 染色指针解决漏标问题
CMS使用增量更新,G1使用原始快照的原因:
SATB相对增量更新效率会高(SATB会产生浮动垃圾),因为不需要在重新标记阶段再次深度被删除的引用对象而CMS会对增量引用的对象做深度扫描,G1很多对象都位于不同的region中,CMS就一块老年代区域,重新深度扫描对象的话G1的成本比较高,所以G1没有选择深度扫描对象,只是对对象进行简单标记,等到下一轮GC进行深度扫描。
4、记忆集与卡表
在新生代进行GC Root可达性分析过程中会碰到跨代引用的情况(老年代对象引用新生代),如果此时再对老年代进行可达性分析效率过低,为此在新生代引入记忆集的数据结构,用来记录非收集区到收集区的指针集合,在垃圾收集的场景中,收集器只需要记忆集判断出某一块非收集区域是否存在指向收集区域的指针即可,无需了解跨代收集的全部细节。
hotspot中通过卡表来实现记忆集,卡表使用一个字节数组来实现,每个元素对应着其标识内存区域一块特定大小的区域,这块区域被称为卡页(卡页大小为512字节),一个卡页中包含多个对象,只要有一个对象的字段存在跨代指针,其对应的卡表的元素将会被标记为1(dirty),标识该元素为脏,否则标记为0。GC时只需要筛选本收集区域卡表中变脏的元素加入GC Roots中。卡表的状态主要是通过写屏障来实现的。