GC垃圾收集

1. 分代收集理论

当前的商业虚拟机收集器大部分都使用了分代收集的理论进行设计。分代收集名为理论,其实是一套大部分程序实际运行情况都符合的经验法则,它建立在两则假说之上。

1)弱分代假说:绝大部分对象都是朝生夕灭的。
2)强分代假说:熬过越多次数的垃圾收集过程的对象就越难以消亡。

这两个假说共同奠定了多款垃圾回收期的一直设计原则:收集器将java堆划分出不同的区域。然后根据对象的年龄来分配到不同的区域之中储存。如果一个区域内大部分对象都是朝生夕灭的,那么他们就集中在一起储存,每次收集时只关注怎么保证少量存活而不是挨个去标记是否应该回收,就能降低回收空间的代价;如果剩下的都是难以消亡的对象,那么他们集中在一起,收集器便可以使用较低的频率来回收这块内存。这就同事兼顾了垃圾手机的时间开销和内存空间有效利用。

把年龄分代理论放到现在商用java虚拟机中,设计者一般至少会吧java划分为新生代,和老年代两个区域。顾名思义,新生代中垃圾回收期每次收集都会发现有大批对象死去,而每次回收后存活的少量对象将会晋升到老年代中存放。但其中不只是仅仅划分内存的过程,其中还存在很多问题:在分代储存的情况下,不同代之间的对象的跨代引用。

加入现在需要对新生代的对象进行垃圾收集,但是新生代的对象完全有可能被老年代的对象引用着。这就不得不除了GC root之外再去遍历整个老年代。反之亦是如此。基于这种问题,就需要对年龄分代收集理论添加第三条经验法则。

3) 跨代引用假说:跨带引用的情况在通代引用的情况之中只有极少数。

其实第三条完全是前两条假说推论而来的隐含结论。因为对象互相引用的情况几乎都是倾向于同时生存或者消亡的。试想一下,当我们新生代存在跨代引用的时候。引用它的老年代是难以回收的,那么导致这个新生代中的对象因为被引用而不会被回收,进而使该对象从新生代晋升到了老年代,这时,跨带引用的问题便被消除了。

依据这条假说,设计垃圾收集器就不需要扫描整个老年代,只需要在新生代创建一个全局数据结构(称之为“记忆集”)。依照这个数据结构,把老年代划分为多个区域,标识出那一块老年代是存在跨代引用的。此时当发生YoungGC,只需要将这块指定的内存加入GCroot进行扫描就可以了。

部分收集( Partial GC) : 指目标不是完整收集整个Java堆的垃圾收集, 其中又分为:

1.新生代收集( Minor GC/Young GC) : 指目标只是新生代的垃圾收集。
2.老年代收集( Major GC/Old GC) : 指目标只是老年代的垃圾收集。 目前只有CMS收集器会有单
独收集老年代的行为。 另外请注意“Major GC”这个说法现在有点混淆, 在不同资料上常有不同所指,
读者需按上下文区分到底是指老年代的收集还是整堆收集。
混合收集( Mixed GC) : 指目标是收集整个新生代以及部分老年代的垃圾收集。 目前只有G1收
集器会有这种行为。
3.整堆收集( Full GC) : 收集整个Java堆和方法区的垃圾收集

2. 标记-清除算法

最早出现的,也是最基础的清除算法便是“标记-清除”算法。标记算法顾名思义,将所有需要清除的对象进行标记,当所有的对象标记结束后,回收所有具有标记的对象。

之所以说它是最基础的回收算法,是因为后续的收集算法大部分都是以此为基础的,对其的缺点进行改善而得到的。其主要的两个缺点:1. 当存在大量的需要回收的对象的时候,需要做大量的标记和回收。导致其效率随着对象的增多而变得很低;2.当标记再进行回收之后,内存中出现了大范围的碎片内存,导致当再需要分配一块空间给一个所需内存较大的对象的时候会导致没有足够大内存分配。算法执行过程如下图

在这里插入图片描述

3. 标记-复制算法

面对标记-清除算法在面临着大量需要回收的对象导致的性能问题。1969年Fenichel提出了一种称为“半区复制”(Semispace Copying) 的垃圾收集算法。其思想是将内存分为两块,其中一块存储对象,当一块内存将要用完了,就将还存活的对象复制到另一块内存上,然后将使用过的内存清除掉。如果内存中的大部分对象是存活的时候,使用这种方法,会造成在复制上很大的开销,但对于大部分对象都是需要回收的情况下,算法则是复制的是很少一部分。而每次分配内存也不需要考虑碎片的内存,只需要推动指针,按顺序分配就行了。代价则是真正储存使用的内存为原来的一半。空间浪费多了一些。
在这里插入图片描述
现在的商用java虚拟机多数优先采用的就是这种算法去回收新生代,IBM公司。曾有一项专门研究对新生代“朝生夕灭” 的特点做了更量化的诠释,在新生代中对象有98%是熬不过第一轮的收集的。因此,并不需要按照1:1的比例来分配内存。

3. 标记-整理算法

标记-复制算法在大量的对象存活的情况下复制效率会大大降低。给你个关键的是,如果不想按照浪费50%的内存就需要 额外的空间担保,以应对对象存活率为100%的极端情况。所以,标记-复制方法并不直接用于老年代。

针对老年代的存亡特性,1974年Edward Lueders提出了另外一种有针对性的“标记-整理”(Mark-Compact) 算法,标记过程都虽然与标记-清除算法一样。但是清除处理并不是将对象直接进行清除。而是将存活的对象向内存的一端移动,然后将边界之外的内存清除掉。

在这里插入图片描述

但如果跟标记-清除算法那样完全不考虑移动和整理存活对象的话, 弥散于堆中的存活对象导致的空间碎片化问题就只能依赖更为复杂的内存分配器和内存访问器来解决。 譬如通过“分区空闲分配链表”来解决内存分配问题(计算机硬盘存储大文件就不要求物理连续的磁盘空间, 能够在碎片化的硬盘上存储和访问就是通过硬盘分区表实现的) 。 内存的访问是用户程序最频繁的操作, 甚至都没有之一, 假如在这个环节上增加了额外的负担, 势必会直接影响应用程序的吞吐量。

基于以上两点, 是否移动对象都存在弊端, 移动则内存回收时会更复杂, 不移动则内存分配时会更复杂。 从垃圾收集的停顿时间来看, 不移动对象停顿时间会更短, 甚至可以不需要停顿, 但是从整个程序的吞吐量来看, 移动对象会更划算。 此语境中, 吞吐量的实质是赋值器(Mutator, 可以理解为使用垃圾收集的用户程序, 本书为便于理解, 多数地方用“用户程序”或“用户线程”代替) 与收集器的效率总和。 即使不移动对象会使得收集器的效率提升一些, 但因内存分配和访问相比垃圾收集频率要高得多, 这部分的耗时增加, 总吞吐量仍然是下降的。 HotSpot虚拟机里面关注吞吐量的ParallelScavenge收集器是基于标记-整理算法的, 而关注延迟的CMS收集器则是基于标记-清除算法的, 这也从侧面印证这点。

另外, 还有一种“和稀泥式”解决方案可以不在内存分配和访问上增加太大额外负担, 做法是让虚拟机平时多数时间都采用标记-清除算法, 暂时容忍内存碎片的存在, 直到内存空间的碎片化程度已经大到影响对象分配时, 再采用标记-整理算法收集一次, 以获得规整的内存空间。 前面提到的基于标记-清除算法的CMS收集器面临空间碎片过多时采用的就是这种处理办法。


《深入理解java虚拟机》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值