1.垃圾内存判定算法
JVM在运行过程中,需要对已死去的实例占用的内存进行回收。首要问题是判断实例是否“死去”。主要有两种算法——引用计数算法和可达性分析算法。
1.1.引用计数算法
实例被引用,计数器加1;引用被取消,计数器减1。
引用计数算法有局限性,例如:实例a和b都有字段c,且a.c=b、b.c=a,如果只是a与b互相引用,则a和b都应该被回收,但引用计数算法无法对此种实例进行回收。
1.2.可达性分析算法
以GC Roots的对象作为起点往下搜索,当一个对象不在GC Roots的任意一条链路中时,则标记此对象“死去”,并进行回收。
GC Roots可以是:虚拟机栈中引用的对象、方法区中静态属性引用的对象、方法区中常量引用的对象、本地方法栈中JNI引用的对象。
确认死去的实例后,就需要对这些实例占用的内存进行回收。回收算法主要有以下三种:标记-清除算法、复制算法、标记-整理算法、分代收集算法。
2.垃圾回收算法
根据使用场景不同,可有以下四种算法。
2.1.标记-清除算法
先对需要回收的对象进行标记,然后统一回收。如下图所示:
标记-清楚的缺点也是显而易见:清除之后的内存区域不连续,碎片太多。而且标记和清楚的效率都不高。
2.2.复制算法
将可用内存按容量分为大小相等的两份,每次只使用其中的一块,当这一块用完之后,把存活的对象复制到另一块,并把使用完了的进行清理。此算法实现简单,运行高效。如下图所示。
由上图可看出,实现复制算法需要保留一般的内存,极大地降低了内存的使用效率。考虑到新生代有98%的实例都是朝生夕灭,如果用复制算法来回收新生代的话,就可以减少保留区内存占用的比率。现在的商业虚拟机都是采用这种算法来回收新生代,一般把新生代分为Eden区和两个Survivor区,GC前,其中一个Survivor区作为保留区,保存每一次GC后还存活的对象,GC后此Survivor与Eden区组成可用空间,另一个Survivor就成为了保留区。
配置SurvivorRatio参数,可以改变Eden区和Survivor区内存大小比例,默认为8:1,也就是说新生代中Eden占80%,From Survivor占10%,To Survivor占10%,有10%的空间会被浪费。提升了复制算法的内存利用率。
2.3.标记-整理算法
所有存活的对象都向一端移动,直接清理端边界以外的内存。如下图所示:
这样,就解决了内存碎片化的问题。
2.4.分代收集算法
根据对象的存活时间来选择收集算法。
(1)一般新生代存活率低,使用复制算法。
(2)老年代存活率高、没有额外空间进行担保,使用“标记-清理”或者“标记-整理”算法。
更多关于垃圾回收问题,可参考https://docs.oracle.com/en/java/javase/12/gctuning/index.html。