您的位置:首页 > 科技 > IT业 > 【JVM】垃圾回收算法(一)

【JVM】垃圾回收算法(一)

2024/11/17 16:43:22 来源:https://blog.csdn.net/Cover_sky/article/details/141596909  浏览:    关键词:【JVM】垃圾回收算法(一)

垃圾回收算法

Java程序在运行过程中会产生大量的对象,但是内存大小是有限的,如果光用而不释放,那内存迟早被耗尽。如C/C++程序,需要程序员手动释放内存,Java则不需要,是由垃圾回收期去自动回收。垃圾回收器回收内存至少需要做两件事情:标记垃圾、回收垃圾。于是诞生了很多算法

垃圾判断算法之引用计数算法

在这里插入图片描述

最简单的垃圾判断算法。在对象中添加一个属性用于标记对象被应用的次数,每多一个其他对象引用,计数+1,当引用失效时,计数-1,如果计数=0,表示没有其他对象引用,就可以被回收。这个算法无法解决循环依赖的问题。像Redis中就使用了这样的算法,Netty中的ByteBuffer也是如此,Python中。
在该算法中,没有其他对应引用A对象和B对象,但是AB对象之间存在着互相引用,以致于垃圾收集器无法回收

垃圾判断算法之可达性分析算法

在这里插入图片描述

通过一系列被称为"GC Roots"的根对象作为其实节点集,从这些节点开始,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连(用图论的话来说,就是从GC Roots到这个对象不可达)时,则证明此对象时不可用的。如图所示,绿色对象虽然互相有关联,但是他们到
GC Roots是不可达的,所以他们将会被判断为是可回收的对象

哪些对象可以作为GC Roots呢?在Java语言中,可作为GC Roots的对象包括下面几种:

  • 1.虚拟机栈(栈帧中的本地变量表)中引用的对象

  • 2.方法区中类静态属性引用的对象

  • 3.方法区中常量引用的对象

  • 4.本地方法栈中JNI(即一般说的Native方法)引用的对象

  • 1.所有Java线程当前活跃的栈帧里指向GC堆里的对象的引用;换句话说,当前所有正在被调用的方法的引用类型的参数/局部变量/临时值

  • 2.VM的一些静态数据结构里指向GC堆里的对象的引用,例如说Hotspot VM里的Universe里由很多这样的引用

  • 3.JNI handles,包括global handles和local handles

  • 4.所有当前被加载的Java类

  • 5.Java类的引用类型静态常量

  • 6.Java类的运行时常量池里的引用类型常量(String或Class类型)

  • 7.String常量池(StringTable)里的引用

内存池

在这里插入图片描述

Memory Pool
内存池,如果拿运行时数据区域类比的话 => 它就是JVM内存模型管理器
Memory Chunk
内存块 => 堆 方法区
Memory Cell
细胞 => 一个对象对应多个Cell
为什么要有内存池?
对象的频繁操作会涉及到用户态和内核态的切换。核心:避免频繁调用操作系统API去向操作系统分配内存、释放内存
技术是没有绝对的,只有优点的技术。解决了一些问题,又诞生了另一些问题。

在OS中,随着我们打开的进程越来越多,内存空间也变得越来越紧张,对于已经打开的内存,OS系统是不会回收的,那么OS是怎么做的呢?OS会进行类似LRU的操作,把不经常用的内存导入到硬盘空间,swap空间,但也不会无限导入,超过了swap空间,就会触发OS的OOM Killer机制。那么如果进程突然切换了,会触发缺页异常,如果在硬盘上,就导入到物理内存中,这也就是垃圾收集器的诞生背景,因为这块内存是JVM自己控制的,所以操作系统没法帮我们做,思维要严谨

MAC长时间不用,硬盘空间占用会非常多,把内存置换到物理硬盘上

垃圾回收算法

由于JVM要对自己管理的对象进行回收,于是就诞生了不同的垃圾回收算法.

标记-清除算法

在这里插入图片描述

最基础的收集算法是"标记-清除"(Mark-Sweep)算法,如同它的名字一样,算法分为"标记"和"清楚"两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象,之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其不足进行改进而得到的。
它的主要不足有两个:

  • 1.效率问题,标记和清楚两个过程的效率都不高;
  • 2.空间问题,标记清楚之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作

复制算法

在这里插入图片描述

为了解决效率问题,一种称为"复制"(Copying)的收集算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一般,未免太高了了一点。
现代的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代的对象98%是"朝生夕死"的,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor.最后清理掉Eden和刚才用过的Survivor空间。HoSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被浪费.当然98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion).
内存的分配担保机制就好比我们去银行借款,如果我们信誉很好,在98%的情况下东鞥按时偿还,于是银行可能会默认我们下一次也能按时按量地偿还贷款,只需要有一个担保人能保证如果我不能还款时,可以从他的账户扣钱,那银行就认为没有风险了。内存的分配担保也一样,如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代

标记-整理算法

在这里插入图片描述

复制收集算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的时,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。因为没有多余的内存区域为老年代做担保,即便有,仍然需要有一块区域考虑到不能担保的情况。
根据老年代的特点,有人提出了另外一种"标记-整理"(Mark-Compact)算法,标记过程仍然与"标记-清楚"算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界意外的内存。

缺点:内存整理是CPU密集型的,比较耗费CPU

内存整理涉及到(排序+合并),那么需要遍历碎片。

  • 1.在合并的时候排序
  • 2.先排序再合并

碎片会存在哪些情况

  • 1.有序
  • 2.无序

思路:做两次遍历(jvm也是这样做的)
向前排序一次,比我小的内存地址往前移动
向后排序一次,比我大的内存地址往后移动

移动的对象为什么还可以访问?

在这里插入图片描述

因为对象的引用不是写死的,而是动态计算出来的。

  • 1.静态存储 写死的,那么每轮GC都需要依赖中间数据结构来存储新地址,工作量会比较大
  • 2.动态计算, 动态地址是怎么计算的?

计算规则:

  • 1.找到内存块的起始位置 + 数据块的起始地址 * 8字节对齐(对应的Hotspot源码中是HeapWord这个结构)
    get_data() + get_start() * get_align_size()

eg:如图所示:
0 + 3 * 8 = 24

分代收集算法

当前商业虚拟机的垃圾收集都采用"分代收集"(Generational Collection)算法。这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法,只需要付出少量存活对象的复制成本就可以完成收集。而老年代中因为对象存活率高、没有额外对他进行分配担保,就必须使用"标记-清除"或"标记-整理"算法来进行回收

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com