垃圾回收机制及算法
垃圾回收的概述
垃圾收集被称为GC,判断对象是否还存活进行回收,垃圾回收作用的区域就是堆区和方法区,主要是堆区。
垃圾回收可以有效的防止内存泄漏,有效的利用可使用的内存,不用考虑内存的管理,但是自动内存管理对于检测内存泄漏和内存溢出问题更加困难。
引用计数法
给每个创建的对象添加一个引用计数器,每当有一个地方引用此对象时,计数器就加1,引用失效就减1,为0表示对象不能被使用。缺点:如果A和B互相引用,却没被其他任何对象引用,那么这个两个对象其实已经是垃圾对象,但是他们引用数不为0,无法回收,java并没有使用此算法,所以循环引用的对象仍然会被回收。
可达性分析算法
可达性分析算法是主流编程语言使用的垃圾回收算法。通过一系列的“GC Roots”的对象作为起始点,从起始点开始向下搜索到对象的路径。搜索所经过的路径称为引用链(Reference Chain),当一个对象到任何GC Roots都没有引用链时,则表明对象“不可达”,即该对象是不可用的。其实就是从各种根节点往下搜,不可达就是垃圾对象。
可作为GC Roots的对象:
栈帧中的局部变量表中的reference引用所引用的对象
方法区中static静态引用的对象
方法区中final常量引用的对象
本地方法栈中JNI(Native方法)引用的对象
Java虚拟机内部的引用
所有被同步锁(synchronized关键字) 持有的对象
判断对象是否存活
在可达性分析算法中判定为不可达的对象, 也不是“非死不可”的, 这时候它们暂时还处于“缓刑”阶段, 要真
正宣告一个对象死亡, 至少要经历两次标记过程。
第一次标记:对象在进行可达性分析后发现没有与GC Roots相连接的引用链,做第一次标记,标记之后再判断这个对象是否要执行finalize()方法,此方法就是为回收对象创造的逃脱机制。如果没有重写finalize方法或者finalize方法已经被虚拟机调用了,那就按原本计划下去,直到被回收。如果重写了finalize()方法,该对象将会被放置在一个名为F-Queue的 队列之中,之后收集器将对F-Queue中的对象进行第二次小规模的标记,如果此时对象重新与引用链上的任何一个对象建立关联,即可逃脱被垃圾回收的命运,由于finalize方法只会被执行一次,因此自救也只会有一次。
对象的引用
JDK1.2之后,Java对引用的概念做了扩充,将引用分为 强引用(Strong Reference) 、 软引用(Soft Reference) 、 弱引用(Weak Reference) 和 虚引用(Phantom Reference) 四种,这四种引用的强度依次递减。
强引用:强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。比如我们常用的 A a = new A就是强引用。
软引用:如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。 软引用可以和一个引用队列 ReferenceQueue联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。
弱引用:用来描述那些非必须对象, 但是它的强度比软引用更弱一些, 被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工作, 无论当前内存是否足够,都会回收掉只被弱引用关联的对象。弱引用相比较于软引用生命周期更短,且内存是否充足不影响回收。
虚引用:最弱的一种引用关系。如果一个对象仅持有虚引用,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用必须和引用队列 ReferenceQueue联合使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。
分代收集理论
根据对象的生命周期将内存划分,然后进行分区管理,他建立在两个分代假说上:弱分代假说和强分代假说,即大部分对象很快被回收,但是对象经过的回收次数越多越难被回收。
垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不同的区域,然后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数) 分配到不同的区域之中存储。如果一个区域中大多数对象都是朝生夕灭, 难以熬过垃圾收集过程的话,那么把它们集中放在一起, 每次回收时只关注如何保留少量存活而不是去标记那些大量将要被回收的对象, 就能以较低代价回收到大量的空间;如果剩下的都是难以消亡的对象, 那把它们集中放在一块, 虚拟机便可以使用较低的频率来回收这个区域, 这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用。
回收类型的划分:
部分收集(Partial GC):新生代收集(Minor GC/Young GC)、老年代收集(Major GC/Old GC) CMS、混合收集(Mixed GC)G1
整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集
标记-清除算法
算法分为“标记”和“清除”两个阶段: 首先标记出所有需要回收的对象,在标记完成后,统一回收掉所有被标记的对象,也可以反过来,标记存活的对象,统一回收所有未被标记的对象。
缺点:
1) 效率低,标记和清除两个过程的执行效率随对象数量增长而降低;
2) 内存空间的碎片化,标记-清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。
标记-复制算法
决标记-清除算法面对大量可回收对象时执行效率低的问题,内存按容量划分为大小相等的两块,每次只使用其中的一块。 当这一块的内存用完了, 就将还存活的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉,因此也不用考虑内存空间碎片的复杂情况。
缺点:
1) 需要两倍空间,也就是需要提前预留一半的内存区域用来存放存活的对象,可用空间少了,总体GC频率增高
2) 存活对象数量越多,需要复制的对象就越多,成本提高,效率降低
3) 老年代存活对象较多,无法使用这种算法。
HotSpot虚拟机针对堆进行了分代,年轻代分成一个Eden区和两个Survivor区,默认8:1:1,就是基于弱分代假说,即大部分对象都熬不过第一次垃圾回收,分配一块较大区域放第一次垃圾回收的对象,两个Survivor区就是为了复制存活的对象,即始终有一个Survivor区被浪费。
标记-整理算法
标记-复制算法在对象存活率较高时就要进行较多的复制操作,效率将会降低,而且复制算法浪费空间,因此结合“标记-清除”和“复制”两个算法的优点,产生了标记整理算法,即第一个阶段从根节点开始标记所有被引用对象,第二阶段遍历整个堆,把存活对象向一端移动,直接清理边界以外的内存。
缺点:
移动则内存回收时会更复杂,不移动则内存分配时会更复杂,从垃圾收集的停顿时间来看,不移动对象停顿时间会更短,甚至可以不需要停顿,但是从整个程序的吞吐量来看,移动对象会更划算,根据实际场景使用相应算法。
垃圾收集器概述
垃圾回收算法分类两类,第一类算法判断对象生死算法,引用计数法、可达性分析算法 ;第二类收集死亡对象方法有四种,标记-清除算法、标记-复制算法、标记-整理算法。一般的实现采用分代回收算法,根据不同代的特点应
垃圾回收算法分类两类,第一类算法判断对象生死算法,引用计数法、可达性分析算法 ;第二类收集死亡对象方法有四种,标记-清除算法、标记-复制算法、标记-整理算法。一般的实现采用分代回收算法,根据不同代的特点应用不同的算法。垃圾回收算法是内存回收的方法论。垃圾收集器是算法的落地实现。
串行垃圾回收(Serial)
单线程环境设计且只使用一个线程进行垃圾回收,会暂停所有的用户线程,不适合交互性强的服务器环境。
并行垃圾回收(Parallel)
多个垃圾收集器线程并行工作,同样会暂停用户线程,适用于科学计算、大数据后台处理等多交互场景。
并发垃圾回收(CMS)
用户线程和垃圾回收线程同时执行,不一定是并行的,可能是交替执行,可能一边垃圾回收,一边运行应用线程,
不需要停顿用户线程,互联网应用程序中经常使用,适用对响应时间有要求的场景。
G1垃圾回收
G1垃圾回收器将堆内存分割成不同的区域然后并发地对其进行垃圾回收。
组合关系,即一个回收年轻代一个回收老年代,虚线弃用,即JDK8后不能使用。
DK8中默认使用组合是: Parallel Scavenge GC 、ParallelOld GC
JDK9默认是用G1为垃圾收集器
JDK14 弃用了: Parallel Scavenge GC 、Parallel OldGC并移除了 CMS GC
GC性能指标
吞吐量:即CPU用于运行用户代码的时间与CPU总消耗时间的比值(吞吐量 = 运行用户代码时间 / ( 运行用户代码时间 + 垃圾收集时间 ))。例如:虚拟机共运行100分钟,垃圾收集器花掉1分钟,那么吞吐量就是99%
暂停时间:执行垃圾回收时,程序的工作线程被暂停的时间
内存占用:java堆所占内存的大小
收集频率:垃圾收集的频次
Serial收集器
单线程收集器,使用标记-复制算法,“单线程”的意义不仅仅说明它只会使用一个CPU或一个收集线程去完成垃圾收集工作;更重要的是它在垃圾收集的时候,必须暂停其他工作线程,直到垃圾收集完毕;目前基本被弃用,不过Serial收集器由于简单并且高效,因为Serial收集器没有线程间的交互,通过-XX:+UseSerialGC配置。
ParNew 收集器
ParNew收集器实质上是Serial收集器的多线程并行版本,使用标记-复制算法,多CPU效率更高,但是垃圾收集的时候仍然需要暂停其他工作线程来工作。ParNew收集器在单CPU服务器上的垃圾收集效率绝对不会比Serial收集器高;但是在多CPU服务器上,效果会明显比Serial好,通过-XX:+UseParNewGC配置,并通过- XX:ParllGCThreads指定线程数。
Parallel Scavenge收集器
吞吐量优先收集器,和ParNew收集器类似,是一个新生代收集器。使用复制算法的并行多线程收集器。Parallel Scavenge是Java1.8默认的收集器,特点是并行的多线程回收,以吞吐量优先。适合后台运算,交互不多的任务。
推荐组合方式 Parallel Scavenge GC+ Parallel Old GC 适用于JDK1.6以上,无法和CMS组合。
特点
1)Parallel Scavenge收集器的目标是达到一个可控制的吞吐量(Throughput); 吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间) (虚拟机总共运行100分钟,垃圾收集时间为1分钟,那么吞吐量就是99%)
2)自适应调节策略,自动指定年轻代、Eden、Survivor区的比例。
参数设置
-XX:+UseParallelGC :使用此收集器
-XX:MaxGCPauseMillis :最大垃圾收集停顿时间,毫秒级,不是越小越好,时间越短,那么就是空间换时间的策略,新生代就会变小,收集频率高了,即多次收集累加停顿时间会延长,造成吞吐量下降。
-XX:GCTimeRatio:大于0小于100的整数,即假设GCTimeRatio的值为n,那么系统将花费不超过1/(1+n)的时间用于垃圾收集,默认99%。
- XX:ParllGCThreads:设置年轻代线程数,小于等于8默认等于CPU核心数,大于8设置3+(5*CPU_COUNT)/8。
-XX:+UseAdaptiveSizePolicy:虚拟机会根据系统运行情况进行自适应调节年轻代、Eden、Suvisor区的比例,晋升老年代的对象年龄等。
Serial Old收集器
Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用标记-整理算法。在JDK1.5及之前,与Parallel Scavenge收集器搭配使用,Parallel Scavenge收集器架构中本身有PS MarkSweep收集器来进行老年代收集,但是他与Serial Old的实现几乎一样,所以两者并没有做任何区别。通过配置-XX:+UseSerialGC使用。
Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,支持多线程并发收集,基于标记-整理算法实现,JDK6出现的,用于代替JDK6之前组合老年代的Serial Old收集器,适用于多CPU模式,在注重吞吐量以及CPU资源敏感的场景非常适合。通过配置-XX:+UseParallelOldGC来使用此收集器
CMS 收集器
老年代垃圾收集器,以获取最短垃圾收集停顿时间为目标的收集器,尽可能缩短垃圾收集时用户线程的停顿时间,适用于与用户交互的程序,系统停顿时间最短,给用户带来良好的体验,CMS收集器使用的算法是标记-清除算法实现。
CMS垃圾收集过程
初始标记->并发标记->重新标记->并发清除
初始标记和重新标记都会停止所有线程,时间较短。最消耗时间的并发标记与并发清除阶段都不需要暂停工作。
初始标记(Initial-Mark)阶段:暂停所有工作线程,标记GC Roots 能够关联到的对象。
并发标记(Concurrent-Mark)阶段:从GC Roots的直接关联对象开始遍历整个对象图。
重新标记(Remark)阶段:由于并发标记阶段,程序的工作线程会和垃圾收集线程同时运行或者交叉运行可能造成标记产生变动,针对这部分变动的对象重新标记。
清除并发(Concurrent-Sweep)阶段:清理删除掉标记判断已经死亡的对象,并释放内存空间。
CMS收集器缺点
1)CMS收集器对CPU资源非常敏感。并发标记和并发清理阶段会因为占用了一部分线程而导致应用程序变慢,总吞吐量会降低。CMS默认启动的回收线程数是(处理器核心数量 +3) /4,4核以下效率会明显降低。
2)CMS收集器无法处理浮动垃圾,因为CMS是并发的,需要预留足够内存空间提供给用户线程使用,导致不能再老年代填满之后触发回收。如果老年代已达到阀值,浮动垃圾就会晋升失败,无法到达老年代,老年代就会抛弃CMS收集器启用Serial Old收集器来进行串行化回收,消耗时间大幅度提升。
3)空间碎片:CMS是一款基于标记-清除算法实现的收集器,所有会有空间碎片的现象。通过-XX: CMSFullGCsBeforeCompaction参数可以设置若干次不整理空间的Full GC之后下一次Full GC整理空间,默认0即每次整理。
并发可达性分析-三色标记
回收过程:标记出哪些对象是存活的,哪些是垃圾,然后进行回收(清除/复制/整理),如果有移动过对象(复制/整理),还需要更新引用。
白色:尚未访问过。
灰色:当前对象已访问过,但是当前对象引用到的其他对象尚未全部访问完。全部访问后,会转换为黑色。
黑色:当前对象已访问过,而且当前对象引用到的其他对象也全部访问过了。
执行过程
1) 初始时,所有对象都在 【白色集合】中;
2) 当GC Roots 直接引用到这个对象,他就会被挪到【灰色集合】中;
3) 再灰色集合中获取对象,如果这个对象还引用到其他对象,把其他对象移进来,自己被挪到【黑色集合】
4) 重复执行,直到所有对象都被查找完毕,灰色集合为空。
5) 剩余【白色集合】的对象即为GC Roots 不可达,进行回收。
当Stop The World时,对象间的引用是不会发生变化的,可以轻松完成标记。而当需要支持并发标记时,即标记期间应用线程还在继续跑,对象间的引用可能发生变化,多标和漏标的情况就有可能发生。
多标-浮动垃圾
标记执行到E断开了,这时候E应该白色垃圾对象,但是实际他已经被置为灰色了,所以本轮垃圾回收,他就不会被清理,但是在下一轮回收,就会被清理掉了,也被称为浮动垃圾,即本该回收却没有回收到,对程序无影响。
漏标
标记执行如下,D引用E,D黑色,E灰色,E引用G,下一步应该是E黑色,G灰色,但是这时候E和G之间断开了,G就成了垃圾对象,后面D又引用到了G,即D->E->G和D->G同时并发发生,因为D已经是黑色了,不会再做重新遍历处理,导致回收的时候G对象被当做垃圾回收掉,影响程序正确性。
漏标的两大必要条件:
条件一:灰色对象断开了白色对象的引用;即灰色对象原来成员变量的引用发生了变化。
条件二:黑色对象重新引用了该白色对象;即黑色对象成员变量增加了新的引用。
解决办法:将对象G记录起来,然后作为灰色对象再进行遍历即可。如定义一个集合,等初始标记完成,再次标记确定,也就是CMS中的重新标记阶段。
G1垃圾收集器
Garbage First是一款面向服务端应用的垃圾收集器,主要针对配备多核CPU及大容量内存的机器,以极高概率满足
GC停顿时间的同时,还兼具高吞吐量的性能特征。
G1的特点
1)G1把内存划分为多个独立的区域Region
2)G1仍然保留分代思想,保留了新生代和老年代,但他们不再是物理隔离,而是一部分Region的集合
3)G1能够充分利用多CPU、多核环境硬件优势,尽量缩短暂停时间STW
4)G1整体采用标记整理算法,局部是采用复制算法,不会产生内存碎片
5)G1的停顿可预测,能够明确指定在一个时间段内,消耗在垃圾收集上的时间不超过设置时间
6)G1跟踪各个Region里面垃圾的价值大小,会维护一个优先列表,每次根据允许的时间来回收价值最大的区域,从 而保证在有限事件内高效的收集垃圾。
Region区域
G1不再坚持固定大小以及固定数量的 分代区域划分,而是把连续的Java堆划分为多个独立区域(Region),每一 个Region都可以根据需要,扮演新生代的Eden空间、Survivor空间,或者老年代空间。
1) G1收集器时,它将整个Java堆划分成约2048个大小相同的独立Region块,每个块大小可浮动,为2的次幂,即2M、4M等等。
2) 新生代和老年代不是物理隔离,它们都是一部分Region的集合。
3) G1垃圾收集器新增Humongous内存区域,用于存储大对象,即超过1 .5个region块。
G1垃圾回收过程
G1提供了两种GC模式,Young GC(年轻代)和Mixed GC(年轻代和老年代),两种均是完全Stop The World的。
Young GC:选定所有年轻代里的Region。通过控制年轻代的region个数,即年轻代内存大小,来控制young GC 的时间开销。
Mixed GC:选定所有年轻代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代 Region,因为老年代也是一个个的region块,通过统计得出最大收益的块进行回收。
初始标记 :和CMS一样只标记GC Roots直接关联的对象。
并发标记 :进行GC Roots Traceing过程。
最终标记 :修正并发标记期间,因程序运行导致发生变化的那一部分对象,计算所有区域活跃度并且回收已经标记死亡的对象,活跃度计算规则,即这个区域的对象是否还在频繁被引用等等,活跃低就说明快要挂了。
筛选回收 :根据时间来进行价值最大化收集。
Young GC
对年轻代所有对象进行标记,如果是垃圾就清理,如果不是就复制到幸存者区域,当次数达到阀值,再复制到老年区,年轻代的GC会出现SWT,并且使用多个线程并行完成。
Mixed GC
初始标记阶段:对存活对象进行标记。
并发标记阶段:用户线程和标记一起运行,找到空白区域,进行删除标记,并且根据并发用户线程运行计算区域活跃度。
最终标记阶段:清理删除标记的区域,使用快照(SATB)算法。
筛选回收阶段:使用复制/整理算法对活跃度最低的区域进行最快地收集,这里老年代的GC和年轻代GC是一起垃圾收集的。
G1常用参数