1 先谈Finalize()
finalize()方法确实可以实现一次对象的自救,但是其不确定性和昂贵的运行代价都表明这个方法的使用需要十分的慎重。那么finalize()在什么时期起作用又是如何实现对象的自救的呢?首先我们要理解虚拟机在扫描到死亡对象的时候并不是直接回收的,而是进行一次标记并且筛选,筛选的条件就是其对象的finalize方法是否有必要执行。如果当前对象没有重写finalize方法或者已经调用过一次finalize方法,那么则视为没有必要执行,此时便失去自救的机会,放入"即将回收"集合中。
否则的话,则将对象放入一个叫F-Queue的队列中,稍后虚拟机将一个个的执行队列中对象的finalize方法(就是在此处对象可以在finalize方法中将自身关联到引用链,从而暂时逃脱被回收的命运),需要注意的是虚拟机保证执行但不保证执行完finalize方法,原因是如果finalize方法执行时间过长或者陷入死循环,则可能让系统奔溃。全部执行之后,虚拟机将对队列的对象重新标记一次,如果还不在引用链中则GG,否则将其移出"即将回收"集合。下面例子参考《深入理解JVM》实现自救并且验证只能自救一次的过程。
public classTestForGc {/**定义一个根节点的静态变量*/
public staticTestForGc INSTANCE;/*** 重写finalize方法,让其被标记为有必要执行并且加入F-Q
*
*@throwsThrowable*/@Overrideprotected void finalize() throwsThrowable {super.finalize();
System.err.println("finalize method in TestForGc Class invoked!");//将自身关联到根节点中,实现自救
INSTANCE = this;
}public static void main(String[] args) throwsInterruptedException {
INSTANCE= newTestForGc();
INSTANCE= null;
System.gc();//睡眠1S,保证F-Q中的方法执行完毕
TimeUnit.SECONDS.sleep(1);if(Objects.nonNull(INSTANCE)) {
System.out.println("i successfully save myself by finalize method!");
}else{
System.out.println("i am dead :(");
}/** 下面验证finalize方法只能调用一次
* 几乎完全一样的代码,却是不同的结局*/INSTANCE= null;
System.gc();//睡眠1S
TimeUnit.SECONDS.sleep(1);if(Objects.nonNull(INSTANCE)) {
System.out.println("i successfully save myself by finalize method again!");
}else{
System.out.println("couldn't invoke finalize again, i am dead :(");
}
}
}
执行结果:
2 垃圾回收器
如果说回收算法是接口,那么垃圾回收器就是这些接口的实现类,共有7种回收器,接下来一一罗列。
2.1 Serial垃圾回收器
Serial是一种单线程垃圾回收器,在工作的时候的时候会暂停所有的用户线程,也就是"stop-the-world",虽然单线程代表了用户线程的停顿,但是也意味着其不用进行线程的交互从而有更高的收集 效率。Serial采用复制算法,是Client端新生代的默认垃圾回收器。其工作图类似于:
2.2 ParNew垃圾回收器。
ParNew是Serial回收器的多线程版本,是Server端新生代的默认回收器,除了并行多线程之外,其他包括实现都是一模一样,当然也是采用复制算法。还有一点重要的是,新生代的收集器除了Serial之外,只有ParNew能跟年老代的CMS合作,其在低CPU的情况下效率比Serial低,但是在多个CPU的情况下要好的多。其工作图:
2.3 Parallel Scavenge垃圾回收器
跟ParNew类似,作用于新生代,并行多线程并且也是采用复制算法。但是其关注的点却不同,其着重的是一种叫做"吞吐量"的东西。所谓的"吞吐量"=运行用户代码的时间 / (运行用户代码的时间 + GC时间),也就是说其更加注重用户代码运行时间而不是减少GC停顿时间。相对于其他收集器来说,可以更加高效的利用CPU,更加适合作为在后台运算而不大需要交互的任务。Parallel收集器提供了两个比较重要的参数。
2.4 Serial Old垃圾回收器
Serial的年老代版本,同Serial基本相似,不同的是采用的是标记-整理算法实现,作为Client端默认的年老代收集器。如果在Server端的话,那么其主要作用有二:
2.5 Parallel Old垃圾回收器
Parallel Scavenge的年老代版本,多线程并行,同样注重吞吐量,使用标记-整理算法。这个收集器可以跟新生代的Parallel Svavenge一起搭配使用,在注重吞吐量和CPU资源敏感的场合中是一对很好的组合。
2.6 CMS垃圾回收器
来了,它来了!CMS垃圾回收器被当做是具有划时代意义的、真正实现并发的垃圾回收器,总而言之=》
,--^----------,--------,-----,-------^--,
| ||||||||| `--------' | O
`+---------------------------^----------|
`\_,-------, _______________________强__|
/ XXXXXX /`| /
/ XXXXXX / `\ /
/ XXXXXX /\______(
/ XXXXXX /
/ XXXXXX /
(________(
`------'
CMS是一款并发的垃圾回收器,但并不代表全程都不需要停顿,只是大部分时间是跟用户线程一起执行的。其整个GC过程中总共有4个阶段。
然而,优秀如CMS也会有不足之处,总共四个阶段的标记及清除算法的实现必定为其带来一些使用的麻烦。
2.7 G1垃圾回收器
G1是一个新秀垃圾回收器,被赋予了很大的使命——取代CMS。G1作为新时代的垃圾回收器,相对于其他垃圾回收器来说有许多优势。
G1回收器将内存空间分成若干个Region,并且这些Region之前相互独立。但是我们都知道这并不能真正的独立,因为一个Region中的对象不一定只会被当前Region的其他对象引用,而可能被堆中的其他对象引用,那G1是如何实现避免全堆扫描的呢?这个问题在分代的其他回收器中也有,但是在这里突显得更加明显而已。再G1中,对象本身都会有一个Remembered Set,这个Set存放着当前对象被其他区域对象引用的信息,这样子,在扫描引用的时候加上这个Set就可以避免全堆扫描了。
具体实现大致为:虚拟机在发现程序正在进行对Reference类型的写操作时,会暂时中断写操作,然后检查Reference引用的对象是否处于不同的区域(如果是分代,则只对年老代的对象进行检查,检查是否引用的对象在新生代),如果是的话则将引用信息记录在被引用的Remembered Set中,这样在GC的时候加上Remembered Set的扫描就可以避免全堆扫描了。
跟CMS类型,G1也有四个阶段(不算Remembered Set的扫描),虽然相似但是还是有些区别的。
3 小结
本文首先介绍了“对象自救”的方法——finalize,并且用一个小例子演示了对象如何实现自救。接着介绍了7种不同的垃圾回收器,新生代中有单线程的Serial可以作为Client端新生代的默认回收器,有多线程版本的Serial——ParNew,还有着重点不同(吞吐量)的Parallel Scavenge;年老代方面有单线程的Serial Old、跨时代意义的并发回收器——CMS,虽然优秀还是其使用的算法和实现导致了它的三个缺点、还有吞吐量年老代版本——Parallel Old收集器,最后还简单介绍了G1收集器的几个过程还有独立的Region间是如何实现避免堆扫描的。
整体下来整篇行文还有些粗糙,日后会慢慢的圆润,如果有关于这方面好的文章可以在下面评论区分享学习一下,下方为各个垃圾回收器的搭配图。