垃圾收集算法
分代收集理论
当前JVM的GC,大多遵循分代收集理论进行设计,分代收集实质为一套符合大多数程序实际运行情况的经验法则,建立在两个分代假说上:
- 弱分代假说(Weak Generational Hypothesis):绝大多数对象都是朝生夕灭的。
- 强分代假说(Strong Generational Hypothesis):熬过越多次垃圾收集过程的对象就越难以消亡。
根据这两个假说,收集器将Java堆划分出不同的区域,然后将回收对象依据其年龄(即经历过垃圾收集的次数)分配到不同的区域之中存储。根据不同的区域,设定不同的回收算法和回收频率,这样子便同时兼顾了GC的时间开销和内存的有效利用。在Java堆划分出后不同的区域后,GC才可以每次仅回收某一个或者某一部分区域,因而产生了"Minor GC" "Full GC"等回收类型的划分。也才可以根据不同的区域安排与其特征相匹配的垃圾收集算法,因而产生了标记-清除,标记-复制,标记-整理等针对性垃圾收集算法。
但分代收集并非只是简单划分一下内存区域那么容易,它至少存在一个明显的困难:对象不是孤立的,对象之间会存在跨代引用。如现在进行一次只局限于新生代区域内的收集(Minor GC),但新生代中的对象被老年代所引用时,为了找出该区域的存活对象,在该区域进行固定GC Root之后,还需要对老年代进行遍历来确保可达性分析的正确性。理论上可行,但也会带来很大的性能负担。由此,便有了分代收集的第三条经验法:
- 跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引用来说仅占极少数。
依据这条假说,我们就不应再为了少量的跨代引用去扫描整个老年代,也不必浪费空间专门记录每一个对象是否存在及存在哪些跨代引用,只需在新生代上建立一个全局的数据结构(该结构被称为“记忆集”,Remembered Set),这个结构把老年代划分成若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。
虽然这种方法需要在对象改变引用关系(如将自己或者某个属性赋值)时维护记录数据的正确性,会增加一些运行时的开销,但比起收集时扫描整个老年代来说仍然是划算的。
常见收集类型如下:
- 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
- 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。
- 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集,目前只有G1收集器会有这种行为。
- 整堆收集(Full GC):收集整个Java堆和方法区的垃圾收集。
标记-清除算法
标记-清除算法分为"标记"和"清除"两个阶段,首先标记出需要回收的对象,在标记完成后,统一回收掉所有标记的对象。这种算法主要有两个缺点:一是执行效率不稳定,当堆中含有大量对象且都是需要回收的时候,会进行大量标记和清除的动作;二是内存空间的碎片化,标记清楚后会产生大量不连续的内存碎片。碎片化严重后,在程序运行中需要分配较大对象时无法找到足够的连续内存时,不得不提前触发另一次的GC。这种算法较为适合于存活对象较多的老年代,执行过程如下图:
标记-复制算法
标记复制将内存划分为大小两个区域A/B,A区域用于对象分配,B区域上不分配对象,仅在GC后,将A区域上还存活的对象一一复制到B区域中。在大部分对象都是可回收的情况,仅需复制小部分对象,有很好的性能,并且解决了空间碎片化的问题。但是在大部分对象都是不可回收时,就会产生两个问题,一是需要复制大量对象,效率低下;二是B区域的空间不足,一般解决方案为依赖与其他内存区域,如老年代。这种算法适合于GC时存活对象不多的新生代,但在执行过程会停止用户线程执行。
标记-整理算法
标记-整理算法为在进行标记后,将所有存活对象都像内存空间的一端移动,然后直接清理掉边界之外的内存。这种方式可以避免空间碎片化的产生,但对象移动操作必须全程暂停用户应用程序才能进行(比较新的ZGC收集器可实现与用户程序的并发执行)。
有一种组合搭配,不在内存分配和访问上增加太大额外负担,做法是让虚拟机平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在,直到内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一次,以获得规整的内存空间。基于标记-清除算法的CMS收集器面临空间碎片过多时采用的就是这种处理办法。这种算法较为适合于存活对象较少的新生代,但在执行过程会停止用户线程执行。
判定对象是否存活算法
引用计数
引用计数算法即在对象之中添加一个引用计数器,当有一个地方引用它时,计数器加一,当引用失效时,计数器减一,当计数器为0时,此对象就是可以收回的。这种方式,逻辑简单,效率也高,是一种非常不错的算法。但主流JVM中却均未使用这种方式,原因是这种简单的算法有许多例外情况需要考虑,必须配合大量额外处理才能正常工作,如单纯的引用计数就很难解决对象之间的相互循环引用问题。
可达性分析
当前主流的JVM的内存管理,都是通过可达性分析(Reachability Analysis)算法来判定对象是否存活的。可达性分析算法即通过一系列的"GC ROOTS"根对象作为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为"引用链",如果某对象到GC Roots之间没有引用链,则此对象是可被回收的。在JVM中,固定的GC Roots包括以下对象:
- 在虚拟机栈中引用的对象
- 在本地方法栈中引用的对象
- 在方法区中类静态属性所引用的对象
- 在方法区中常量所引用的对象
- JVM 中的常驻对象,如异常、类加载器等
- 被同步锁(synchronized)持有的对象
- 反映JVM内部情况的JMXBean、JVMTI中注册的回调、本地代码缓存等
对象引用
无论是引用计数还是可达性分析,判断对象是否存活都需要引用。
在JDK1.2 之后,对象引用概念有了扩充,将引用分为了强引用、软引用、弱引用和虚拟引用,这四种引用强度依次减弱。
- 强引用是指程序中的引用赋值,只要引用关系存在,GC就永远不会回收被引用的对象。
- 软引用用来描述一些有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生OOM前,会把这些对象回收。在回收之后如果还没有足够内存,才会抛出OOM。实现类为SoftReference。
- 弱引用用来描述非必须对象,被弱引用关联的对象只能生存到下一次垃圾收集为止。实现类为WeakReference,如JDK动态代理对象。
- 虚引用,一个对象是否存在虚引用,完全不会对其生存时间构成影响,也无法通过虚引用来的取得一个对象实例。为对象设置虚引用的目的在于这个对象被GC回收时收到一个系统通知。实现类为PhantomReference。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 一个奇形怪状的面试题:Bean中的CHM要不要加volatile?
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)