序言:此文章只讲清楚三件事:哪些内存需要回收?什么时候回收?如何回收?希望读者再阅读完后可以评论区卷起来哟!!
目的:当需要排查各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,我们必须进行必要的监控和调节。
判断对象已死:
引用计数法:
在对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加一,当引用失效时,计数器值就减一。任何时候计数器值为零的对象就是不可能再被使用的。
问题:这个看似简单的算法在很多情况下必须配合大量额外处理才能保证正确工作,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。
可达性分析算法:
从GC Roots的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”。如果某个对象到GC Roots间没有任何引用链相连,则证明此对象是不可能再被使用的。
什么对象能作为GC Roots的对象:
虚拟机栈(栈帧中的本地变量表)中引用的对象。
方法区中类静态属性引用的对象。
方法区中常量引用的对象。
本地方法栈中Native方法引用的对象。
java虚拟机内部的引用,如基本数据类型对应的Class对象,异常对象,系统类加载器。
被同步锁synchronized关键字持有的对象。
反映java虚拟机内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等。
再谈引用:判断对象是否存活都和“引用”离不开关系。
强引用:类似:Object obj = new Object(),无论任何情况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。
软引用:描述一些还有用但非必须的对象。只被软引用关联的对象,在系统中发生内存溢出之前,会把这些对象列进回收范围之中进行二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。
弱引用:描述那些非必须的对象。被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作,无论当前内存是否足够,都会回收掉植被弱引用关联的对象。
虚引用:为一个对象设置虚引用关联的唯一目的只是为了能在这个对象被收集器回收时收到一个系统通知。
垃圾收集算法:
标记-清除算法:先标记、后清除。先将不可回收对象进行标记,剩下的那些需要回收的统一清除。缺点:会产生大量不连续的内存碎片。
标记-复制算法:将内存分为两块,每次使用其中的一块,当内存不足时,将存活的对象复制到另一块内存中,剩下的直接清除。缺点:虽然回收后内存时连续的,但是每次只使用其中的一块会浪费一些内存。
标记-整理算法:类似标记清除算法,只是在回收的时候将存活的对象移动到一端,剩下的清除掉。缺点:虽然充分的利用了内存,也避免了内存碎片,但是要移动对象,效率很低。
分代收集理论:按照对象存活的特性分成不同的区域,在不同的区域采用不同的算法。
JVM中堆分为新生代和老年代:
新生代:分为Eden区,Survivor1、Survivor2,这些区域一般都是标记-复制算法。
老年代:对象在新生代中经历多次复制后仍然存活(默认对象年龄达到15),会被复制到老年代,或者大对象等直接在老年代分配,而老年代根据垃圾收集器不同会采用不同的算法,如标记-清除、标记-整理等。
经典垃圾收集器:
Serial收集器:单线程工作,在进行垃圾收集时,必须暂停其他所有工作线程,直到它收集结束。
ParNew收集器:Serial收集器的多线程版本。
Parallel Scavenge收集器:新生代收集器,基于标记-复制算法实现的收集器。目标是达到一个可控制等吞吐量。
Serial Old收集器:Serial收集器的老年代版本,单线程工作,基于标记-整理算法实现。
Parallel Old收集器:Parallel Scavenge收集器的老年代版本。基于标记-整理算法实现。
CMS收集器:以获取最短回收停顿时间为目标的收集器。(标记-清除算法)
运行过程:
初始标记:标记下GC Roots能够直接关联的对象,速度很快。但此步骤仍需要停顿用户线程。(需要STW)
并发标记:进行GC Roots跟踪的过程。(不需要STW)
重新标记:修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录。但此步骤仍需要停顿用户线程。(需要STW)
并发清除:清除标记阶段判断的已经死亡的对象。(不需要STW)
缺点:无法处理浮动垃圾,标记-清除算法会产生内存碎片。优点:并发收集、低停顿。
Garbage First收集器:(标记-复制算法)
虽然还保留着新生代和老年代,但是新生代和老年代不再是区域上的隔离,它将整个java堆划分为多个大小相等的独立区域,叫Region。而新生代和老年代就是由一个个Region动态组成的区域,它们可以是不连续的区间,每个Region根据需要扮演新生代的Eden空间、S1空间、S2空间以及老年代空间。除此之外,还有一类特殊的区域叫做Humongous,专门用来存储大对象。G1的堆内可以划分为多个Region,Region的总数默认有2048个。对于Region来说,是逻辑连续的空间,其大小的取值范围是1MB到32MB之间。
G1工作过程:
初始标记:标记下GC Roots能够直接关联的对象。(需要STW)
并发标记:从GC Roots开始对堆中的对象进行可达性分析,找出存活的对象,耗时比较长,但可以与用户线程并发执行。(不需要STW)
最终标记:修正并发标记期间因用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,虚拟机将这段时间对象变化记录在线程Remember Set Logs里面,最终标记需要把Remember Set Logs的数据合并到Remember Set中,这阶段需要停顿线程,但可以并行执行。(需要STW)
筛选回收:首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划。这阶段可做到和用户线程并发执行。但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高回收效率。(需要STW)
低延迟垃圾收集器:
Shenandoah收集器:(标记-复制算法)
初始标记:与G1相同。
并发标记:与G1相同。
最终标记:与G1相同。
并发清理:清理整个区域连一个存活对象都没有找到的Region(这类Region被称为Immediate Garbage Region)。
并发回收:把回收集里面的存活对象先复制一份到其他未被使用的Region之中。此阶段回收时间的长短取决于回收集的大小。
初始引用更新:并发回收阶段复制对象结束后,还需要把堆中所有指向旧对象的引用修正到复制后的新地址。此阶段会产生一个非常短暂的停顿。
并发引用更新:此阶段与用户线程并发执行,时间长短取决于内存中涉及的引用数量的多少。
最终引用更新:修正存在GC Roots中的引用。停顿时间只与GC Roots数量相关。
并发清理:经过并发回收和引用更新之后,整个回收集中所有的Region已再无存活对象,最后在调用一次并发清理来回收这些Region的内存空间,供以后新对象分配使用。
ZGC收集器:
对于并发整理算法的实现,Shenandoah使用转发指针和读屏障来实现,ZGC虽然也用到了读屏障,但是有一个标志性的设计就是采用了染色指针技术。
三色标记:
黑色:表示对象已经被垃圾收集器访问过,且这个对象的所有引用都已经扫描过,它是安全存活的。
灰色:表示对象已经被垃圾收集器访问过,但这个对象至少存在一个引用没有被扫描过。
白色:表示对象还没有被垃圾收集器访问过。在可达性分析刚开始阶段,所有对象都是白色,若可达性分析结束,仍是白色,则代表不可达。
阶段
并发标记:与G1、Shenandoah相同。
并发预备重分配:这个阶段需要根据特定的查询条件统计得出本次收集过程需要清理哪些Region,将这些Region组成重分配集。ZGC的重分配集只是决定了里面的存活对象会被重新复制到其他的Region中,里面的Region会被释放。
并发重分配:把重分配集中的存活对象复制到新的Region上,并为重分配集中的每个Region维护一个转发表,记录从旧对象到新对象的转向关系。
并发重映射:修正整个堆中指向重分配集中就旧对象的所有引用。