在 Java 中,所有的对象都是要存在内存中的(也可以说内存中存储的是一个个对象),因此我们将内存回收,也可以叫做死亡对象的回收;
GC 回收的目标是堆上的对象;而栈中的局部变量会跟随栈帧的声明周期结束,静态变量生命周期是整个程序,因此都不需要 GC 回收;
GC 回收操作可分为两步——找到垃圾对象和回收垃圾对象
1. 找到垃圾对象
1)引用计数(Python 采用这种方式)
为 new 出来的对象,单独安排一块空间,保存计数器,表示有多少个引用指向该对象,当一个对象的引用计数器为 0,就可以视为垃圾了;
引用计数存在的问题:
a. 比较浪费内存
b. 存在循环引用问题,下面是个循环引用的示例:
public class Main {public Main t;public static void main(String[] args) {Main a = new Main();Main b = new Main();a.t = b;b.t = a;a = null;b = null;}
}
2)可达性分析(Java 采用这种方式)
有一个或一组线程,周期性的扫描代码中的所有对象(从一些特定的对象出发,尽可能的进行访问的遍历,把所有能够访问到的对象都标记为 "可达",未被标记的对象则为 "垃圾");这些特定的对象称为 "GC Roots",从这些节点开始向下搜索,搜索走过的路径称之为 "引用链",当一个对象到GC Roots 没有任何的引用链相连时(说明 GC Roots到这个对象不可达),则该对象是不可用的,需要被回收;
在 Java 中,可作为 GC Roots 的对象包含以下几种
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中 JNI(Native 方法)引用的对象;
2. 回收垃圾
1)标记清除
该算法分为 "标记" 和 "清除" 两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象;
缺点:
a. 效率问题:标记和清除这两个过程的效率都不高;
b. 空间问题:标记清除后会产生大量不连续的内存碎片,导致剩余的内存空间不连续,这可能会导致以后在程序运行中,需要分配较大对象时,无法找到足够连续的内存;
2)复制算法
"复制" 算法是为了解决 "标记,清理" 的效率问题。它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这块内存需要进行垃圾回收时,会将此区域还存活着的对象复制到另⼀块上面, 然后再把已经使用过的内存区域一次清理掉。(通过复制的方式,把有效的对象归类到一起,再统一释放剩下的空间)这样做的好处是每次都是对整个半区进行内存回收,不会出现内存碎片问题,此算法实现简单,运行高效。
缺点:
有效对象非常多时,复制拷贝的开销会非常大;
3)标记整理:
此算法的标记过程仍与 "标记,清除" 过程一致,但后续步骤不是直接对可回收对象进行清理,而是让所有存活对象都向一端移动,然后直接清理掉该端边界以外的内存;
1 √ | 2 | 3 √ | 4 | 5 √ | 6 |
如图,若 1,3,5是无效的垃圾,则经该算法整理后会变为:
2 | 4 | 6 |
缺点:搬运的开销仍很大
4)分代回收算法(Java 采取的方式)
分代回收算法可以说是上述三种方法的结合体,通过区域划分,实现不同区域和不同的垃圾回收策 略,从而实现更好的垃圾回收。
该算法把 Java 堆分为新生代和老年代;其中新生代放新建的对象,老年代存放在经新生代的多次GC 之后还存活的对象;
新生代又分为 伊甸区和幸存区(又分为大小相等的两块);
在新生代中,每次垃圾回收都有大批对象死去,只有少量存活,因此采用复制算法;
在伊甸区中,使用复制算法将有效对象复制到幸存区,然后将整个伊甸区释放;
在幸存区中,每次只使用其中一块,也使用复制算法,将活过 GC 的有效对象,拷贝到另一块幸存区,然后将该快幸存区释放;
而老年代中对象存活率高、没有额外空间对它进行分配担保,就采用 "标记-清理" 或者 "标记-整理" 算法;
当某个对象已经在幸存区存活很多轮 GC 之后,JVM 就认为该对象短时间内不会释放,会把该对象拷贝的老年代, GC 仍会使用标记算法扫描老年代,但是扫描老年代的频率会比扫描新生代的频率低很多;