垃圾收集器与内存分配策略
垃圾回收主要完成了三件事情:确定哪些垃圾需要回收,什么时候进行回收以及如何把垃圾回收。此篇仅针对JAVA堆的垃圾回收。
一.哪些垃圾需要回收(判断对象已死)
1.是否存活算法:
1️⃣引用计数算法:如果引用则加1,任何时候都没有引用则为0,这时候虚拟机就可以将它回收。致命缺点:容易存在循环引用,引起内存泄漏。
2️⃣可达性分析算法:以GC ROOTS为起点进行遍历,如果对象可达则表明不可以回收,否则可以被垃圾回收器回收。
2.引用的种类:
可达性分析的基础是通过对象之间的引用关系进行遍历,但是这种引用显得太刻板,因此有了以下几种引用:
1️⃣强引用:无论任何情况下,垃圾收集器都不会回收掉这类引用。
2️⃣软引用:当内存快溢出时,收集器会将其回收。
3️⃣弱引用:引用对象只能存活到下次垃圾回收。
4️⃣虚引用:唯一目的是为了垃圾回收时收到一个系统通知。
3.死亡的过程
若被收集器判断为不可达,那么这个对象并不是非死不可的。若对象重写了finalize()方法,并且方法中又重新建立连接,那么这个对象会获得自救。然而这种自救方式只有一次机会,第二次收集器将会直接将其判断为死亡。
4.方法区的回收
方法区的回收内容:废弃的常量以及不再使用的类型。回收条件十分苛刻,因此回收性价比很低。
二.垃圾收集算法
以下均为追踪式垃圾收集的范畴。
1.分代收集理论
理论基础:1️⃣弱分代假说,意思是绝大多数的对象都是朝生夕死的。2️⃣强分代假说,意思是熬过越多次的垃圾收集那么这个对象是越难消亡的。3️⃣跨代引用相对于同代引用是数量少很多的。一般通过在新生代的基础上增加一个全局数据结构(记忆集)来实现跨代的引用。
2.三种基本的算法
1️⃣标记-清除算法。将需要回收的对象标记后回收。缺点:1.执行效率不稳定,如果需要回收的对象多,需要的动作很多。2.内存的碎片很多,大对象不容易找到连续的内存来存储。
2️⃣标记-复制算法。将对象后标记后复制到新的半区。特点:1.解决了第一种方法的效率问题,因为第一种是标记需要回收的,而这种方法是标记不需要回收的,在新生代需要回收的对象大大多于不需要回收的。2.解决了碎片问题。3.缺点是需要留出额外的内存空间来进行复制操作。
*当存活区内存不够的时候,可以通过分配担保机制将对象直接进入老年代。
3️⃣标记-整理算法。
对于老年代经常采用的一种移动式算法。特点:1.移动对象的时候需要更新引用,会引起stw。2.不会产生内存碎片。
三.Hotspot算法实现细节
1.根节点的枚举
任何垃圾回收器在GC Roots枚举的时候都需要STW,但可以缩短这个时间,其中一种方法是使用一组叫做OopMap的数据结构来达到这个目的,会在特定的位置记录下栈里和寄存器中哪些位置是引用,缩短扫描的时间。
2.安全点
每个点都创建oopmap性价比很低,因此只会在特点位置生成oopmap,这些位置就被称为安全点。选取的标准是“是否具有让程序长时间执行的特征”。另外一个问题是要保证所有线程都跑到最近的安全点,主要选择有:抢先式中断和主动式中断。抢先式指的是当垃圾收集发生时,如果线程没有跑到安全点则让它恢复跑到安全点后再暂停,而主动式指的是线程在运行的过程中不断询问当前位置是否是安全点,若是则停下来。
3.安全区域
由于线程可能sleep或者blocked,这时候线程便无法响应虚拟机中的请求,不能走到安全的位置挂起自己。因此需要安全区域,可以看做是一段延长的安全点,在这个区域内任意位置开始垃圾收集都是安全的。
4.记忆集与卡表
记忆集主要解决了对象跨代引用带来的问题,主要选择的精度有:字长精度、对象精度和卡精度。而卡精度常常被称作卡表,每条记录都精确到一块内存区域,每个内存块被称作“卡页”,只要卡页中其中一个对象具有跨代引用,那么它就是dirty的,就需要加入GC roots一起扫描。
5.写屏障
hotspot虚拟机可以通过写屏障来维护卡表,相当于aop,在每次赋值操作的时候更新卡表,为了防止伪贡献问题的产生,可以每次都判断卡表需不需要更新。
6.并发的可达性分析
一般用三种颜色来表示对象的可达状态:1️⃣白色:说明没有被访问过,若分析结束还是白色就会被回收。2️⃣灰色:对象已经被访问过,但至少还存在一个引用没有被访问。3️⃣黑色:被访问过,且所有引用已经被引用。
如果在可达性分析的同时并发会发生两个问题:
1️⃣原来消亡的对象被标记为存活,关系不大,只是产生浮动垃圾,下次再收集即可。
2️⃣对象消失,会导致程序崩溃。只有同时满足两个条件才会发生:1.插入了一条黑色到白色的引用(增量更新解决) 2.删除了全部从灰色对象到白色对象的引用(原始快照SATB解决)。
四.经典的垃圾收集器
1.新生代
①Serial垃圾收集器
线程数:单线程。强调的是当垃圾回收时其他的线程必须停止。
算法:标记-复制算法。
-XX:+UseSerialGC 是指新生代用Serial 老年代用SerialOld
②ParNew收集器
线程数:多线程。强调可以采用多线程进行并行,但是不能并发。
算法:标记-复制算法。
** 与CMS配合一度非常流行,但CMS被淘汰后,ParNew也被淘汰。
—XX:+UseParNewGC 是指新生代用ParNew,老年代用CMS。
③Parallel Scavenge收集器
线程数:多线程。强调的是吞吐量,也是不能并发。
算法:标记-复制算法。
可以通过-XX:MaxGCPauseMillis以及-XX:GCTimeRatio来设置,也可以通过-XX:+UseAdaptiveSizePolicy来自动设置。
2.老年代
①Serial Old收集器
线程:单线程
算法:标记-整理算法。
用途:主要和Parallel Scavenge搭配使用,作为CMS的预备方案。
②Parallel Old收集器
线程:多线程
算法:标记-整理算法。
用途:与Parallel Scavenge配合构成一个吞吐量优先的组合。
③CMS收集器
目标:获得最短回收停顿。
线程:多线程并发收集。
算法:标记清除算法。
流程:
1️⃣初始标记。根据GC Roots标记直接关联到的对象,此过程需要STW。
2️⃣并发标记。遍历所有对象,这过程可以与用户线程并发。
3️⃣重新标记。根据“增量更新”对于那些白色对象进行重新标记。此过程也需要STW。
4️⃣并发清理。对于那些需要回收的对象进行清理。
缺点:
1️⃣并发时需要默认启动(处理器核心数目+3)/4的线程数专门来进行垃圾回收,影响服务器效率。
2️⃣并发过程中产生的浮动垃圾需要下次垃圾回收时再处理,因此需要留出额外的空间给浮动垃圾,而如果超过阈值,会触发SerialOld收集器进行回收。
3️⃣空间碎片多。
3.不分代
①G1收集器
JDK9之后G1收集器正式成为了默认垃圾收集器。整体采用标记-整理算法。
同时注重了吞吐量和低延迟。采用了Mixed GC没有了代之间的概念。
将JAVA堆分成多个大小相同的Region,每个region可以扮演Eden区、Survivor空间或者老年代空间。
若大对象的大小大于一个Region容量一半时,会被存放在N个连续的Humongous Region中,G1将其当做老年代看待。
Ⅰ预测停顿时间
G1将每个Region作为最小的回收单位,让G1去跟踪每个Region回收的价值,根据设定的收集停顿时间优先回收那些回收价值大的。
Ⅱ已经解决的那些关键问题
1️⃣Region之间互相引用问题。每个Region各自维护指向自己的指针,需要JAVA堆中的10%-20%来维持这种操作。
2️⃣并发时如何保证收集线程和用户线程互不干扰。采用原始快照SATB。
3️⃣如何建立可靠的停顿模型。以衰减均值为理论基础来实现,根据脏卡数量等各种统计数据来估计平均时间,但新数据所占的权重大。
Ⅲ 过程
1️⃣初始标记。标记可以被GC Roots可以直接访问到的对象,并且修改TAMS指针的值。
2️⃣并发标记。进行可达性分析,并且重新标记SATB记录下的对象。
3️⃣最终标记。STW一小段时间,处理结束时的那一小段的SATB记录。
4️⃣筛选回收。根据回收成本进行筛选回收。需要STW。
Ⅳ 特点
1️⃣内存连续,便于存放大对象。
2️⃣卡表复杂,所占用内存大。
3️⃣不但需要写前屏障也需要写后屏障,消耗更多的运算资源。
4️⃣小内存使用CMS更好,大内存使用G1更好。平衡点在6GB到8GB之间。
②低延迟收集器
目前仍然处在实验阶段。
1.Shenandoah收集器
回收阶段也支持多线程并发。摒弃了记忆集,使用连接矩阵来解决跨代引用问题。停顿时间减少很多,但是吞吐量也减少很多。
过程:
①初始标记。
②并发标记。
③最终标记。完成剩下的SATB,统计出回收价值最大的Region,将这些Region放入回收集,会有一小段STW。
④并发清理。清理那些没有一个存活对象的区域。
⑤并发回收。将回收集中的存活对象复制到未被使用的Region中去,由于是并发的,引用都需要改变,这里使用了读屏障“Brooks Pointers”转发指针。
⑥初始引用更新。复制完后需要将旧引用改成新的引用,但这个阶段只是个开始信号,没有什么实际作用,有个很小的STW。
⑦并发引用更新。正式开始引用更新操作。
⑧最终引用更新。改正GC Roots的引用,有个小的STW。
⑨并发清理。将回收集清空。
Brooks Pointers:在原来的对象布局结构的最前面统一加上新的引用,没移动前是指向自己,移动后指向新的引用。由于与用户线程并发,使用的时候必须用户线程和收集器线程对于复制对象的操作需要CAS。
BrooksPointers的使用使得在访问对象的同时需要有读屏障,这开销代价很大。
2.ZGC收集器
一款采用Region内存分布,不设分代,使用了读屏障,染色指针和内存多重映射技术来实现的标记整理算法。
特点:
1️⃣动态内存,有小Region,中Region以及大Region。
2️⃣染色指针技术。解决了并发整理问题,将内存中的高四位作为引用标记。优点:
①若Region存活的对象被移走,那么Region可以直接被释放。也被称为自愈。
②减少了内存屏障的使用,可以直接将这些内容维护在指针内。
③未来可以扩展更多的位数来储存更多的标志。
过程:所有过程都可以并发操作,仅仅存在两个小停顿。
1️⃣并发标记。可达性分析,是在指针上进行的。
2️⃣并发预备重分配。统计出需要清理哪些Region,但是是会扫描所有的Region,生成重分配集。
3️⃣并发重分配。把重分配集中的对象复制到新的Region上,为每个重分配集中的Region维护一个转发表,由于有染色指针的支持,可以很快知道一个对象是否处在重分配集当中,如果是,那么在第一次访问的时候会根据转发表访问新对象,并将引用指向新对象,这样就只在第一次访问的时候降低了访问的速度。
4️⃣并发重分配。修正整个堆中指向重分配集中旧对象的所有引用。这个过程不是必须的,因此经常跟下次的并发标记一起进行。