一、GC回收区域
前面两篇博客了解到,JVM的内存模型中程序计数器、虚拟机栈、本地方法栈这三哥内存区域随线程的生命周期结束而消亡,栈中存储的栈帧分配多少内存大体上是在编译期就已经确定的,因此这几个区域的内存与回收都是明确的,当方法结束或者线程结束时,内存自然跟着回收了。
与栈不同,Java堆和方法区有着不确定性:一个接口的多个实现类需要的内存可能会不同,一个方法执行不同条件分支需要的内存也不同,这些是在编译期无法确定的,只有在运行期间才能得知,这部分的分配和回收是动态的,GC所关注的也是这部分内存的回收管理。
二、回收对象区分
既然已经明确了GC回收的主要区域是堆和方法区,方法区的回收条件十分苛刻,且回收成效甚微,在这里只着重讨论GC在堆中的活动,前面了解到堆中主要存放创建的Java对象,那么GC在回收前要做的第一件事就是确定哪些对象还有存在的价值,哪些对象已经没用了,而GC区分对象基于两种方式:引用计数法、可达性分析。
2.1 引用计数法
在对象中添加一个引用计数器,每当有地方引用它时,计数器加一;引用失效时,计数器减一。计数器为0时,代表该对象不再被使用。
优点:简单高效
缺点:占用了额外的内存空间进行计数,难以解决对象间的循环引用问题,如下:
... ...
public Object instance = null ;
public void test(){
ObjDemo objA = new ObjDemo();
ObjDemo objB = new ObjDemo();
objA.instance = objB;
objB.instance = objA;
}
... ...
2.2 可达性分析算法
目前,主流的虚拟机都是通过可达性分析算法来判定对象是否存活。
基本思路是通过“GC Roots”根对象作为起始点,根据引用关系向下搜索,搜索过程中所走过的路径称为“引用链”(Refrence Chain),如果一个对象到GC Roots间没有任何的引用链,则表示该对象不再被使用。

固定可以作为GC Root的对象包括:
(1)虚拟机栈中引用的对象,如线程被调用的方法堆栈中使用到的参数、局部变量、临时变量。
(2)在方法区中类静态属性引用的对象,如引用类型静态变量。
(3)方法区中常量引用的对象
(4)Native方法引用的对象
(5)基本数据类型对应的Class对象
(6)异常对象,如NullPointerException
(7)同步锁持有的对象
(8)程序执行中,临时加入的对象
三、强、弱、软、虚引用
无论是引用计数还是可达性分析,本质都是判断对象是否还存在引用关系。Java对引用划分为四类:强引用(Strongly Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)。四种强度逐级减弱。
(1)强引用:无论何种情况下,只要强引用关系存在,GC就不会回收掉被引用的对象。如下:
public class Demo {
public static void main(String[] args) {
StronglyRefDemo stronglyRefDemo = new StronglyRefDemo();
System.out.println("输出:"+stronglyRefDemo);
System.gc();
System.out.println("gc后:"+stronglyRefDemo);
}
}
运行结果如图:

(2)软引用:用来描述一些还有用,但非必须的对象。被软引用关联的对象,在系统即将发生OOM异常前,会被纳入GC回收范围进行第二次回收,如果被软引用关联的对象回收后,依然没有足够的内存空间才会抛出OOM异常。
软引用通过 SoftReference<Object> softReferenceObj = new SoftReference<>(new Object()); 创建。
(3)弱引用:中来描述非必须的对象,强度比软引用还弱。被弱引用关联的对象只能存活到下一次GC发生为止。无论当前内存空间是否足够,都会回收掉这些被弱引用关联的对象。
弱引用最典型的是TreadLocal会引起内存泄露,感兴趣的朋友可以去查阅一下。
弱引用通过WeakReference<Object> objectWr = new WeakReference<>(new Object());创建。
(4)虚引用:虚引用只有一种功效,就是为了能在对象被GC回收时收到一个通知。一个对象是否有虚引用关系,对其生命时间完全构不成影响,是四种引用当中最弱的一种引用关系。
虚引用创建方式:
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> oneObjectPr = new PhantomReference<>(new Object(),queue);
四、自我救赎之finalize
GC可达性分析判定为待回收的对象,并不意味着该对象必须被回收,真正被回收至少要经历两次标记的过程:
(1)可达性分析后发现没有有GC Roots之间有引用链,将第一次标记该对象。
(2)检查对象是否需要执行finalize()方法,如果该对象没有覆盖finalize()方法,或者finalize()已经执行过了,虚拟机会将这两种情况视为“没有必要执行”。如果对象被判定需要执行finalize()方法,该对象会进入一个名为F-Queue的队列中,虚拟机随后会建立一个低调度优先级的Finallizer线程去执行该对象的finalize()方法。finalize()方法是待回收对象的最后一次救赎,只要待回收对象在finalize()中重新与引用链上的任何一个对象建立关联,就能拯救自己被回收的命运,否则,就会被GC回收掉。
一个对象的finalize()方法最多只会被系统调用一次。自我救赎复活案例代码如下:

执行结果如下:

五、垃圾收集算法
5.1 标记-清除算法
和它的名字一样,算法被分为“标记”和“清除”两个阶段:
(1)标记所有需要回收的对象;
(2)标记完成后,统一回收掉被标记的对象。
标记-清除算法是最基础的算法,后续的算法也基本上是对标记-清除算法的缺点进行改造:
优点:(1)算法简单 (2)适用于存活对象比较多的情况;
缺点:(1)执行效率不稳定,标记和清除过程的执行效率随着对象的增加而降低;(2)标记清除后,会产生内存空间碎片,可能会导致后面的大对象无法找到足够的连续内存而引发一次GC。

5.2 标记-复制算法
目的:解决标记-清除算法在面对大量回收对象时回收效率低下。
在“半区复制”算法基础上,标记-复制采用了更优化的半区复制分代策略:
(1)把新生代分为一块较大的Eden空间和两块较小的Survivor空间(From和To),每次分配内存只使用Eden和其中一块Survivor;
(2)垃圾收集时,将Eden和From Survivor中仍然存活的对象一次性复制到另外一个To Survivor空间上;
(3)直接清理Eden和From Survivor空间。
(4)From Survivor 和 To Survivor 互换。

至此,标记-复制算法的优缺点显而易见:
优点:不会产生内存空间碎片;
缺点:(1)对象存活率较高时,大量的复制操作会降低效率 (2)空间浪费
5.3 标记-整理算法
目的:针对标记复制算法的缺点,并不适合在老年代当中使用,老年代当中的大对象复制会极大影响效率,且复制需要更大的内存空间去完成,对此引出了标记-整理算法:
(1)标记:过程与“标记-清除”算法一致;
(2)整理:所有存活对象向内存空间一端移动;
(3)清除:清理掉边界以外的内存。
