教科书里垃圾回收的思路
一、垃圾回收,回收的是什么?
你说是什么,当然是垃圾啊!!!神经病啊!!!
回收的是已经不再活着的对象,也就是死去的对象。(对象终有一死,请广大群众关爱自己,妈呀,说得好可怕)
二、既然回收的是死去的对象,那么如何判断对象已经死去?
当然是下列算法可以判断对象死去:
1、引用计数算法
-
概念
给对象中添加一个引用计数器,当有一个地方引用他时,计数器值就加1,当引用失效时,计数器值就减1.任何时刻,计数器为0的对象就是不可能再被使用的。
-
弊端
无法解决循环引用的问题。
2、可达性分析算法
-
概念
通过一些列“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots不可达时,这个对象不可能再被使用。
-
当对象不可达时,将会被第一次标记并且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有override finalize方法,或者finalize方法已经被虚拟机调用过,则该对象被视为没必要执行finalize方法。
finalize方法做了什么?
如果对象override了finalize方法且finalize没有被调用过,那么该对象会被放在F-Queue队列中,GC会对F-Queue队列中的对象进行第二次标记,虚拟机会在稍后调用符合上述条件的finalize,如果在finalize中重新与引用链上的任意一个对象关联(把自己(this)赋值给某个类变量,或者对象的成员变量),则在第二次标记时该对象会被移除出“即将回收”的集合。
任何一个对象的finalize方法都只会被系统自动调用一次。
-
可达性分析需保证一致性
在整个分析期间,不可以出现对象引用关系还在不断变化的情况
知识扫盲
-
什么是GC Roots?
- 虚拟机栈(栈帧中的本地变量表)中引用的对象;
- 方法区中类静态属性引用的对象;
- 方法区中常量引用的对象;
- 本地方法栈中引用的对象。
-
什么可判定为引用?
引用分为4类:强引用,软引用,弱引用,虚引用。
-
强引用(一定不回收)
只要强引用还存在,垃圾回收器永远不会回收掉被引用的对象。
-
软引用(内存溢出之前,列为下次回收)
软引用关联着的对象,只有在将要发生内存溢出异常之前,会把这些对象列进回收范围的第二次回收。
-
弱引用(一定回收)
当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联着的对象。
-
虚引用
-
三、对象已经死去,用什么算法回收呢?
标记—清除算法
-
概念
标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
-
弊端
1)效率低;
2)会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,因为没有足够的连续内存而提前触发另一次垃圾收集。
复制算法
-
概念
将内存分为两块,每次只使用其中的一块,当一块内存用完之后,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
标记—整理算法
-
概念
标记出所有需要回收的对象,在标记完成后,所有活着的对象都向一端移动,然后直接清理掉边界以外的内存。
分年代收集法
-
新生代——复制算法
新生代每次垃圾收集时都有大批对象死去,只有少量存活
-
老年代——标记—清理或标记—整理算法
老年代对象存活率高,没有额外的空间分配担保。
java堆分局对象存活周期的不同分为新生代和老年代。
四、找死去的对象的时候,需要枚举GC Roots,怎么枚举?
- 枚举根节点
在类加载完成的时候,就把对象内什么偏移量上是什么类型的数据计算出来,在JIT编译中,也会在特定的位置记录下栈和寄存器中哪些位置是引用。
- 在安全点执行GC
既然要从分界点找引用链,那么什么时候找?
当然是从根节点(GC Roots)找了
安全点的选定基本上是选循环、跳转、异常的点。
-
GC发生时,所有线程如何跑到安全点停下来?
- 抢先式中断
把所有线程中断,如果有不在安全点点线程,就恢复线程,让他跑到安全点上。
- 主动式中断
当GC需要中断线程时,不直接对线程操作,仅设置一个标志,各个线程执行时主动轮询这个标志,发现中断标志为真时就自动挂起。
五、枚举到了GC Roots,选定了安全点和中断方式,那么用什么收集垃圾呢?
1、Serial 收集器
单线程,复制算法,stop the world,适用于client模式下的虚拟机。
2、ParNew 收集器
Serial 收集器的多线程版本,采用复制算法。
- 并行:多条垃圾收集线程并行工作,但此时用户线程仍处于等待状态;
- 并发:用户线程与垃圾收集线程同时执行(不一定是并行的,可能会交替执行),用户程序在继续执行。
3、Parallel Scavenge 收集器
吞吐量:运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间)
停顿时间越短越适合需要与用户交互的程序,良好的响应速度能提升用户体验
高吞吐量可以高效率地利用cpu时间,尽快完成程序的运算任务,适合在后台运算,不需要与用户有交互的程序
GC停顿时间缩短是以牺牲吞吐量和新生代空间换取的
4、Serial Old 收集器
Serial 收集器的老年代版本,单线程,标记—整理算法。
5、Parallel Old 收集器
ParNew 收集器的老年代版本,多线程,采用标记—整理算法。
6、CMS 收集器
标记—清除算法,并发执行。
以获取最短回收停顿时间为目标的收集器,目前很大一部分的java应用集中在互联网站或者B/S系统的服务端。
分为4个步骤:
-
初始标记——单线程、stop the world
标记GC Roots能直接关联到的对象
-
并发标记——与用户线程一起
从GC Roots开始,对堆中的对象进行可达性分析,找出存活的对象。
-
重新标记——并行、stop the world
修正并发标记期间因程序继续运作而导致标记产生变动的那部分对象的标记记录
-
并发清除——与用户线程一起
弊端:
- 对CPU资源敏感。
- 无法处理浮动垃圾,由于在并发清理阶段还有用户线程,所有还有垃圾不断产生,这部分垃圾无法在当次收集中清理掉。且由于是并发执行,不能像老年代一样,等到内存几乎被填满了再执行,需要留一部分给并发收集时的用户程序。
- 大量碎片空间产生。
7、G1 收集器
整体是标记—整理,局部是复制
特点:
-
并行与并发
-
分代收集
-
空间整合
整体看来是标记—整理算法,局部是复制算法,这两种意味着不会产生大量的空间碎片
-
可预测的停顿
在制定长度的时间偏短哪,消耗在垃圾回收上的时间不超过n毫秒
G1中,java将堆分为多个大小相等的独立区域(Region),G1跟踪Region里面的垃圾堆积的价值大小,在后台维护一个优先列表,根据允许的回收时间,优先回收价值大的Region。
对象间的引用不会进行全堆扫描,而是维护Remembered Set,虚拟机发现程序对Reference类型的数据进行写时,会产生一个Write Barrier中断,检查对象是否在不同的Region中,如果是,就在Remembered Set中记录下来,当内存回收时,在GC根节点枚举中加入Remembered Set,可保证不对全堆扫描。
步骤
-
初始标记——单线程、stop the world
标记GC Roots能直接关联到的对象
-
并发标记
从GC Roots开始,对堆中的对象进行可达性分析,找出存活的对象
-
最终标记——并行 stop the world
修正因并发标记期间程序运行而产生的变动
-
筛选回收