Java垃圾回收算法

垃圾回收

判断对象是否是垃圾

引用计数法

在对象中添加一个引用计数器,如果被引用计数器加1,引用失效计数器减1,如果计数器为0则被标记为垃圾。

原理简单,效率高。但是在Java中很少使用,因为存在对象间循环引用的问题,导致计数器无法清零。

可达性分析

主流语言的内存管理都使用可达性分析判断对象是否存活。基本思路是通过一系列称为“GC Roots”的根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程走过的路径称为引用链,如果某个对象到GC Roots没有任何引用链相连,则会标记为垃圾。

可作为GC Roots的对象:虚拟机栈和本地方法栈中引用的对象、类静态属性引用的对象、常量引用的对象。

GC算法

标记-清除算法

分为标记和清除阶段,首先从每个GC Roots出发依次标记有引用的对象,最后清除没有标记的对象。

执行效率不稳定,如果堆包含大量对象且大部分需要回收,必须进行大量的标记清除,导致效率随对象数量增长而降低。

存在内存空间碎片化问题,会产生大量不连续的内存碎片,导致以后需要分配大对象时容易出发FullGC。

在这里插入图片描述

标记-复制算法

为了解决内存碎片化问题,将可用内存按容量划分为大小相等的两块,每次只使用其中一块。当使用的这块空间用完了,就将存活对象复制到另一块,再把已使用过的内存空间一次清理掉。主要用于新生代。

实现简单,运行高效,解决了内存碎片化问题。代价是可用内存缩小为原来的一半,浪费空间。

HotSpot把新生代划分为一块较大的Eden和两块较小的Survivor(form,to),每次分配内存只使用Eden和Survivor中的一块。垃圾收集时将Eden和Survivor(from)中仍然存活的对象一次性复制到另一块Survivor(to)上,然后直接清理掉Eden和已经用过的Survivor(from)。from 和 to交换,to变成下一次的from,并且年龄+1,当年龄变成15时,移到老年代。

在这里插入图片描述

标记-整理算法

标记整理算法在对象存活率高时要进行较多的复制操作,效率低。如果不想浪费空间,就需要有额外空间分配担保。应对被使用内存中所有对象都存活的极端情况,所以老年代一般不使用此算法。

老年代使用标记-整理算法,标记过程与标记-清除算法一样,但不直接清理可回收对象,而是让所有存活对象都向内存空间一端移动,然后然后清理掉边界以外的内存。

标记-清除与标记-整理的差异在于前者是一种非移动式算法而后者是移动式。如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活的区域,是一种极为负重的操作。而且移动必须全称暂停用户线程。如果不移动对象就会导致空间碎片化问题,只能依赖更复杂的内存分配器和访问器解决。

在这里插入图片描述

分代收集算法

分代收集算法是目前大部分JVM所采用的方法,其核心思想是根据对象存活的不同生命周期将内存划分为不同的域,一般情况下将GC堆分为老年代和新生代。

老年代的特点是每次垃圾回收时只有少量对象需要被回收;

新生代的特点是每次垃圾回收时都有大量垃圾需要回收。因此不同区域有不同的算法。

新生代–复制算法

目前大部分JVM的GC对新生代都采取复制算法,因为新生代的特点是每次垃圾回收时都有大量垃圾需要回收,即要复制的操作少,但通常不是1:1来划分。一般将新生代划分为一块较大的Eden空间和两个较小的Survivor空间(From Space,To Space),每次使用Eden空间和其中一块Survivor空间,当进行回收时,将两块空间中还存活的对象复制到另一块Survivor中。

老年代与标记整理算法

老年代每次回收只需要少量的对象,因而采用标记整理算法。

  • JAVA虚拟机提到过的处于方法区的永久代,它用来存储class类,常量,方法描述等。对永久代的回收主要包括废弃常量和无用的类。
  • 对象的内存分配主要在新生代的Eden和Survivor的From Sapce(Survivor目前存放对象的那一块),少数情况会直接分配到老年代。
  • 当新生代的Eden和From Space空间不足时就会发生一次GC,进行GC后,Eden和From Space区的存活对象会被挪到To Space,然后将Eden和From Space进行清理。
  • 如果To Space不足够存储某个对象,则将这个对象存储到老年代。
  • 在进行GC 后,使用的便是Eden和To Space了,如此反复。
  • 当对象在Survivor区躲过一次GC 后,其年龄 +1 ,默认情况下年龄到15的对象就会被移到老年代。
分区收集法

分区算法是将整个堆空间划分为连续的不同小区间,独立回收。这样做的好处是可以控制一次回收多少个小区间,根据目标停顿时间,每次合理地回收若干个小区间,从而减少一次GC所产生的停顿。

JAVA中的四种引用

强引用

Java中最常见的就是强引用。把对象赋给一个引用变量,这个变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到,JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因。

软引用

软引用需要用Soft Reference类来实现,描述非必需对象。在系统将发生内粗你溢出前,会把软引用关联的对象加入回收范围以获得更多内存空间。对于只有软引用的对象来说,当系统内存足够时他不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。用来缓存服务器中间计算结果及不需要实时保存的用户行为等。

弱引用

弱引用需要用WeakReference类来实现,描述非必需对象,它比软引用的生存周期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,总会回收该对象占用的内存。

虚引用

虚引用需要PhantomReference类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的作用主要是跟踪对象被垃圾回收的状态

GC垃圾回收器

Java堆内存被划分为新生代和年老代两部分,新生代主要使用复制和标记-清除算法;

年老代主要使用标记-整理垃圾回收算法,因此Java虚拟机针对新生代和年老代分别提供了多种不同的垃圾收集器,jdk1.6中Sun HotSpot虚拟机的垃圾收集器如下:

在这里插入图片描述

Serial(单线程、复制算法)

最基础的收集器,使用复制算法、单线程工作,只用于一个处理器或一条线程完成垃圾回收,进行垃圾收集时必须暂停其他所有工作线程。

Serial是虚拟机在客户端模式的默认新生代收集器,简单高效,对于内存受限的环境它是所有收集器中额外内存消耗最小的,对于处理器核心较少的环境,Serial由于没有线程的交互开销,可获得最高的单线程收集效率。

PerNew(Serial+多线程)

Serial的多线程版本,除了使用多线程进行垃圾收集外其余行为完全一致。

ParNew是虚拟机在服务端模式的默认新生代收集器,一个重要原因是除了Serial外只有它能与CMS配合。自从JDK9开始,ParNew加CMS不再是官方推荐的解决方案,官方希望被G1取代。

在这里插入图片描述

Parallel Scavenge(多线程、复制算法)

新生代收集器,基于复制算法是可并行的多线程收集器,与ParNew类似。

特点是它的关注点与其他收集器不同,Parallel Scavenge的目标是达到一个可控制的吞吐量,吞吐量就是处理器用于运行用户代码的时间与处理器消耗总时间的比值。

Serial Old(单线程、标记整理算法)

Serial的老年代版本,单线程工作,使用标记-整理算法。

Serial Old是虚拟机在客户端模式的默认老年代收集器,用于服务端有两种用途:

  • 在JDK5及以前与Parallel Scavenge搭配
  • 作为CMS失败预案

Serial和Serial Old搭配垃圾回收过程图:

在这里插入图片描述

Parallel Old(多线程、标记整理算法)

Parallel Scavenge的老年代版本,支持多线程,基于标记-整理算法。JDK6提供,注重吞吐量可考虑Parallel Scavenge加Parallel Old。

在这里插入图片描述

CMS(多线程、标记清除算法)

获取最短回收停顿时间为目标,基于标记-清除算法,过程相对复杂。

分为四个步骤:初始标记、并发标记、重新标记、并发清除。

初始标记和重新标记需要STW(Stop The World,系统停顿)

  • 初始标记仅是标记GC Roots能直接关联的对象,速度很快。仍需要暂停所有工作线程。
  • 并发标记进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程.
  • 重新标记为了修正在并发标记期间,因用户线程继续运行而导致标记产生变动的那一部分的标记记录,仍然需要暂停所有工作线程.
  • 并发清除清除GC Roots不可达对象,和用户线程一起工作,不需要暂停工作线程,由于耗时最长并发标记和并发清除过程中,垃圾收集线程可以和用户一起并发工作,所以整体上来看CMS收集器的内存回收和用户线程是一起并发地执行。

CMS收集器工作过程:

在这里插入图片描述

缺点:

  • 对处理器资源敏感,并发阶段虽然不会导致用户线程暂停,但会降低吞吐量
  • 无法处理浮动垃圾,有可能出现并发失败而导致Full GC
  • 基于标记-清除算法,产生空间碎片。
G1(标记整理)

Garbage first垃圾收集器是目前垃圾收集器理论发展的最前沿成果,相比于CMS收集器,G1收集器两个最突出的改进是:

  • 基于标记-整理算法,不产生内存碎片
  • 可以非常精确控制停顿时间,在不牺牲吞吐量前提下,实现低停顿垃圾回收。

G1收集器避免全区域垃圾收集,它把堆内存划分为大小固定的几个独立区域,并且跟踪这些区域的垃圾收集进度,同时在后台维护一个优先级列表,每次根据所允许的收集时间,优先回收垃圾最多的区域。区域划分和优先级区域回收机制,确保G1收集器可以在有限时间获得最高的垃圾收集效率。

G1的运作过程:

  • **初始标记:**标记GC Roots能直接关联的对象,让下一阶段用户线程并发运行时能正确地在可用Region中分配新对象。需要STW但耗时很短,在Minor GC时同步完成。
  • **并发标记:**从GC Roots开始对堆中对象进行可达性分析,递归扫描整个堆的对象图。耗时长但可与用户线程并发,扫描完成后要重新处理SATB记录的在并发时有变动的对象。
  • **最终标记:**对用户线程做短暂暂停,处理并发阶段结束后仍然遗留下来的少量SATB记录。
  • **筛选回收:**对各Region的回收价值排序,根据用户期望停顿时间指定回收计划。必须暂停用户线程,由多条收集线程并行完成。

由用户指定期望停顿时间是G1的一个强大功能,但该值不能设得太低,一般设置为100~300ms

在这里插入图片描述

ZGC

JDK11中加入的具有实验性质的低延迟垃圾收集器,目标是尽可能在不影响吞吐量的前提下,实现在任意堆内存大小都可以把停顿时间限制在10ms以内的低延迟。

基于Region内存布局,不设分代,使用了读屏障、染色指针和内存多重映射等技术实现可并发的标记-整理,以低延迟为首要目标。

ZGC的Region具有动态性,是动态创建和销毁的,并且容量大小也是动态变化的。

参考书籍:《深入理解Java虚拟机》周志明

参考博客:https://blog.csdn.net/u011080472/article/details/51324422

图片来源于以上博客(侵删)

障、染色指针和内存多重映射等技术实现可并发的标记-整理,以低延迟为首要目标。

ZGC的Region具有动态性,是动态创建和销毁的,并且容量大小也是动态变化的。

参考书籍:《深入理解Java虚拟机》周志明

参考博客:https://blog.csdn.net/u011080472/article/details/51324422

图片来源于以上博客(侵删)

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值