当前位置: 首页 > 科技观察

说明GC算法的动态图——让垃圾收集动起来!_0

时间:2023-03-22 12:34:03 科技观察

说到Java的垃圾回收,相信很多人和我一样,第一反应和面试题一样。如果你还没有背过一些GC算法和收集器的知识,出门在外都不敢说自己。八条腿的作文我已经背下来了。说起来有点尴尬,这些知识真正用到工作中的场景不多,学起来也比较枯燥,但是面试官就是爱问,我们能做什么?既然卷成这样了,不学也没办法,Hydra牺牲周末时间画了几张动图给大家看。希望通过这些图片,可以帮助大家更好的理解垃圾回收算法。废话不多说,首先我们从基本问题入手,看看如何判断一个对象是否应该被回收。判断对象的生存垃圾回收的根本目的是使用一些算法来管理内存,从而有效地利用内存空间。在进行垃圾回收之前,需要对对象的存活情况进行判断。jvm中判断对象存活的算法有两种。单独介绍。1.引用计数算法给对象增加一个引用计数器。每当有对它的引用时,计数器加1,当引用无效时计数器减1。当计数器为0时,表示当前对象可以回收。这种方法的原理很简单,判断起来也很高效,但是有两个问题:每次引用堆中的对象和清除引用,都需要进行计数器的加减运算,这会带来性能损失。当两个对象互相引用时计数器永远不会为0。也就是说,即使这两个对象不再被程序使用,仍然没有办法被回收。通过下面的例子来看看循环引用的计数问题:publicvoidreference(){Aa=newA();Bb=newB();a.instance=b;b.instance=a;}引用计数的变化过程如下图所示:可以看出,方法执行完成后,释放了栈中的引用,但是两个对象在堆内存中留下了循环引用,导致这两个实例最终的引用计数都不为0,最终这两个对象的内存不会被释放,也正是因为这个缺陷,引用计数算法还没有实际应用在gc的过程中。2.可达性分析算法可达性分析算法是jvm默认使用的垃圾查找算法。需要注意的是,虽然说的是寻找垃圾,但可达性分析算法实际上是寻找还活着的对象。至于之所以这样设计,是因为如果直接去寻找未引用的垃圾对象,实现起来比较复杂,比较耗时。反过来,这将节省标记幸存对象的时间。可达性分析算法的基本思想是以一系列称为GCRoots的对象为起点,从这些节点开始向下搜索。搜索所经过的路径称为引用链。当一个对象到达GCRoots而没有连接任何引用链时,就证明该对象已经不存在了,可以作为垃圾进行回收。在java中,可以作为GCRoots的对象有以下几种:虚拟机栈中引用的对象(栈帧的局部变量表)方法区中静态属性引用的对象方法区中常量引用的对象是局部的方法栈中JNI(nativemethod)引用的对象jvm的内部引用,如基本数据类型对应的Class对象,一些常驻异常对象等,以及系统类加载器同步的系统类加载器持有的对象引用synchronizationlock反映了jvmJMXBean的内部情况,注册在JVMTI中的callbacklocalcodecache等。此外,还有一些临时的GCRoots。这是因为垃圾回收多采用分代回收和部分回收。在考虑跨代或跨地域引用的对象时,需要将这部分关联对象添加到GCRoots中,以保证准确性。其中比较重要,提到较多的是前四个,其他的可以简单理解。了解了jvm是如何寻找垃圾对象的,我们再来看看不同垃圾回收算法的执行过程。垃圾收集算法1.标记-清除算法标记-清除算法是一种非常基础的垃圾收集算法。当堆中的有效内存空间耗尽时,会触发STW(stoptheworld),然后分为mark和clear两个阶段进行垃圾回收工作:Mark:从GCRoots的节点开始扫描,markallsurvivingobjects,并记录为reachableobjects清除:扫描整个堆内存空间,如果发现有未标记为Reachable对象的对象,则通过下图对其进行回收,简单看下两阶段执行过程:但是这个算法会带来几个问题:GC的时候会产生STW,让整个应用停止,导致用户体验不好,标记和清除两个阶段的效率比较低。标记阶段需要从根集合开始扫描,清除阶段需要遍历堆中的所有对象,只处理非存活对象。清除后会产生大量的不连续点。内存碎片。结果,当程序在运行时需要分配更大的对象时,找不到足够的连续内存,就会触发新的垃圾回收动作。另外,jvm并没有真正遍历垃圾对象,而是将内部数据全部删除。而是保存垃圾对象的首地址和末地址,再次分配内存时,直接在地址列表中分配。该措施提高了一些标记和清除算法的效率。2.复制算法复制算法主要用于新生代。它将内存分成大小相同的两块,一次只使用其中一块。在任何时间点,所有动态分配的对象只能分配在其中一个内存空间中,而另一个内存空间是空闲的。复制算法可以分为两步:当其中一个内存的有效内存空间耗尽时,jvm会停止应用程序,启动复制算法的gc线程,将存活的对象复制到另一个空闲内存空间.复制的对象会严格按照内存地址排列。同时gc线程会更新存活对象的内存引用地址指向新的内存地址。内存空间和空闲内存空间是颠倒的,这样每次回收内存,就回收一半的内存空间。我们通过下图来看一下复制算法的执行过程:复制算法的优势在于弥补了标记清除算法,会有内存碎片的坏处,但它也有一些问题:只使用了一半的内存,所以内存利用率低,造成浪费。如果对象的存活率很高,那么很多对象需要重新复制。并更新他们的申请地址,这个过程会花费很长时间。从上面的缺点我们可以看出,如果需要使用复制算法,有一个前提,就是要求对象的存活率比较低。因此,复制算法更多地用在对象“生死存亡”发生较多的新生代。3.Mark-collat??ion算法mark-compact算法与mark-clear算法非常相似,主要用于老年代。分为以下两个步骤:标记:和标记清除算法一样,先标记对象,通过GCRoots节点扫描存活对象进行标记整理:将所有存活对象移动到一端的空闲空间,按照内存地址的顺序排序,并更新它们对应于被引用的指针,然后清理除结束内存地址以外的所有内存空间缺点:与mark-and-clear算法相比,弥补了内存空间碎片化的缺点。与复制算法相比,弥补了浪费一半内存空间的缺点。然而,标记清除算法也有其缺点。一方面需要标记所有存活的对象,另一方面又增加了移动对象和更新引用地址的操作,因此标记算法的使用成本较高。4.分代收集算法事实上,java中的垃圾收集器并不是唯一使用的垃圾收集算法,目前大多数使用的是分代收集算法。一般jvm会根据对象的生命周期将内存分成若干块。一般它将堆内存划分为新生代和老年代,然后根据各个年龄段的特点选择最佳的垃圾回收算法。主要思路如下:在新生代中,每次收集都会有大量对象死亡,所以可以选择复制算法。您只需要复制少量对象并更改引用即可完成垃圾回收。在老年代,对象的存活率比较高。使用复制算法并不能很好的提高性能和效率。另外,没有额外的空间来保证它的分配,所以选择一种标记清除或标记清除算法来进行垃圾收集。下面通过图来简单了解下各种算法的主要应用领域:至于为什么在某个领域选择某个算法,还是与三种算法的特点息息相关,再从三个方面进行比较dimensions:执行效率:从算法的时间复杂度来看,复制算法最好,其次是标记清除,标记排序内存利用率最低:标记排序算法和标记清除算法较高,且thecopyalgorithmismosttinessMemorytiness:复制算法和标记清除算法比较整洁,标记清除算法最差。当一个线程开始工作时,STW需要挂起所有工作线程。总结在这篇文章中,我们首先介绍了垃圾回收的基本问题。什么样的物品可以作为垃圾回收?jvm中的可达性分析算法解决了这个关键问题,衍生出多种常用的垃圾回收算法,不同的算法各有优缺点,根据各自的特点应用在各个时代。虽然这篇文章唠叨了这么多,但这些还是基础知识。想要彻底掌握jvm中的垃圾回收,需要了解很多垃圾回收器、内存分配等知识,不过我们今天的介绍也就这些了。我希望这个插图可以帮助您更好地理解垃圾收集算法。本文转载自微信公众号“码农仓上”,可通过以下二维码关注。转载本文请联系码农公众号。