————— 第二天 —————
————————————
下面我们一起来研究这三个问题。
首先我们需要知道如何哪些垃圾需要回收?判断对象是否需要回收有两种算法。一种是引用计数算法、一种是可达性分析算法。
引用计数算法很简单,它通过记录对象被引用的次数从而判断该对象的重要程度。如果该对象被其它对象引用,则它的引用计数加一,如果删除对该对象的引用,那么它的引用计数就减一,当该对象的引用计数为0时,那么该对象就会被回收。
引用计数存在什么问题呢?当有两个对象相互引用时,由于它们互相引用对方所以计数都不为零,这就会导致这两个对象无法回收。
所以,Java虚拟机采用的是另一种方法来判断对象是否存活,它就是可达性分析算法。
可达性分析算法,首先要确定一系列根对象(GC Roots),并从根对象为起点根据对象之间的引用关系搜索出一条引用链(Reference Chain),在引用链的对象就存活,而不在引用链的对象就认定为可回收对象。
有一个比喻十分恰当:可达性分析算法就好比是在清洗葡萄串,我们可以从一根枝提起一大串葡萄,他们就像一串引用链,而没有和引用链相连的对象就像是散落在池子里的葡萄,可以回收。
虚拟机栈中引用的对象(正在运行的方法使用到的变量、参数等)
方法区中类静态属性引用的对象(static关键字声明的字段)
方法区中常量引用的对象,(也就是final关键字声明的字段)
本地方法栈中引用的对象(native方法)
Java虚拟机内部的引用。(系统内部的东西当然能作为根了)
学会判断内存中哪些垃圾需要回收后,我们就需要掌握几个重要的垃圾回收算法。
标记-清除法是最基本的一种垃圾回收算法,总的来说分为两步:
标记
标记所有需要回收的对象(灰色),也就是在做垃圾的判定。
清除
将标记为灰色的部分,清除掉。
需要注意的是:所谓的清除,并不需要真正地把整个内存的字节进行清零操作,只需要把空闲对象的起始结束地址记录下来放入空闲列表里,表示这段内存是空闲的就行。
优点速度快,只需要做个标记就能知道哪一块需要被回收,但是他的缺点也是致命的。
他的主要缺点有两个:一是执行效率不稳定,二是会涉及到内存碎片化的问题。
所谓标记复制算法和标记整理算法,都是对标记清除算法缺点的改进,所以才说标记清除算法是最基础的方式。
与标记-清除算法不同,标记-整理算法是移动式的。他会让所以存活的对象都向内存空间一端移动,然后清除到边界以外的内存。
标记
移动
是什么样的弊端呢?标记-整理算法涉及到了对象的移动,在整理阶段,由于移动了可用对象,需要去更新引用。效率就低了。
标记-复制算法,相比前面的比较不同,他将内存空间分为两块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,然后呢再清除正在使用的内存块中的所有对象。最后再交换两个内存的角色,最后完成垃圾回收。
大体来看可以分为 这么几个步骤:
复制
清空
易位
不难看出,标记复制算法不需要标记算是提升了效率。此外他也不会参数碎片问题。
但是。标记复制算法的缺点也是十分明显的,它需要双倍空间。
既然说JVM虚拟机不会单独采用某种算法,而是会结合三种算法让他们协同工作,其具体的实现就是java虚拟机里的分代垃圾回收机制。
上图所示,就是Java堆内存的划分。为什么需要这么划分区域呢?那是因为我们的java对象寿命都是不同的,有的可能需要长时间使用,而有的可能用完就可以丢去。于是我们可以根据其生命周期的不同特点,进行不同的垃圾回收策略。
举个很好理解的例子:新生代处理垃圾,就像是处理生活日用垃圾,而老年代处理的垃圾,更像是过年大扫除,家里实在太多垃圾了来一次重清理。大扫除清理的垃圾,都是在家中存放时间较长的,往往可能曾经很受用,如今退役了先放着过年再打扫清除掉。
每一次,我们创建一个对象,都会在伊甸园区占据一定内存大小,渐渐地伊甸园就满了。当我们再要创建对象时,就会发现空间不够了。
这时,就会触发一次垃圾回收,新生代触发的垃圾回收有个称呼叫做MinorGC。
MinorGC触发后,伊甸园区就会对各个对象进行可达性分析,从而知道哪些对象应该作为垃圾被清理。
进入幸存区的幸运儿,将会被标记上一个“幸运值”,代表他们抗住了多少次清理。
最后,幸存区to和幸存区from还需要交互一下位置,这里不是指物理位置交换,而是说,它俩的定义发生了交换,下次就是左边那个为幸存区to,右边的为幸存区from了。
换句话说,幸存区to始终是空的。
我们再模拟多几次,加深一下印象:
假如又进来了不少对象,伊甸园又满了!
那就会触发一次MinorGC,把幸存者移步到幸存区to,其他一律清空。
最后别忘了,幸存区from和幸存区to又要再交换一下”位置“。
作者:程序员小灰