jvm提供了垃圾回收器进行垃圾回收,垃圾回收器的职责就是回收内存中不再被引用的对象,以便释放内存。垃圾回收器利用可达性分析算法去分析哪些对象需要被回收,可达性分析算法是这样的:首先一些对象被定义为gc roots,然后沿着这些gc roots对象的引用链往下查找,无法通过gc roots的引用链被查找到的对象即为不可达对象。三色标记是可达性分析算法的一种实现,它包含三种颜色:白色、灰色和黑色,不同颜色有不同的意义,在三色标记结束之后,gc将去回收白色的对象。我们知道,在jvm的内存模型中,堆内存是gc回收的重要区域,因为大部分的对象都是保存在堆中的,而堆又分成了三大部分:新生代、老年代和永久代。新生代和老年代的标记回收算法不同,新生代用标记-复制算法,老年代用标记-清除算法,不论哪种算法,标记都是首先需要做的,只有在标记之后,gc才能够知道哪些对象是需要被回收的。
在讲标记过程之前,先讲一下几个概念:gc roots、三种颜色的意义、新生代、老年代
会被定义为gc roots的对象有:
1、虚拟机栈中引用的对象:虚拟机栈中保存的是一个个的栈桢,每个栈桢中保存的是
它对应的方法的局部变量、部分运行结果、方法出口等信息,局部变量所引用的对象会被
定义为gc roots;
2、本地方法栈中引用的对象:也就是native方法中引用的对象会被定义为gc roots;
3、方法区中类的静态变量和常量引用的对象会被定义为gc roots。
三种颜色的意义:
白色:未被标记的对象
灰色:已被标记,但是还有至少一个直接引用未被标记的对象
黑色:已被标记,并且所有直接引用均已被标记的对象
新生代:新生代中保存的是大部分的新被创建的对象【新创建的对象如果很大的话,不会保存在新生代中,会被保存到老年代中】,这些对象大部分都是朝生夕死,因此在新生代中会有大量的minor gc;新生代又划分为三个区域;Eden区、survivor from区、survivor to区,新生代用标记-复制算法进行minor gc,在执行minor gc的时候,会将Eden区和survivor from区中被标记为存活的对象转移到survivor to区中,然后将Eden区和survivor from区清空,然后将存活的对象年龄加1,如果某个对象年龄达到了15,则将这个对象转移到老年代;年龄计算完毕,将survivor from和survivor to两个区域的功能互换,那么下一次minor gc的时候就是将Eden区和survivor to区的存活对象转移到survivor from区,然后进行年龄的计算了。
老年代:老年代中保存的是年龄比较大的对象以及占用内存比较大的新创建的对象,老年代比较稳定,不会进行大量的major gc,但是当要保存一个比较大的新创建的对象的时候,如果没有足够大的连续内存去保存它,那么会触发major gc,以便腾出空间去保存它;新生代在执行minor gc时有可能导致年龄大的对象转移到老年代,如果这时候老年代没有足够的空间了,那么也会触发老年代执行major gc。老年代用标记-清除算法执行major gc,它会扫描老年代中所有的对象,标记出存活的对象,然后将没有标记的对象回收,当major gc之后依然没有足够的空间去保存新的对象的时候,就会抛出OOM异常。
具体而言,标记回收的过程是这样的:
我们知道,java中的线程分为用户线程和守护线程,用户线程是执行具体任务的线程,守护线程是为用户线程服务的后台线程,当我们启动一个用户线程的时候,jvm会自动地启动一个进行垃圾回收的守护线程,用于回收这个用户线程执行过程中产生的垃圾,这个垃圾回收线程在开始回收对象之前,会对对象进行三色标记,三色标记的过程:
1、初始的时候,除了gc roots对象是黑色的以外,其他所有的对象都是白色的
2、将gc roots对象直接引用的白色对象标记为灰色
3、将灰色对象直接引用的白色对象标记为灰色,并将灰色对象本身标记为黑色
4、如果某个灰色对象已经没有未被标记的直接引用了,那么将这个对象标记为黑色
5、如果依然有灰色对象存在未被标记的直接引用,则重复上面的从3开始的标记步骤,直到没有灰色对象存在
这时只剩下了黑色和白色对象,gc要回收的便是白色对象。
但其实这个标记过程是会有问题的,因为执行三色标记的守护线程是和它对应的用户线程并发执行的,那么就会有并发标记的问题:如果在并发标记过程中,对象的引用关系发生了变化,那么就有可能产生多标和漏标的问题。
多标:如果某个之前被引用的对象a又不被引用了,而引用关系改变时a对象已经被标记为黑色了,那么a对象就被多标了。多标的问题不大,顶多就是a对象在这次的gc中逃逸掉了,那么它可以在下一轮gc中被回收掉
漏标:如果某个白色对象n由被对象a引用变为被对象b引用,并且引用关系改变的时候,a是灰色的,而b是黑色的,那么就会有问题,因为新引用它的b已经是黑色了,所以已经无法通过b来标记对象n了,那么一直到三色标记结束,n对象依然是白色,就会导致对象n被gc回收,这就导致了不该被回收的对象被回收掉了,会使程序出现严重的bug。
解决漏标的问题,可以用写屏障+增量更新方案:
增量更新:当某个黑色的对象black去新引用一个对象obj时,会将这个黑色对象black标记为灰色,这样obj就不会被漏标了。CMS垃圾回收器就是使用了增量更新。
需要注意的是,即便某个对象被标记为不可达【白色】了,它也不一定真的会被回收,它还有可能通过一个方法翻盘,这个方法叫做finalize。
用finalize翻盘:
finalize是定义在Object类中的一个方法,它的作用是在回收对象之前做一些诸如关闭资源的操作,这个方法只可能被执行一次,因为Object类是所有类的父类,所以任何类都可以去重写这个方法。
gc在对对象进行了第一次标记之后,还要去判断这个对象的finalize方法是否有必要执行,只有对象重写了finalize方法并且它的finalize方法还未执行过,它才有必要执行finalize方法;如果某个对象的finalize方法没有必要执行,那么它的命运就决定了,它肯定会被gc回收;如果某个对象的finalize方法有必要执行,那么这个对象会被放在一个专用队列F-Queue中,jvm会启动一个专门的低优先级的线程去执行这个队列中所有对象的finalize方法,如果一个对象在finalize方法中被重新引用了,那么它会被移出F-Queue,也就是它逃避掉了被gc清除的命运;而那些在finalize方法中未被重新引用的对象,在finalize方法执行完毕之后,会被进行第二次标记,这些对象就真的要被清除了。