概述
在开始介绍各种垃圾回收集器之前,你要知道:
(1)那些内存需要回收
(2)什么时候回收
(3)如何回收
第一个,那些内存需要回收呢?
答案是堆和方法区。上一篇说过了Java内存运行时的各个部分,其中,程序计数器,虚拟机栈,本地方法栈三位随线程而生,随线程而灭。
每一个栈帧中分配的内存基本上是在类结构确定下来的时候就已经确定了,因此,这几个区域的内存分配和回收都具备确定性,在这几个区域中,不需要过多考虑回收的问题,因为方法结束后缀线程结束时,内存自然就跟着回收了。
但是,Java堆和方法区就不一样了。一个接口的多个实现类需要多少内存是不一定的,他们在运行期间才知道需要创建那些对象,他们的内存分配和回收都是动态的。
以后咱们说的内存回收与分配指的都是这两个部分的内存!
第二个,什么时候回收
垃圾收集器在对对进行回收之前,第一件事就是确认那些对象是“活的”,那些已经“GG”了。
判断对象是否还存活的算法:
(1)引用计数算法
给对象添加一个引用计数器,每当有一个地方引用它的时候,计数器就加1;当引用失效的时候,计数器减去1,任何时刻计数器为0的对象,就是不可能被使用的对象。
客观的说,该算法的实现简单,判断的效率也高,但是他有一个问题,就是很难解决对象之间相互引用。
例如:对象A和对象B,都有一个a属性,然后A.a=B,B.a=A。这样A和B永远不会被搞定
(2)可达性分析算法
该算法的基本思路是通过一系列称之为“GC Roots”的对象作为起点向下搜索,搜索走过的路径称之为“引用链”,当一个对象和“GC Roots”没有引用链连接的时候(也可称为对象到“GC Roots”不可达),认为该对象是可以回收的。
垃圾收集算法
这里只简单介绍几种算法的思想
(一)标记-清除算法
这个是最基本的收集算法,分为“标记”和“清除”两个阶段:
首先标记出来需要回收的对象,在标记完成之后回收所有被标记的对象。
之说以说它是最基础的收集算法,是因为之后收集算法都是基于这种思路,并对其不足加以改进。
其不足主要有两点:
(1)效率不高,标记和清除两个步骤效率都不高
(2)空间问题,标记清除之后会有大量不连续的内存碎片,空间碎片太多会导致以后在程序运行过程中需要分配较大的对象时,无法找到足够的连续内存,从而不得不提前触发gc动作。
(二)复制算法
(1)为了解决效率问题,复制算法出现了,它把内存按照容量划分成两块相等的部分,每次只使用一块。
当一块内存使用完的时候,就把还存活的对象复制到另一块上,然后再把第一块内存空间一次清理掉。
这样分配的时候也就不需要考虑内存碎片的情况了,只要移动指针,按照顺序分配内存即可,实现简单,运行效率也高。
(2)但是,这种算法的代价就是将内存缩小为原来的一半,代价有点大啊!
(3)现在的商业虚拟机都是使用这种收集算法来回收“新生代”,因为新生代里面对象的特点是98%的对象活不过第二集.
所以并不需要按照1:1的比例来划分内存空间,而是将内存划分成一块较大Eden区和两块较小的Survivor区,每次使用Eden区和一块Survivor(假如是S1)区。
当回收的时候,将Eden区和S1中还存活的对象一次性地复制到另一块Survivor(S2)中,最后清除掉Eden和S1。
Hotspot虚拟机默认Eden和survivor的比例是8:1:1,也就是新生代中可用的内存是整个新生代容量的90%,只有10%的内存被浪费。
注意:
我们并不能保证每次回收的存活对象小于10%,当survivor的空间不够用的时候,需要依赖其他内存(这里指的是老年代)进行分配担保(这一块在垃圾收集器执行规则的时候再说吧)。这就和银行的担保机制一个味了。
(三)标记-整理算法
复制算法比较适合“新生代”这种存活率较低的内存区域,但是对于老年代,动不动就存活率90%以上的区域,显然不太合适。
所以有人提出了“标记-整理”算法。
(1)标记的过程还和之前的一样,但是后继步骤不是直接对可回收对象进行清理,而是将所有的存活对象向一端移动,然后直接清理掉边界之外的内存。就和吃鸡一样啊,毒气范围之外的才能活。
(四)分代收集算法
当商业虚拟机的垃圾收集都采取这个算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同,将Java堆划分为新生代和老年代。这样就可以在不同区域使用比较适合其特点的算法了。
在新生代中使用复制算法,老年代使用标记整理算法。
该说不说的,这个算法有点抄作业的感觉啊!