垃圾回收
内存回收很多优化无非就是让短期存活的对象尽量都留在survivor里,不要进入老年代,这样在minor gc的时候这些对象都会被回收,不会进到老年代从而导致full gc。
# 1 JAVA堆的内存分配与回收
堆空间的基本结构:
1.大部分情况下,对象首先在Eden区域分配。
2.在一次新生代垃圾回收后(Eden区和Survivor区回收),如果对象还存活,则进入s0或s1,并且年龄加1。
3.当对象年龄增加到一定程度(默认大于15),就会被晋升到老年代。
晋升到老年代的年龄设置可以通过指定参数配置,但这个值会在虚拟机运行过程中调整。
# 2 空间分配担保
空间分配担保是为了确保在Minor GC之前老年代本身还有容纳新生代所有对象的剩余空间。
空间分配担保的规则为:
只要老年代的连续空间大于新生代对象的总大小或者大于历次晋升的平均大小,就会进行Minor GC,
否则进行Full GC。
# 3 怎么判断对象已经死亡?
# 3.1 引用计数法
给对象中添加一个引用计数器,每当有一个地方引用它,计数器就加1;当引用失效时,计数器就减1;任何时候计数器为0的对象就是不可能再被使用的。
这个方法使用简单,效率高,但是主流的虚拟机都没有用它来管理内存,主要原因是它很难解决对象之间相互循环引用的问题。
# 3.2 可达性分析算法
# 1 介绍
将指定类型的对象作为GC Roots节点,从这些节点开始搜索,将有引用的节点串成一条引用链,当一个对象到GC Roots没有任何引用链相连的话,则证明此对象是不可用的,需要被回收。
下图中Object6 ~ Object10之间虽有引用关系,但他们到GC Roots不可达,因此为需要被回收的对象。
# 2 哪些对象可以作为GC Roots呢?
1.虚拟机栈中引用的对象
2.本地方法栈中引用的对象
3.方法区中类静态属性引用的对象
4.方法区中常量引用的对象
5.所有被同步锁持有的对象
# 4 怎么判断一个类是无用的类?
方法区主要回收的是无用的类,判断一个类无用,需要同时满足以下3个条件:
1.该类的所有实例都已经被回收,也就是JVM堆中不存在该类的任何实例。
2.加载该类的加载器ClassLoader已经被回收。
3.该类对应的java.lang.Class对象没有在任何地方被引用,无法在任何地方通过反射的方式访问该类。
# 5 垃圾收集算法
# 5.1 标记-清除算法
该算法分为“标记”和“清除”阶段:首先标记出所有不需要回收的对象,在标记完成后统一回收掉所有没有被标记的对象。它是最基础的收集算法,后续的算法都是对其不足进行改进得到。这种垃圾收集算法会带来两个明显的问题:
1.效率问题
2.空间问题(标记清除后会产生大量不连续的碎片)
# 5.2 标记-复制算法
将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
# 5.3 标记-整理算法
标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
# 5.4 分代收集算法
当前虚拟机的垃圾收集都采用分代收集算法,这种算法没有什么新的思想,只是根据对象存活周期的不同将内存分为几块。
一般将java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。
比如在新生代中,每次收集都会有大量对象死去,所以可以选择”标记-复制“算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。
而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
# 6 垃圾收集器
# 6.1 Serial收集器
Serial(串行)收集器:单线程收集器。仅用一条线程完成垃圾收集工作,且运行期间必须暂停其他所有工作线程(Stop The World),直到它收集结束。
新生代采用标记-复制算法,老年代采用标记-整理算法。
缺点:暂停所有线程影响用户体验。
优点:简单高效,没有线程交互的开销。
# 6.2 ParNew收集器(-XX:+UseParNewGC)
ParNew收集器其实就是Serial收集器的多线程版本,除了多线程,其他行为一样。
它可以和CMS收集器配合使用(新生代使用ParNew,老年代使用CMS)。
# 6.3 Parallel Scanvenge 收集器
JDK1.8的默认收集器,新生代采用标记-复制算法,老年代采用标记-整理算法。
看上去几乎和ParNew都一样。Parallel Scavenge收集器主要在于高效率的利用CPU。
# 6.4 CMS收集器(4-8G)
-XX:+UseConcMarkSweepGC(old)
# 6.4.1介绍
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。
它非常符合在注重用户体验的应用上使用,它是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程 (基本上)同时工作。
从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种 “标记-清除”算法实现的,它的运作过程相对复杂一些。整个过程分为四个步骤:
1 初始标记
暂停所有的其他线程(STW),并记录下gc roots直接能引用的对象,速度很快。
2 并发标记
并发标记阶段就是从GC Roots的直接关联对象开始遍历整个对象图的过程,这个过程耗时较长但是不需要停顿用户线程,可以与垃圾收集线程一起并发运行。因为用户程序继续运行,可能会有导致已经标记过的对象状态发生改变。
3 重新标记
重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。主要用到三色标记里的增量更新算法做重新标记。
4 并发清理
开启用户线程,同时GC线程开始对未标记的区域做清扫。这个阶段如果有新增对象会被标记为黑色不做任何处理。
5 并发重置
重置本次GC过程中的标记数据。
# 6.4.2 CMS优缺点
优点:
并发收集、低停顿。
缺点:
1)对CPU资源敏感(会和服务抢资源)。
2)无法处理浮动垃圾(在并发标记和并发清理阶段又产生垃圾,这种浮动垃圾要等到下一次gc再清理)。
3)它使用的回收算法是“标记-清除”算法,会导致收集结束时会有大量空间碎片产生。当然通过参数- XX:+UseCMSCompactAtFullCollection可以让jvm在执行完标记清除后再做整理。
4)执行过程中的不确定性,会存在上一次垃圾回收还没执行完,然后垃圾回收又被触发的情况,特别是在并发标记和并发清理阶段会出现,一边回收,系统一边运行,也许没回收完就再次触发full gc,也就是"concurrent mode failure",此时会进入stop the world,用serial old垃圾收集器来回收。
# 6.5 G1收集器
G1是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。具备高吞吐量性能特征。
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region。
G1收集器具备以下特点:
1.并行与并发
2.分代收集
3.空间整合
4.可预测的停顿
# 6.6 如何选择垃圾收集器?
JDK1.8默认使用Parallel。JDK1.9默认使用G1。
ES、KAFKA内存较大的可使用G1。
1.如果内存小于100M,使用串行收集器。
2.如果是单核,并且没有停顿时间要求,串行或者JVM自己选择。
3.如果响应时间最重要,并且不能超过1秒,使用并发收集器。
1.内存4G以下可以用Parallel。
2.内存4-8G可以用ParNew+CMS。
3.内存8G以上可以用G1。
4.内存几百G以上用ZGC。
# 7 三色标记
# 7.1 介绍
JVM中的垃圾回收是基于标记-复制、标记清除和标记-整理三种模式的,其中最重要的是如何标记。
像Serial、ParNew这类回收器,本质是暂停用户线程进行全面标记的算法。缺点是标记时间长导致STW时间也长,影响体验。
像CMS、G1这类回收器,使用的是并发标记,可以在不暂停用户线程的情况下进行标记,从而可以用极少的时间或者没有中断来进行GC。实现并发标记的算法就是三色标记法。
标记过程:
1.在GC标记刚开始的阶段,所有对象均为白色集合。
2.将所有GC Roots直接引用的对象标记成灰色集合。
3.判断若灰色集合中的对象不存在子引用,则将其放入黑色集合,若存在子引用对象,则将所有子引用对象放入灰色集合,当前对象放入黑色集合。
4.按照步骤3,以此类推,直到灰色集合中所有对象变成黑色后,本轮标记完成。当前白色集合内所有对象称为不可达对象,即垃圾对象。
问题:
三色标记过程是跟用户线程并发运行的,对象引用处于随时可变的情况,可能出现多标或漏标问题。
# 7.2 浮动垃圾(多标)
本来应该是标记白色的对象,结果被标记成灰色或黑色,造成该对象不会被回收。
比如E对象被D对象引用着,刚好GC在扫描,将E对象标记成灰色,此时,D对E的引用被置空,这时候E对象以及后续子引用应该被当成垃圾回收,但是因为E已经被标记为灰色,导致没有被及时清理掉,变成浮动垃圾。
还有一种情况,并发标记开始后产生的新对象,通常做法是直接当成黑色,本轮不会进行清除。但是这部分对象也有可能变成垃圾,所以也算是浮动垃圾的一部分。
# 7.3 漏标
灰色对象指向白色对象的引用消失了,然后一个黑色对象重新引用了白色对象。
按照三色标记算法,黑色对象是已完成状态,不会再去找子引用,这样会导致这个白色对象虽然正在被线程使用中,但是无法被标记为灰色或者黑色,造成一个正在使用的对象被错误回收。
总结:漏标只有同时满足以下两个条件才会发生:
1.灰色对象断开了白色对象的引用。
2.黑色对象重新引用了该白色对象。
解决方案:
通过读写屏障解决问题。其中有两种具体实现:增量更新(Incremental Update)
读写快照(Snapshot At The beginning,SATB)
CMS:采用增量更新算法
当一个白色对象被一个黑色对象引用,将黑色对象重新标记为灰色,让垃圾回收器重新扫描。
G1:采用读写快照
原始快照就是当灰色对象要删除指向白色对象的引用关系时,就将这个要删除的引用记录下来,在并发扫描结束之后,再将这些记录过的引用关系中的灰色对象为根,重新扫描一次,这样就能扫描到白色的对象,将白色对象直接标记为黑色。
目的就是让这种对象在本轮gc清理中能存活下来,待下一轮gc的时候重新扫描,这个对象也有可能是浮动垃圾
# 8 GC安全点与安全区域
# 8.1 GC安全点
当垃圾收集需要回收线程时,不直接对线程操作,仅仅简单地设置一个标志位,各个线程执行过程中会去查询这个标志位,一旦发现标志位为true时就自己在最近的安全点上主动中断挂起。查询标志位的地方跟安全点是重合的。
触发点:
1.方法返回之前。
2.调用某个方法之后。
3.抛出异常的位置。
4.循环的末尾。
# 8.2 安全区域
安全点是针对正在执行的线程设定的。
如果一个线程处在Sleep或者中断状态,它就不能响应JVM的中断请求,再运行到安全点上。
因此JVM引入了安全区域的概念。
安全区域是指在一段代码中,引用关系不会发生变化。在这个区域内的任意地方开始GC都是安全的。