【JVM虚拟机】垃圾回收算法和内存的分配和回收机制

垃圾回收算法

一、标记——清除算法

标记/清除算法的基本思想就跟它的名字一样,分为“标记”和“清除”两个阶段
首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象

1)标记阶段

标记的过程其实就是前面介绍的可达性分析算法的过程,遍历所有的GC root对象,对从 GCRoots对象可达的对象都打上一个标识,一般是在对象的header中,将其记录为可达对象;

2)清除阶段

清除的过程是对堆内存进行遍历,如果发现某个对象没有被标记为可达对象(通过读取对象header信息),将其收回

在垃圾收集器进行GC时,必须停止所有java执行线程(也称“Stop The World”),原因是在标记阶段进行可达性分析时,不可以出现分析过程中对象引用关系还在不断变化的情况,否则的话可达性分析结果的准确性就无法得到保证。在等待标记清除结束后,应用线程才会恢复运行。

3)缺点:
a、效率问题

标记和清除两个阶段的效率都不高,因为这两个阶段都需要遍历内存中的对象,很多时候内存中的对象实例数量是非常庞大的,这无疑很耗费时间,而且GC时需要停止应用程序,这会导致非常差的用户体验。

b、空间问题(内存碎片化)

标记清除之后会产生大量不连续的内存碎片(从上图可以看出),内存空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。

二、复制算法

复制算法每次都是对整个半区进行内存回收,这样就减少了标记对象遍历的时间,在清除使用区域对象时,不用进行遍历,直接清空整个区域内存,而且在将存活对象复制到保留区域时也是按地址顺序存储的,这样就解决了内存碎片的问题,在分配对象内存时不用考虑内存碎片等复杂问题,只需要按照顺序分配内存即可。

复制算法简单高效,优化了标记清除算法的效率低,内存碎片多的问题,存在

缺点:

1、将内存缩小为原来的一半,浪费了一半的内存空间,代价太高:
2、如果对象的存活率很高,极端一点的情况假设对象存活率为100%,那么我们需要将所有存活的对象复制一遍,耗费的时间代价也是不可忽视的。

三、标记—整理算法

标记-整理算法算法与标记清除算法很像,事实上,标记/整理算法的标记过程仍然与标记/清除算法一样,但后续步骤不是直接对可回收对象进行回收,而是让所有存活的对象都向一端移动,然后直接清理掉端边线以外的内存。

可以看到,回收后可回收对象被清理掉了,存活的对象按规则排列存放在内存中。这样一来,当我们给对象分配内存时,jvm只需要持有内存的起始地址即可。标记/整理算法弥补了标记/清除算法存在内存碎片的问题消除了复制算法内存减半的高额代价,可谓一举两得。

标记/整理算法的缺点

效率不高:不仅要标记存活对象,还要整理所有存活对象的引用地址,在效率上不如复制算法。

四、分代回收算法

分代收集算法的思想是按对象的存活周期不同将内存划为几块,一般是把java堆分为新生代和老年代(还有一个永久代,是HotSpot)特有的实现,其他的虚拟机实现没有这一概念,永久代的收集效果很差,一般很少对永久代进行垃圾回收),这样就可以根据各个年代的特点采用最合适的收集算法。

特点:

新生代:朝生夕灭,存活时间很短。采用复制算法来收集
老年代:经过多次Minor GC而存活下来,存活周期长。采用标记/清理算法或标记/整理算法收集老年代

新生代中每次垃圾回收都发现有大量的对象死去,只有少量存活,因此采用复制算法回收新生代,只需要付出少量对象 的复制成本就可以完成收集;
老年代中对象的存活率高,不适合采用复制法,而且如果老年代采用复制法,它是没有额外的空间进行分配担保的,因此必须使用标记/整理算法来进行回收

新生代中的对象几乎都是朝生夕死的(达到98%),现在的商业虚拟机都采用复制算法来回收新生代。由于新生代的对象存活率低,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden区,From Survivor区和Survivor to区,三者的比例为8:1:1。每次使用Eden和From Survivor区域,To Survivor区是空的。GC进行时,Eden区中所有存活的对象都会复制到To Survivor区,而在From Survivor区中,仍存活的对象会根据他们的年龄值决定去向,年龄值达到年龄阈值(默认为15,新生代中的对象每熬过一轮垃圾回收,年龄值就加1)的对象会被移到老年代中,没有达到阈值的对象会被复制到To Survivor区。接着清空Eden区和From Survivor区,新生代中存活的对象都在To Survivor区。接着,From Survivor区和To Survivor区会交换他们的角色,也就是新的To Survivor区就是上次GC清空的From Survivor区,新的From Survivor区就是上次GC的To Survivor区,总之,不管怎样都会保证To Survivor区在一轮GC后是空的。GC时当To Survivor区没有足够的空间存放上一次新生代收集下来的存活对象时,需要依赖老年代进行分配担保,将这些对象存放在老年代中。复制算法就不合适了。

分代回收:

我们从一个object1来说明其在分代垃圾回收算法中的回收轨迹。
1、object1新建,出生于新生代的Eden区域

2、通过minor GC后,object1还存活,移动到From Survivor 空间,此时还在新生代。

3、再通过minor GC后,object1仍然存活,此时会通过复制算法,将object1移动到To Survivor区域,此时object1的年龄age+1。

4、再通过minor GC后,object1仍然存活,此时survivor中和object1同龄的对象并没有达到survivor的一半,所有此时通过复制算法,将from Survivor和To Survivor区域进行互换,存活的对象被移动到了To Survivor。

5、再通过minor GC,object1仍然存活,如果此时survivor中和object1同龄的对象已经达到survivor的一半以上(To Survivor的区域已经满了),object1被移动到了老年代区域。

6、object1存活一段时间后,发现此时object1不可达GcRoots,而且此时老年代空间比率已经超过了阈值,触发了majorGC(也可以认为是fullGC,但具体需要垃圾收集器来联系),此时object1被回收了。fullGC会触发stop the world。

在以上的新生代中,我们有提到对象的age,对象存活于survivor状态下,不会立即晋升为老年代对象,以避免给老年代造成过大的影响,它们必须要满足以下条件才可以晋升
1、minor gc之后,存活于survivor区域的对象的age会+1,当超过(默认)15的时候,转移到老年代。
2、动态对象,如果survivor空间中相同年龄所有的对象大小的综合和大于survivor空间的一半,年级大于或等于该年级的对象就可以直接进入老年代。

内存的分配和回收

1、分配策略;

1)对象优先分配在Eden区
2)大对象直接进入老年代
3)长期存活的对象将进入老年代
4)动态对象年龄判定(年龄超过阈值或survivor空间相同年龄所有对象大小总和和大于survivor区一半,年龄大于或等于该年龄的对象直接进入老年代)

2、空间分配担保机制:

在执行Minor GC前,JVM会首先检查Tenured是否有足够的空间存放新生代尚存活对象,由于新生代使用复制收集算法,为了提升内存利用率,只使用了其中一个Survivor作为转换备份,因此当出现大量对象在Minor GC后仍然存活的情况时,就需要老年代进行分配担保,让Survivor无法容纳的对象直接进入老年代,但前提是老年代需要有足够的空间容纳这些存活的对象。但存活对象的大小在实际完成GC前是无法明确知道的,因此Minor GC前,**JVM会先首先检查老年代连续空间是否大于新生代对象总大小或历次晋升的平均大小,如果条件成立,则进行Minor GC,否则进行Full GC(让老年代腾出更多空间)。**然而取历次晋升的对象的平均大小也是有一定风险的,如果某次MinorGC存活后的对象突增,远远高于平均值的话,依次可能导致担保失败(老年代也无法存放这些对象了),此时就只好在失败后重新发起一次Full GC(让老年代腾出更多空间)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值