判断对象是否还被使用的算法
判断对象是否还在被使用的算法有:
引用计数算法
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器都为0的对象就是不可能被使用的。但是Java语言中没有选用引用计数算法来管理内存,其最主要原因是它很难解决对象之间的相互循环引用的问题。
根搜索算法
也称为“可达性分析算法”,通过一系列的名为GC Roots
的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots
没有任何引用链相连,按照图论的话来说就是从GC Roots
到这个对象不可达时,则证明此对象是不可用的。
在Java语言中,有如下的一些对象可以作为GC Roots
对象:
虚拟机栈(栈帧中的本地变量表)中引用的对象
本地方法栈(Native方法)中引用的对象
方法区中类静态属性引用的对象
方法区中常量引用的对象
垃圾回收/收集算法
标记-清除(Mark-Sweep)算法
算法分为“标记”和“清除”阶段。首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。它是最基础的收集算法,效率也很高,但是会带来两个明显的问题,一是效率问题, 二是空间问题(标记清除后会产生大量不连续的碎片)。
复制(Copying)算法
为了解决效率问题,“复制”收集算法出现了。它可以将内存分为大小相同的两块,每次使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另一块去,然后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收。
标记-整理(Mark-Compact)算法
根据老年代的特点推出的一种标记算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象回收,而是让所有存活的对象向一端移动,然后直接清理掉端边界以外的内存。
“标记-整理”算法一方面在“标记-清除”算法上做了升级,解决了内存碎片的问题,也规避了复制算法只能利用一半内存区域的弊端。看起来很美好,但从上图可以看到,它对内存变动更频繁,需要整理所有存活对象的引用地址,在效率上比复制算法要差很多。
分代回收(Generational Collection)算法
分代收集算法严格来说并不是一种思想或理论,而是融合上述3种基础的算法思想,而产生的针对不同情况所采用不同算法的一套组合拳。一般来说,根据对象存活周期的不同将内存分为几块,一般将Java堆分为新生代和老年代,这样我们就可以根据各个年代的特点选择合适的垃圾收集算法。 比如在新生代中,每次收集都会有大量对象死去,所以可以选择“复制”算法,只需要付出少量对象的复制成本就可以完成每次垃圾收集。而老年代的对象存活几率是比较高的,而且没有额外的空间对它进行分配担保,所以我们必须选择“标记-清除”或“标记-整理”算法进行垃圾收集。
垃圾回收/收集器
垃圾回收算法是内存回收的方法论,垃圾回收器则是内存回收的具体实现。Java虚拟机规范中对垃圾收集器的实现并没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大的差别,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。如下为Sun HotSpot虚拟机所提供的所有收集器:
从图中可以看出每个垃圾收集器所适用于的JVM运行时内存区域,如Serial/ParNew/Parallel Scavenge是年轻代区域的垃圾收集器,Serial Old/Parallel Old/CMS是年老代的垃圾收集器,而G1可以同时对年轻代和年老代的内存进行回收。在上图中如果两个收集器之间存在连线,则说明它们可以搭配在一起使用。
Serial收集器
Serial收集器是最基本、历史最悠久的收集器,在JDK 1.3.1版本前是虚拟机新生代垃圾收集的唯一选择。Serial收集器是一个单线程的收集器,而且在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束。这项工作是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户正常工作的线程全部停掉,这对很多应用来说是难以接受的。图展示了Serial收集器(老年代采用Serial Old收集器搭配)的运行过程:
Serial收集器采用复制算法在新生代工作,Serial Old收集器采用标记-整理算法在老年代工作。
Serial收集器简单而高效(与其他收集器的单线程相比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得更高的单线程收集效率。实际上到现在为止,它依然是HotSpot虚拟机运行在Client模式下的默认的新生代收集器。
ParNew收集器
ParNew收集器就是Serial收集器的多线程版本,它也是一个新生代收集器。除了使用多线程进行垃圾收集外,其余行为包括Serial收集器可用的所有控制参数、收集算法(复制算法)、Stop The World、对象分配规则、回收策略等与Serial收集器完全相同,两者共用了相当多的代码。ParNew收集器的工作过程如下图(老年代采用Serial Old收集器搭配):
ParNew收集器采用复制算法在新生代工作,Serial Old收集器采用标记-整理算法在老年代工作。
ParNew收集器除了使用多线程收集外,其他与Serial收集器相比并无太多创新之处,但它却是许多运行在Server模式下的虚拟机中首选的新生代收集器,这其中有一个与性能无关的重要原因是,除了Serial收集器外,目前只有它能和CMS收集器配合工作,CMS收集器是JDK 1.5推出的一个具有划时代意义的收集器。
ParNew收集器在单CPU的环境中绝对不会有比Serial收集器有更好的效果,甚至由于存在线程交互的开销,该收集器在通过超线程技术实现的两个CPU的环境中都不能百分之百地保证可以超越Serial收集器。在多CPU环境下,随着CPU的数量增加,它对于GC时系统资源的有效利用是很有好处的。ParNew收集器默认开启的收集线程数与CPU的数量相同,在CPU非常多的情况下可使用-XX:ParallerGCThreads
参数设置。
Parallel Scavenge收集器
Parallel Scavenge收集器也是一个并行的多线程新生代收集器,类似于ParNew收集器,它也使用复制算法。Parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等其他收集器的关注点是尽可能缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(高效率的利用CPU)。
停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户体验。而高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
Parallel Scavenge收集器除了会显而易见地提供可以精确控制吞吐量的参数,还提供了一个参数-XX:+UseAdaptiveSizePolicy
,这是一个开关参数,打开参数后,就不需要手工指定新生代的大小(-Xmn
)、Eden和Survivor区的比例(-XX:SurvivorRatio
)、晋升老年代对象年龄(-XX:PretenureSizeThreshold
)等细节参数了。虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种方式称为GC自适应的调节策略(GC Ergonomics)。自适应调节策略也是Parallel Scavenge收集器与ParNew收集器的一个重要区别。
另外值得注意的一点是,Parallel Scavenge收集器无法与CMS收集器配合使用,所以在JDK 1.6推出Parallel Old之前,如果新生代选择Parallel Scavenge收集器,老年代只有Serial Old收集器能与之配合使用。
Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”(Mark-Compact)算法。此收集器的主要意义也是在于给Client模式下的虚拟机使用。
Parallel Old收集器
Parallel Scavenge收集器的老年代版本。使用多线程和“标记-整理”算法。在注重吞吐量以及CPU资源的场合,都可以优先考虑Parallel Scavenge收集器和Parallel Old收集器。
CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器,它非常符合那些集中在互联网站或者B/S系统的服务端上的Java应用,这些应用都非常重视服务的响应速度。从名字中的Mark Sweep这两个词可以看出,CMS收集器是一种“标记-清除”算法实现的。CMS收集器是HotSpot虚拟机第一款真正意义上的并发收集器,它第一次实现了让垃圾收集线程与用户线程(基本上)同时工作。CMS收集器是适用在年代老区域的垃圾收集。它的运作过程相比于前面几种垃圾收集器来说更加复杂一些,整个过程分为四个步骤:
(1)初始标记
仅仅只是标记一下GC Roots能直接关联到的对象,速度很快,需要“Stop The World”。
(2)并发标记
同时开启GC和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以GC线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。
(3)重新标记
重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短。
(4)并发清除
开启用户线程,同时GC线程开始对为标记的区域做清扫。
由于整个过程中耗时最长的并发标记和并发清除的两个阶段,收集器线程都可以与用户线程一起工作。所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
从它的名字就可以看出它是一款优秀的垃圾收集器,主要优点:并发收集、低停顿。但是它有下面三个明显的缺点,一是对CPU资源敏感,二是无法处理浮动垃圾,三是它使用的回收算法-“标记-清除”算法会导致收集结束时会有大量空间碎片产生。
G1收集器
G1(Garbage-First)是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器。以极高概率满足GC停顿时间要求的同时,还具备高吞吐量性能特征。G1收集器是当今收集器技术发展最前沿的成果之一,HotSpot开发团队赋予它的使命是(在比较长期的)未来可以替换掉JDK 1.5中发布的CMS收集器。与其他GC收集器相比,它具备一下特点:
(1)并行与并发
G1能充分利用CPU、多核环境下的硬件优势,使用多个CPU(CPU或者CPU核心)来缩短Stop-The-World停顿时间。部分其他收集器原本需要停顿Java线程执行的GC动作,G1收集器仍然可以通过并发的方式让Java程序继续执行。
(2)分代收集
虽然G1可以不需要其他收集器配合就能独立管理整个GC堆,但是还是保留了分代的概念。
(3)空间整合
与CMS的“标记–清理”算法不同,G1从整体来看是基于“标记整理”算法实现的收集器,从局部上来看是基于“复制”算法实现的。
(4)可预测的停顿
这是G1相对于CMS的另一个大优势,降低停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为M毫秒的时间片段内。
G1收集器的运作大致可划分为以下几个步骤:
(1)初始标记
仅仅只是标记一下GC Roots能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程,但耗时很短。
(2)并发标记
从GC Roots开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。
(3)最终标记
继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,这阶段需要停顿线程,但是可并行执行。
(4)筛选回收
首先对各个Region中的回收价值和成本进行排序,根据用户所期望的GC停顿是时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
通过下图可以比较清楚地看到G1收集器的运作步骤中并发和需要停顿的阶段:
G1收集器在后台维护了一个优先列表,每次根据允许的收集时间,优先选择回收价值最大的Region(这也就是它的名字Garbage-First的由来)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了GF收集器在有限时间内可以尽可能高的收集效率(把内存化整为零)。
小结
到JDK8为止,默认的垃圾收集器是Parallel Scavenge和Parallel Old。从JDK9开始,G1收集器成为默认的垃圾收集器。目前来看,G1回收器停顿时间最短而且没有明显缺点,非常适合Web应用。在JDK8中测试Web应用,堆内存6G,新生代4.5G的情况下,Parallel Scavenge回收新生代停顿长达1.5秒,G1回收器回收同样大小的新生代只停顿0.2秒。
Minor GC、 Major GC、 Full GC
Minor GC
Minor GC指对年轻代的堆内存进行垃圾回收。
Minor GC触发条件是Eden区域满了。Minor GC的工作内容主要有:
(1)将Eden中没有引用的对象直接被GC掉,其他还有引用的对象会复制到To Survivor区域。
(2)会将From Survivor中还存活的对象,一部分复制到To Survivor,一部分满足条件(这个条件后面会说明)的对象复制到年老代。
(3)From Survivor和To Survivor互换角色,From Survivor会变成To Survivor,To Survivor会变成From Survivor。
通过Minor GC之后,Eden会被清空,Eden区中绝大部分对象会被回收,而那些无需回收的存活对象,将会进到Survivor区。在Survivor区会将Eden区和From存活的对象放到Survivor的To区(如果To区不够,则直接进入Old区)。然后From Survivor和To Survivor互换角色,等待下次的Minor GC的到来。
Major GC
Major GC指对年老代的堆内存进行垃圾回收。
Full GC
Full GC指即对年轻代又对年老代堆内存进行垃圾回收。Full GC时会同时进行Major GC和Minor GC。Full GC触发条件如下:
(1)调用System.GC时,系统建议执行Full GC,但是不必然执行
(2)老年代空间不足
(3)方法区空间不足
(4)通过Minor GC后进入老年代的平均大小大于老年代的可用内存
一点补充
Survivor中的对象进入年老代的满足如下条件之一即可:
(1)部分对象会在From Survivor和To Survivor区域中复制来复制去,如此交换15次(由JVM参数MaxTenuringThreshold决定,这个参数默认是15),最终如果还是存活,就存入到老年代。
(2)如果对象的大小大于Eden的二分之一会直接分配在年老代
(3)动态年龄判断,大于等于某个年龄的对象超过了Survivor空间一半,这些大于等于某个年龄的对象直接进入老年代
学习资料参考于:
https://cloud.tencent.com/developer/article/1592943
https://mp.weixin.qq.com/s/feJKRqYJTVEIxl6jvjevAg