详解分代回收理念,复制算法,标记清除,标记整理算法

垃圾回收概述

在内存动态分配和垃圾收集技术语言还在胚胎阶段时,开发者就在思考三个问题。那些内存需要回收?什么时候回收?如何回收?

我们讲的运行时数据区主要分为三大类。栈,堆,方法区。

栈内存中的程序计数器,虚拟机栈,本地方法栈3个区域随着线程而生,也随线程而灭。每一个栈帧分配的内存基本上在编译期就已经确定下来了。因此这几个区域基本上是具备确定性的,我们不需要在这几个栈区域考虑垃圾回收问题。当方法结束或线程结束时,内存自然也跟着回收了。

而Java堆和方法区则有着明显的不确定性。一个接口所需要的多个实现类需要的内存可能不一样。一个方法所执行的不同条件分支所需要的内存也可能不一样(写个if,else创建的对象多少,以及执行次数的不确定)。只有处于运行期间,我们才知道究竟会创建哪些对象,创建多少个对象。这部分内存的分配和回收是动态的。好了,这里主要指的是堆内存的垃圾回收。

方法区的垃圾收集主要回收两部分内容:废弃的常量池和不再使用的类型。以一个字符串对象为”java”的常量为例。当这个常量没有任何字符串对象引用他,虚拟机也没有其他地方引用他,如果发生内存回收,”java”这个常量就会被清理出常量池。常量池中的其他类(接口),方法,字段的符号引用也于此类似。而类型信息被回收的条件就相当苛刻了。要保证该类所有实例已经被回收。该类的类加载器被回收。该类地class对象没有任何地方被引用,此时该类型才允许被回收。注意:此处依旧是允许被回收,具体还有相关参数可以控制。

扩展:在大量使用反射,动态代理,CGLib等字节码框架,动态生成的JSP以及OSGI这类频繁自定义类加载器的场景中,通常都需要Java虚拟机具备类型卸载的能力,以保证不会对方法区造成过大的压力。

为什么需要分代回收

1. 绝大部分对象都是朝生夕死------新生代。

2. 熬过多次垃圾回收的对象就越难回收(一般是静态变量或者常量)。------老年代

垃圾回收算法

复制算法----新生代

Eden区的发展史

所有新生代都才有复制算法进行垃圾回收

image.png

最开始设计的新生代划分了两个相等的区域。左边用来存放新进来的对象。当内存满的时候,开始对左边的区域进行筛选。如果存活(根可达)的对象,则copy到右边。当所有存活对象拷贝完毕之后,对左边所有对象进行全部清除。这样做的优点有:1:实现简单,运行高效。2:没有内存碎片。缺点是:利用率只有一半。

为了解决最开始设计的新生代问题,我们根据绝大多数对象都是朝生夕死的原则(各大公司做过数据分析,98%的对象都逃不过朝生夕死的特点)。因此提出了”Appel式回收”,具体做法是把新生代分为一块较大的Enden区和两块较小的Survivor区。HotSpot虚拟机默认的Eden区和Survivor区大小比例为8:1。即每次新生代中可用的内存为整个新生代容量的90%(Eden区的80%和Survivor的10%),另外10%用来存储逃过本次GC的存活对象。

image.png

优点:这样做提高了内存的使用率,避免了频繁GC。但是我们得出的98%只是普查场景的数值。如果每次存活的对象大于From或To区的最大存储量又该如何呢?那么我们就会把这些对象提放入老年代。

空间分配担保

在发生Minor GC的时候,我们每次都会判断一件事。啥事呢?就是我们要先判断老年代可用的连续空间是否大于整个新生代空间。为什么是整个新生代空间呢?因为在Minor GC前,我们并不知道此次会出现多少垃圾。那么我们就取极限法——整个新生代全是垃圾。来判断老年代是否可以容纳。(但到了这里还并不是所谓的空间担保,那么真正的空间担保是什么呢?)

当老年代的连续可用空间小于新生代的总空间就直接进行Full GC么?不,要知道Full GC对程序的影响是相当大的,我们所有垃圾回收器都极力想避免,或延长Full GC的到来。我们如果仅仅这样取极限法就进行Full GC是肯定会浪费大量空间导致Full GC提前来临的。

那么垃圾回收器是如何处理的呢?答案是当老年代最大连续空间小于新生代总空间时,我们将进入空间分配担保环节。垃圾回收器会记录以往新生代每次Minor GC所产生的垃圾平均值。老年代连续空间会再次默认你会产生和历史平均值一样多的内存再次担保本次Minor GC继续执行。

既然提到了担保,那么必然是有风险的。如果担保失败,也就是这次Minor GC产生的垃圾远超过历史平均值。那么我们依旧会老老实实发起Full GC。即使这样做可能会绕一个大圈子,但我们依旧这么做。可见Full GC对程序的影响,对于绕点圈子来说都不值一提了。

复制算法的底层实现

绿色代表根可达对象。

灰色代表朝生夕死的对象。

白色代表空闲内存。

image.png

 image.png

 image.png

image.png

image.png

 image.png

初始阶段,新对象都会被分配到Eden区,这时候Survivor区是空的。当Eden区满了的时候,就会触发Minor GC进行新生代的垃圾回收,存活下来的对象会被存入Survivor的From区域。此时对象的年龄是1。之后清空Eden空间。当下一次Minor GC来临的时候会重复此过程,只不过这次Survivor区域的From,To会交换身份。同时上一次Minor GC幸存下来的对象会被复制到to区域年龄再次加1。(总结:from和to区的数据都是跟着Eden区的变化而变化的。只有移动次数(年龄)达到15的对象,才会主动进入老年代)。

标记清除算法----老年代

image.png

根据可达性分析算法,分析出哪些对象时可以被回收的(垃圾)。第一遍扫描进行标记。第二遍扫描进行对标记的垃圾回收。

优点:1:空间利用率百分百。(和复制算法比较)

2:对象地址不会发生改变,不需要修改引用的地址。(和标记整理算法比较)

缺点:1:扫描两次效率太低。

2:造成了内存碎片。假设有大对象进入,就会提前触发Full GC。

标记整理算法----老年代

image.png

所谓的标记整理,并不是在标记清除的基础上进行整理。根据可达性分析,分析出哪些对象时可以被回收的(垃圾)。

1.标记:第一次扫描对对象进行标记

2.整理:对存活的对象开始整理,依次按内存开始的区域依次摆好,整整齐齐,中间没有空隙。(此步骤最耗时,因为牵扯到了指针的移动)。

3在摆放完最后一个对象的同时,对之后的内存直接进行回收

扩展:为什么不一边清理一边移动呢?

1. 因为打包干活效率高(一波删除)

2. 对象的遍历顺序与内存顺序不同。如果一个对象非常靠前,当有确定存活的对象进行插队,那么这个对象的引用也会发生改变。

优点:1:空间利用率百分百。(和复制算法比较)

2:没有内存碎片。(和标记清除算法比较)

缺点:1:移动地址,效率更低。

2:地址发生了改变。因此引用地址发生了改变。此时需要线程暂停修改引用地址。(复制算法也牵扯到这类问题。但是复制算法的对象很少,所以暂时的时间很少,一般不拿出来单独讨论)。

猜你喜欢

转载自blog.csdn.net/weixin_47184173/article/details/109610926