jvm-垃圾回收(垃圾收集算法)


当成功区分出内存中存活对象和死亡对象后,GC接下来的任务就是执⾏垃圾回收,释放掉⽆⽤对象所占⽤的内存空间,以便有⾜够的可⽤内存空间为新对象分配内存。

⽬前在JVM中⽐较常⻅的三种垃圾回收算法是标记-清除算法(Mark-Sweep)、复制算法(Copying)、标记-压缩算法(Mark-Compact)。

标记清除算法

标记-清除算法(Mark-Sweep)是⼀种⾮常基础和常⻅的垃圾收集算法,该算法被J.McCarthy等⼈在1960年提出并应⽤于Lisp语⾔。

清除算法分为标记和清除两个阶段。该算法⾸先从根集合进⾏扫描,对存活的对象标记,标记完毕后,再扫描整个空间中未被标记的对象并进⾏回收,如下图所示。

  • 标记:GC从引⽤根节点开始遍历,标记所有被引⽤的对象。⼀般是在对象的Header中记录。
  • 清除:GC对堆内存从头到尾进⾏线性的遍历,如果发现某个对象q其Header中没有标记为可达对
    象,则将其回收。
    在这里插入图片描述
标记-清除算法的主要不⾜有
  • 效率问题:标记和清除两个过程的效率都不⾼;
  • 在进⾏GC的时候,需停⽌整个应⽤程序,导致⽤户体验性差;
  • 空间问题:标记-清除算法不需要进⾏对象的移动,并且仅对不存活的对象进⾏处理,因此标记清
    除之后会产⽣⼤ᰁ不连续的内存碎⽚,空间碎⽚太多可能会导致以后在程序运⾏过程中需要分配较
    ⼤对象时,⽆法找到⾜够的连续内存⽽不得不提前触发另⼀次垃圾收集动作。
    在这里插入图片描述

复制算法

为了解决标记-清除算法在垃圾收集效率⽅⾯的缺陷, M. L. Minsky 于 1963 年发表了著名的论⽂“⼀种使⽤双存储区的 Lisp 语⾔垃圾收集器( A LISP Garbage Collector Algorithm Using SerialSecondary Storage )”。 M. L. Minsky 在该论⽂中描述的算法被⼈们称为复制算法(Copying),它也被M. L. Minsky 本⼈成功地引⼊到了 Lisp 语⾔的⼀个实现版本中。

复制算法别出⼼裁地将堆空间⼀分为⼆,并使⽤简单的复制操作来完成垃圾收集⼯作,这个思路相当有趣。

复制算法将可⽤内存按容ᰁ划分为⼤⼩相等的两块,每次只使⽤其中的⼀块。当这⼀块的内存⽤完了,就将还存活着的对象复制到另外⼀块上⾯,然后再把已使⽤过的内存空间⼀次清理掉。这种算法适⽤于对象存活率低的场景,⽐如新⽣代。这样使得每次都是对整个半区进⾏内存回收,内存分配时也就不⽤考虑内存碎⽚等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运⾏⾼效。该算
法示意图如下所示:
在这里插入图片描述

应⽤场景

事实上,现在商⽤的虚拟机都采⽤这种算法来回收新⽣代。因为研究发现,新⽣代中的对象每次回收都基本上只有10%左右的对象存活,所以需要复制的对象很少,效率还不错。不适合存活ᰁ对象⽐较多的场景。

优点
  • 没有标记和清除过程,实现简单,运⾏⾼效
  • 复制过去以后保证空间的连续性,不会出现"碎⽚" 问题。
缺点
  • 此算法的缺点也是很明显,就是需要两倍的内存空间
  • 如果对象的存活率很⾼,我们可以极端⼀点,假设是100%存活,那么我们需要将所有对象都复制⼀遍,并将所有引⽤地址᯿置⼀遍。复制这⼀⼯作所花费的时间,在对象存活率达到⼀定程度时,将会变的不可忽视。

标记整理算法

复制收集算法在对象存活率较⾼时就要进⾏较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,以应对被使⽤的内存中所有对象都100%存活的极端情况,所以在⽼年代⼀般不能直接选⽤这种算法。

标记-整理算法或标记-压缩算法(Mark-Compact)是标记-清除算法和复制算法的有机结合。把标记-清除算法在内存占⽤上的优点和复制算法在执⾏效率上的特⻓综合起来,这是所有⼈都希望看到的结果。不过,两种垃圾收集算法的整合并不像 1 加 1 等于 2 那样简单,我们必须引⼊⼀些全新的思路。1970 年前后, G. L. Steele , C. J. Cheney 和 D. S. Wise 等研究者陆续找到了正确的⽅向,标记-整理算法的轮廓也逐渐清晰了起来。

标记-整理算法的标记过程类似标记清除算法,但后续步骤不是直接对可回收对象进⾏清理,⽽是让所有存活的对象都向⼀端移动,然后直接清理掉端边界以外的内存,类似于磁盘整理的过程,该垃圾回收算法适⽤于对象存活率⾼的场景(⽼年代),其作⽤原理如下图所示。

在这里插入图片描述
标记-整理算法与标记-清除算法最显著的区别是:标记-清除算法不进⾏对象的移动,并且仅对不存
活的对象进⾏处理;⽽标记整理算法会将所有的存活对象移动到⼀端,并对不存活对象进⾏处理,因此
其不会产⽣内存碎⽚。标记-整理算法的作⽤示意图如下:
在这里插入图片描述
标记-整理算法的最终效果等同于标记-清除算法执⾏完成后,再进⾏⼀次内存碎⽚整理,因此,也可以把它称为标记-清除-压缩(Mark-Sweep-Compact)算法。

⼆者的本质差异在于标记-清除算法是⼀种⾮移动式的回收算法,标记-压缩是移动式的。是否移动回收后的存活对象是⼀项优缺点并存的⻛险决策。

可以看到,标记的存活对象将会被整理,按照内存地址依次排列,⽽未被标记的内存会被清理掉。如此⼀来,当我们需要给新对象分配内存时,JVM只需要持有⼀个内存的起始地址即可,这⽐维护⼀个空闲列表显然少了许多开销。

优点
  • 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,JVM只需要持有⼀个内存的起始地址即可。
  • 消除了复制算法当中,内存两倍的⾼额代价。
缺点
  • 从效率上来说,标记-整理算法要低于复制算法。
  • 移动对象的同时,如果对象被其他对象引⽤,则还需要调整引⽤的地址。
  • 移动过程中,需要全程暂停⽤户应⽤程序。即:STW
三种算法对比
项目标记-清楚标记-压缩复制算法
速度中等最慢最快
空间开销少(但会堆积碎⽚)少(不堆积碎⽚)通常需要活对象的2倍⼤⼩(不堆积碎⽚)
移动对象

分代收集算法

前⾯所有这些算法中,并没有⼀种算法可以完全替代其他算法,它们都具有⾃⼰独特的优势和特点。分代收集算法应运⽽⽣。

分代收集算法(Generational Collecting),是基于这样的⼀个事实:不同的对象⽣命周期是不⼀样的。因此不同⽣命周期的对象可以采取不同的收集⽅式,以便提⾼回收效率。⼀般是把java堆分成新⽣代和⽼年代,这样就可以根据各个年代的特点使⽤不同的回收算法,以提⾼垃圾回收的效率。
在这里插入图片描述

对象分配⼀般过程

为新对象分配内存是⼀件⾮常严谨和复杂的任务,JVM的设计者们不仅需要考虑内存如何分配、在哪⾥分配等问题,并且由于内存分配算法与内存回收算法密切相关,所以还需要考虑GC执⾏完内存回收后是否会在内存空间中产⽣内存碎⽚。

  • 1: new 的对象先放在Eden区。此区有⼤⼩限制。
  • 2:当Eden区空间填满时,程序⼜需要创建对象,JVM的垃圾回收器将对Eden区进⾏垃圾回收(MinorGC),将Eden区中的不再被其他对象所引⽤的对象进⾏销毁。再加载新的对象放到Eden区。
  • 3: 然后将Eden区中的剩余对象移动到Survivor 0区。
  • 4:如果再次触发垃圾回收,此时上次幸存下来的放到Survivor 0区的,这次如果没有回收,就会放到Survivor 1区。
  • 5: 如果再次经历垃圾回收,此时会᯿新放回Survivor 0区,接着再去Survivor 1区。
  • 6: 什么时候去⽼年代呢?可以设置次数。默认是15次。可以设置参数: -XX:MaxTenuringThreshold= 进⾏设置。
  • 7:在⽼年代,相当悠闲。当养⽼区内存不⾜是,再此触发GC:Major GC,进⾏⽼年代的内存清理。
  • 8: 若⽼年代执⾏了 Major GC 之后发现依然⽆法进⾏对象的保存,就会产⽣OOM异常。java.lang.OutOfMemoryError: Java Heap Space
    在这里插入图片描述

总结:

  • 针对幸存者S0、S1区的总结:复制之后有交换,谁空谁是to。
  • 关于垃圾回收:频繁在新⽣代收集,很少在⽼年代收集,⼏乎不在永久代/元空间收集
对象分配的特殊情况

在这里插入图片描述

import java.util.ArrayList;
import java.util.List;
import java.util.Random;
/**
-Xms600m -Xmx600m
*/
public class HeapInstanceTest {
	byte[] buffer = new byte[new Random().nextInt(1024*200)];
	public static void main(String[] args) {
		List<HeapInstanceTest> list = new ArrayList<HeapInstanceTest>();
		while (true){
			list.add(new HeapInstanceTest());
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
		}
	}
}

在这里插入图片描述

各分代特点
新⽣代(Young Generation)

新⽣代特点:区域相对⽼年代较⼩,对象⽣命周期短、存活率低,回收频繁

这种情况适合复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象⼤⼩有关,因此很适⽤于年轻代的回收。⽽复制算法内存利⽤率不⾼的问题,通过hotspot中的两个survivor的设计得到缓解。

⽼年代(Old Generation)

⽼年代特点:区域较⼤,对象⽣命周期⻓、存货效率⾼,回收不及年轻代频繁

这种情况存在⼤量存活率⾼的对象,复制算法明显变得不合适。⼀般是由标记-清除或者是标记-清除与标记-整理的混合实现。

  • 标记(Mark)阶段的开销与存活对象的数ᰁ成正⽐;
  • 清除(Sweep)阶段的开销与所管理区域的⼤⼩成正⽐相关;
  • 压缩(Compact)阶段的开销与存活对象的数据成正⽐。

⽼年代存放的都是⼀些⽣命周期较⻓的对象,就像上⾯所叙述的那样,在新⽣代中经历了N次垃圾回收后仍然存活的对象就会被放到⽼年代中。此外,⽼年代的内存也⽐新⽣代⼤很多(⼤概⽐例是1:2),当⽼年代满时会触发Major GC(Full GC),⽼年代对象存活时间⽐较⻓,因此FullGC发⽣的频率⽐较低。

永久代(Permanent Generation)

永久代主要⽤于存放静态⽂件,如Java类、⽅法等。永久代对垃圾回收没有显著影响,但是有些应⽤可能动态⽣成或者调⽤⼀些class,例如使⽤反射、动态代理、CGLib等bytecode框架时,在这种时候需要设置⼀个⽐较⼤的永久代空间来存放这些运⾏过程中新增的类。

分代的思想被现有的虚拟机⼴发使⽤。⼏乎所有的垃圾回收器都区分新⽣代和⽼年代。

增量式垃圾回收

增量式垃圾回收并不是⼀个新的回收算法, ⽽是结合之前算法的⼀种新的思路。

之前说的各种垃圾回收, 都需要暂停程序, 执⾏GC, 这就导致在GC执⾏期间, 程序得不到执⾏. 因此出现了增量式垃圾回收, 它并不会等GC执⾏完, 才将控制权交回程序, ⽽是⼀步⼀步执⾏, 跑⼀点, 再跑⼀点, 逐步完成垃圾回收, 在程序运⾏中穿插进⾏. 极⼤地降低了GC的最⼤暂停时间。

总体来说,增量式垃圾回收算法的基础仍是传统的标记-清除和复制算法。增量式垃圾回收通过对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的⽅式完成标记、清理或复制⼯作。

缺点

  • 使⽤这种⽅式,由于在垃圾回收过程中,间断性地还执⾏了应⽤程序代码,所以能减少系统的停顿时间。但是,因为线程切换和上下⽂转换的消耗,会使得垃圾回收的总体成本上升,造成系统吞吐量的下降。

分区算法

⼀般来说,在相同条件下,堆空间越⼤,⼀次GC时所需要的时间也就越⻓,有关GC产⽣的停顿也越⻓。为了更好的控制GC产⽣的停顿时间,将⼀块⼤的内存区域分割成多个⼩块,根据⽬标的停顿时间,每次合理回收若⼲个⼩区件,⽽不是整个堆空间,从⽽减少⼀次GC所产⽣的停顿。

分代算法将按照对象的⽣命周期⻓短划分成两个部分,分区算法将整个堆空间划分成连续的不同⼩区间。

每⼀个⼩区间都独⽴使⽤,独⽴回收。这种算法的好处是可以控制⼀次回收多个⼩区间。
在这里插入图片描述
注意:这些都只是基本的算法思路,实际GC实现过程要复杂得多,⽬前发展中的前沿GC都是复合 算法,并且并⾏和并发兼备。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值