垃圾回收的概述与算法

垃圾回收概述

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

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

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

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

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

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

为什么需要分代回收

  1. 绝大部分对象都是朝生夕死------新生代。
  2. 熬过多次垃圾回收的对象就越难回收(一般是静态变量或者常量)。------老年代

 

垃圾回收算法

复制算法----新生代

Eden区的发展史

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

垃圾回收的概述与算法

 

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

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

垃圾回收的概述与算法

 

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

 

空间分配担保的个人理解:

在发生Minor GC的时候,共有两处发生了空间分配担保。

1:Eden区进入Servivor时,会触发第一种空间分配担保。此时会判断From或To区是否空间足够。如果不够则直接进入老年代。

2:当新生代有对象进入老年代时,会触发第二种空间分配担保。此时会判断老年代空间是否足够容易所有新生代的对象所占用的空间总和。如果不够存放则先尝试回收,回收失败则进行Full GC(全局GC,包含整个堆区+方法区)再次尝试。

复制算法的底层实现

绿色代表根可达对象。

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

白色代表空闲内存。

 

垃圾回收的概述与算法

 

垃圾回收的概述与算法

 

垃圾回收的概述与算法

垃圾回收的概述与算法

 

垃圾回收的概述与算法

 

垃圾回收的概述与算法

 

垃圾回收的概述与算法

 

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

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

 

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

垃圾回收的概述与算法

 

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

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

缺点:1:扫描两次效率太低。(为什么要扫描两次,我发现垃圾清除一个不香么?我们可以把标记和删除当做两次批量操作来处理。小时候老师罚我们一篇背不过的古诗词抄100遍。我习惯的处理方式总是从上到下先抄一个字,一个字抄完100遍,再抄下一个字100遍。于是抄完整个古诗后,老师发现我还是不会背!但不可否认,在完成这件事上,我的效率是更高的。所以我们受到了启发。如果是我们是一个海盗。当我们发现一个宝藏岛的时候,我们不应该直接上去寻找一处宝藏,找到后就运走然后再来岛上寻找第二个宝藏运走。我们应该先直接探索整个小岛,绘制出有宝藏的地图。当探索结束后,第二波来岛上一次性把整个岛上的宝藏全部带走才是真的香!)。

垃圾回收的概述与算法

 

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

 

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

 

垃圾回收的概述与算法

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

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

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

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

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

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

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

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

 

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

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