尚硅谷2020最新版宋红康JVM教程学习笔记 六 垃圾回收算法

点击查看合集

什么是垃圾

运行程序中没有任何指针指向的对象就是垃圾。如果不进行垃圾回收,内存迟早都会被消耗完。除了释放没用的对象,垃圾回收也可以清楚内存里的内存碎片。碎片整理将所占用的堆内存移到堆的一端,以便JVM将整理出的内存分配给新的对象。
随着应用程序所应付的业务越来越庞大,没有GC就不能保证应用程序的正常进行。而经常造成STW的GC又跟不上实际的需求,所以需要不断地对GC进行优化。

自动内存管理

无需开发人员手动参与内存的分配与回收,这样降低内存泄露和内存溢出的风险。自动内存管理机制,将程序员从繁重的内存管理中释放出来,可以更专心地专注于业务开发。
但是如果过度地依赖自动内存管理,可能会弱化java开发人员在程序出现内存溢出时定位问题和解决问题的能力。
当需要排查内存溢出、内存泄漏问题时,或者垃圾收集称为系统达到更高并发量的瓶颈时,我们就必须对这些自动化技术实施必要的监控和调节。
java堆是垃圾收集器的工作重点。从次数上讲,频繁收集Young区较少收集Old区,基本不动方法区(永久代/元空间)

垃圾标记阶段

引用计数算法

对每个对象保存一个整形的引用计数器属性。用于记录对象被引用的情况。有引用+1.引用失效-1.当为0的时候对象就不再被使用,可进行回收
优点:实现简单,垃圾对象便于辨识。判定效率高,回收没有延迟性。(发现引用次数为0后直接删除,而不用等待一起GC)
缺点:
1.它需要单独的字段存储计数器,会增加存储空间开销。
2.每次复制都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
3.引用计数器有一个严重的问题,即无法处理循环引用的情况。因此Java的垃圾回收器中没有使用这类算法(内存泄露)

可达性分析算法(根搜索算法、追踪性垃圾收集)

1.可达性分析算法是以根对象集合(GC Roots 根集合就是一组必须活跃的引用)按照从上至下的方式搜索被根对象集合所连接的目标对象是否可达。
2.使用可达性分析算法后,内存中的存活对象都会被根对象集合直接或间接连接着,搜索走过的路径称为引用链
3.如果目标对象没有任何引用链相连,则是不可达的,可以标记为垃圾对象
4.被根对象集合直接或间接连接的对象才是存活的对象。

GC Roots(根对象集合)

根对象集合包括:
1.虚拟机栈/本地方法栈中引用的对象(局部变量表)
2.方法区/堆中静态属性引用的对象。(Java类的引用类型静态变量)
3.方法区/堆中常量引用的对象(字符串常量池里的引用)
4.被同步锁synchronized持有的对象
5.java虚拟机内部的引用(基本数据类型对应的Class对象,一些常驻异常对象 (NullpointerException OutOfMemoryError)系统类加载器)
6.反映java虚拟机内部情况的JMXXBean\JVMTI中注册的回调\本地代码缓存等
优点:
实现简单并且执行高效,并且可以有效地接再循环引用问题,防止内存泄露的发生。

对象终止( finalization)机制

java语言提供了对象终止(finalize方法)机制来允许开发人员提供对象被销毁之前的自定义处理逻辑。该对象被回收前会调用该方法。
由于finalize()方法的存在,虚拟机中的对象一般处于三种可能的状态。
1.可触及的:存在到达该对象的引用链
2.可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活
3.不可触及的:finalize()被调用,并且没有复活,就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用依次。
只有对象在不可触及时才真正要被回收。

标记次数

判断一个对象是否可回收,至少要经历两次标记的过程
1.没有到达该对象的引用链
2.1:如果没有重写finalize()方法或者已经执行过该方法,则直接被判定为不可触及
2.2:如果重写了finalize()方法,且没有被调用过那么对象会被插入到F-Queue队列中,由虚拟机自动创建一个低优先级的Finalizer线程触发其finalize()方法执行
2.3:finalize()方法是对象免于回收的最后机会,稍后GC会对F-Queue队列中的对象进行第二次标记。如果对象在finalize()方法中与引用链上的任何一个对象建立了联系,那么第二次标记时就会被移出要被回收的对象的集合。(如果之后再次出现没有到达该对象的引用链的情况时finalize()方法也不会被再次执行,因此此方法只会执行一次)

垃圾清除阶段

当区分出存活对象和死亡对象后,GC接下来的任务就是执行垃圾回收,释放掉无用对象所占用的内存空间。

标记-清除(Mark-Sweep)算法

当堆中的内存耗尽,就会停止程序(stop the world),然后进行两项工作,第一项是标记,
第二项是清除
标记:从引用根节点开始遍历,标记所有被引用的对象。(非垃圾对象)
清除:从堆的头到尾进行线性遍历,如果发现某个对象在其Header中没有标记为可达对象,则将其回收。(清除并不是真的置空,而是把需要清除的地址保存在空闲的地址列表里。)
在这里插入图片描述
优点:
容易理解
缺点:
1.效率不高
2.GC时,需要停止整个应用程序。
3.GC后,内存空间变的不连续,产生内存碎片。需要维护一个空闲表。

复制(Copying)算法

将活着的内存空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收(新生代也使用复制算法)
在这里插入图片描述
优点:
1.没有标记和清除过程,实现简单,运行高效
2.复制过去以后保证空间的连续性,不会出现碎片问题(用指针碰撞)
缺点:
1.会浪费一半的内存空间
2.对于G1这种分拆成为大量region的GC,复制而不是移动,意味着GC需要维护region之间对象引用关系,不管是内存占用或者时间开销都不小

标记-压缩(标记-整理、Mark-Compact)算法

在这里插入图片描述
标记-整理算法可以将标记的存活对象按照内存地址依次排列,而未被标记的内存会被清理掉。如此一来,当我们需要给喜怒第项分配内存时,JVM只需要持有一个内存的起始地址即可,而不用去维护一个空闲列表。
优点:
1.GC后不出现内存碎片
2.内存利用率高
缺点:
1.效率上低于复制算法
2.移动对象时,如果对象被其他对象引用,则还需要调整引用的地址

三种算法对比
在这里插入图片描述

分代收集算法

前面所有这些算法中,并没有一种算法可以完全替代其他算法,都有各自的优点。分代收集算法应运而生。
因为对象的声明周期不同,因此适合的回收算法也不同。堆区分为老年代和年轻代,这样就可以根据各个年代的特点使用不同的回收算法。
年轻代:复制算法
老年代:标记-清除算法或者是标记-清除算法和标记-整理算法结合使用。
HotSport的CMS回收器是基于标记清除算法实现的,对于对象的回收效率很高。而对于碎片问题,CMS采用基于Mark-Compact算法的Serial Old回收器作为补偿措施:当内存回收不佳(碎片导致Concurrent Mode Failure时)将采用Serial Odl 执行Full GC以达到对老年代内存的整理。

增量收集算法

上述现有的算法,在垃圾回收时,应用软件将处于STW(Stop the World)状态。此时应用程序的线程都会挂起,暂停一切正常的工作,从而严重影响用户体验或者系统的稳定性。增量收集算法就是用来解决这个问题。

基本思想:
如果一次性将所有的垃圾进行处理,需要造成系统长时间的停顿,那么就可以让垃圾收集线程和应用程序线程交替执行。每次垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。一次反复,直到垃圾收集完成

总的来说,增量收集算法的基础仍是创痛的标记-清除和复制算法。增量收集算法通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作

缺点:
线程切换和上下文转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

分区算法

堆空间越大GC的时间越长,STW也就越长,因此将一块大的内存区域分割成多个小块,根据目标的停顿时间,每次合理的回收若干个小区间(region),而不是整个堆空间,从而减少每次GC的STW。
分代算法将对象按生命周期长短分成两个部分,分区算法将整个堆空间划分成连续的不同小区间。每一个小区间都独立使用,独立回收。这种算法的好处是可以控制一次回收多少个小区间。从而控制每次GC的STW时间(低延迟)

注意:这些只是基本的算法思路,实际GC实现过程要复杂的多,目前还在发展中的前沿GC都是混合算法,并且并行和并发兼备

猜你喜欢

转载自blog.csdn.net/qq_30033509/article/details/110956370
今日推荐