垃圾收集算法-cnblog

发布时间 2023-12-24 23:24:56作者: xyfyy

垃圾收集算法

今天刚学习了一下垃圾回收中如何判断一个对象是否应该回收,当判断结束,很自然的下一个好奇点就在于,如何去将一个对象的空间进行回收?
在之前看一些java虚拟机的概念中,往往会看到分代收集的思想,直到今天才对其有一个简单的认识。

分代收集简单来说就是将java对象分成了“容易变成垃圾的对象”和“不容易变成垃圾的对象”两大类。分代收集中,有两个重要的经验法则:

弱分代假说:绝大多数对象都是朝生夕灭的(ps:容易变成垃圾)

强分代假说:熬过越多次垃圾收集过程中的对象越难以消亡(显然易见)

依据这两个法则,很自然会想到将对象按照其“存活年龄“(熬过垃圾回收的次数)分配到不同的区域中存储,这样可以在这两个区域用不同的垃圾收集方法来提高效率。

正如《深入理解java虚拟机》一书中这句话:

如果一个区域中大多数对象都是朝生夕灭,难以熬过垃圾收集过程的话,那么把它们集中放在一起,每次回收时只关注如 何保留少量存活而不是去标记那些大量将要被回收的对象,就能以较低代价回收到大量 的空间;如果剩下的都是难以消亡的对象,那把它们集中放在一块,虚拟机便可以使用 较低的频率来回收这个区域,这就同时兼顾了垃圾收集的时间开销和内存的空间有效利用。

这两块区域,就是新生代(Young Generation)和老年代(Old Generation)。下面根据分代理论,引入几个名词:

■ 新生代收集(Minor GC/Young GC):指目标只是新生代的垃圾收集。
■ 老年代收集(Major GC/Old GC):指目标只是老年代的垃圾收集。目前只有 CMS 收集器会有单独收集老年代的
行为。
■ 混合收集(Mixed GC):指目标是收集整个新生代以及部分老年代的垃圾收集。目前只有 G1 收集器会有这种行
为。
■ 整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾收集。

当引入这两个区域后,有一个问题就出现了,如果新生代中的对象被老年代中的对象引用了,那么就不能仅仅只针对这两个区域分别进行垃圾回收了(如果分别回收,可能存在新生代中对象被老年代中对象引用,但是检测不到,从而将对象回收,导致内存访问异常)。为了解决这个问题,看来就必须将整个老年代中的对象放入GC Roots当中,但这样遍历会为内存回收带来很大的负担。此时,第三条经验法则被人们所发现:

跨代引用假说(Intergenerational Reference Hypothesis):跨代引用相对于同代引 用来说仅占极少数。

依据此经验法则,就不应再为了少量的跨代引用来扫描整个老年代,增加负担。只需要引入一个数据结构,Rememberd Set(记忆集),其会标识出老年代中的哪一块内存会存在跨代引用(具体如何标识,等待后续学习~)。

当我们引入一个新的数据结构去记忆,势必会增加维护这个数据结构正确性的开销,但是这个开销相比于遍历老年代来说是划算的。


分代理论说明白之后,那么具体该怎么对内存空间进行释放呢?

第一种方案就是标记-清除算法

image-20231224225704563

从上图可以清晰看到,可回收对象在回收后,变成未使用的内存空间,存活对象依旧在原来的位置。联想到操作系统中内存管理的动态分区分配(顺带复习一下操作系统hhh),随着分配进行,会导致内存中出现小的内存块。这种方案也是一样,标记、清除之后会产生大量不连续的内存碎片,空间碎 片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内 存而不得不提前触发另一次垃圾收集动作。另一个问题就是,如果java堆中包含大量对象,且大部分需要回收,会进行大量的标记、清除操作,其效率较低。

第二种方案:标记-复制算法

image-20231224230515570

这种算法,思想上就是用整个内存空间的一半作为缓冲。当一半内存使用完后,将存活的对象拷贝到另一半上,由于第一条经验法则(大多数对象都是朝生夕灭),只需要拷贝少量对象即可。显然这样拷贝也不会存在空间碎片的问题。但是问题就是,浪费了太多的空间了..(以空间换时间吧)现在的商用 Java 虚拟机大多都优先采用了这种收集算法去回收新生代。老年代显然不适合,因为老年代中大部分对象不会变成”垃圾“,会导致大量的拷贝,并不划算。

为了缓解大量空间浪费,根据IBM 公司一项专门研究对新生代“朝生夕灭”的特点做了更量化的诠释——新生代中的对象有 98%熬不过第一轮收集。因此并不需要按照 1∶1 的比例来划分新生代的内存空间。自然而然,就可以减少那块缓冲的空间(可以只用10%、20%等等)。具体可以看《深入理解java虚拟机》中的描述,有一种Appel回收就是采用这种方法。

第三种算法:标记-整理算法

image-20231224231235952

这种算法也很好理解,就是在每次回收时,将存活对象进行一次拷贝,移动到前面,将可回收的对象的内存空间直接占据。显然如果移动存活对象,尤其是在老年代这种每次回收都有大量对象存活区域,移动存活对象并更新所有引用这些对象的地方将会是一种极为负重的操作,而且这种对象移动 操作必须全程暂停用户应用程序才能进行。其有点就是没有了内存碎片,解决了空间碎片化的问题。如果不采用这种方案,解决空间碎片化只能依赖更为复杂的内存分配器和内存访问器来解决。假如在这个环节上增加 了额外的负担,势必会直接影响应用程序的吞吐量。因此这也需要进行权衡。还有一种方案,就是一种均衡方案,让虚拟机平时多数时间都采用标记-清除算法,暂时容忍内存碎片的存在, 直到内存空间的碎片化程度已经大到影响对象分配时,再采用标记-整理算法收集一 次,以获得规整的内存空间。

总结

通过这些学习,简单了解到了内存回收的一些方案,感觉和操作系统的内存管理思想有一些相似之处,可能整个计算机的管理思想都是类似,没有任何一种方案是完美的,需要根据实际情况来考虑。加油!