所谓垃圾回收就是指将JVM中无“人”使用的对象清理掉,以达到节省内存空间的作用。先找到、后清理
一、垃圾标记阶段:对象存活判断
在GC执行垃圾回收之前,首先需要区分出内存中哪些是存活对象,哪些是已经死亡的对象。只有被标记为已经死亡的对象,GC才会被回收。
1、引用计数算法(java中未使用)
对每个对象保存一个整形的引用计数器属性。用于记录对象被引用的情况。只要有一个对象引用了A,计数器则加1;当引用失效时计数器减1。计数器为0时,表示对象A不可能再被使用可进行回收
优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
缺点:
- 它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。
- 引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法
如图栈中有一个引用指向了堆中objB与objC,objB被objA与objC所引用所以计数器为2。objC被objB引用计数器为1。
当栈帧出栈时objA被回收,指向objB的引用也就消失。objB引用计数器变为1,此时已无“人”在使用objB与objC但是因为objB与objC的相互引用导致计数器为1而无法被回收形成了内存泄露。
2. 可达性分析算法(根路径搜索算法、跟踪性垃圾收集)
GC Roots是一组必须活跃的引用
- 可达性算法是以根对象集合为起始点,按照从上至下的方法搜索被根对象集合所连接的目标对象是否可达。
- 内存中存活对象都会被根对象集合直接或间接连接着,搜索所走过的路径被称为引用链
GC Root包含的元素
- 虚拟机栈中引用的对象。主要是各线程被调用的方法中使用到的参数、局部变量
- 本地方法栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 被同步锁synchronized持有的对象
- java虚拟机内部的引用:基本类型对应的Class对象,一些常驻异常对象、系统类加载器
- 虚拟机内部JMXBean、JVMTI中注册的回调、本地代码缓存
虚拟机中的对象状态
如果从所有的根节点都无法访问到某个对象,说明对象己经不再使用了。一般来说,此对象需要被回收。但事实上,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段。一个无法触及的对象有可能在某一个条件下“复活”自己,如果这样,那么对它的回收就是不合理的,为此,定义虚拟机中的对象可能的三种状态。如下:
- 可触及的:从根节点开始,可以到达这个对象。
- 可复活的:对象的所有引用都被释放,但是对象有可能在finalize()中复活。
- 不可触及的:对象的finalize()被调用,并且没有复活,那么就会进入不可触及状态。不可触及的对象不可能被复活,因为finalize()只会被调用一次。
以上3种状态中,是由于finalize()方法的存在,进行的区分。只有在对象不可触及时才可以被回收。
对象的finalization机制(刀下留人)
java提供了对象终止机制(finalization)来允许开发人员对对象销毁之前进行自定义处理。通常进行一些资源释放等工作
对象被GC之前会先调用对象的finalization方法。
判定一个对象objA是否可回收,至少要经历两次标记过程:
1.如果对象objA到GC Roots没有引用链,则进行第一次标记。
2.进行筛选,判断此对象是否有必要执行fina1ize()方法
① 如果对象objA没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,objA被判定为不可触及的。
② 如果对象objA重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法执行。
③ finalize()方法是对象逃脱死亡的最后机会,稍后Gc会对F-Queue队列中的对象进行第二次标记。如果objA在finalize()方法中与引用链上的任何一个对象建立了联系,那么在第二次标记时,objA会被移出“即将回收”集合。之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize方法只会被调用一次。
不可以主动调用finalization方法
- 在调用finalize时可能导致对象复活,也就是说对于路径不可达的对象在finalize方法中有可能产生新的指向引用导致变的“可达”了
- finalize执行实现是没有保障的,他不是立即生效的
- 不好的finalize写法会影响GC性能,本应死亡的对象可能因为不好的finalize写法导致莫名其妙的“复活”产生内存泄露
二、垃圾清除阶段(标-清、复制、标-压)
1、标记-清除算法
当堆中有效内存空间被耗尽时,就会停止整个程序(STW:stop the world),然后进行两项工作,一是标记,二是清除
- 标记:从引用根节点开始遍历,标记所有被应用对象(可达对象)。
- 清除:对堆内存从头到尾进行线性遍历,如果发现对象在其header中未标记,则进行回收
优点:
- 简单易理解
缺点:
- 效率不高
- 进行GC时,需要停止整个应用程序,导致用户体验差
- 清理出的空闲内存是不连续的,产生内存碎片。需要维护一个空闲列表。
衍生规则之动态分区分配策略
因为标记清除后会产生大量内存碎片,所以对这些内存碎片的再分批就产生了不同实现方式
- 首次适应算法(Fisrt-fit)
在分配对象时遍历空闲链表,一旦发现有大于等于待分配对象的大小时,就把该内存碎片分配给对象,并停止向下遍历 - 最佳适应算法(Best-fit)
在分配对象时,遍历整个空闲链表,寻找最适合的内存空间进行分配 - 最差适应算法
在分配对象时,遍历空闲连表查找最大的内存碎片,将其分隔为合适大小然后分配对象,但这种方法会产生大量内存碎片
清除?是覆盖
这里的清除并不是置空,而是把需要清除的对象地址保存在空闲的地址列表里。下次有对象需要加载时,判断垃圾的位置空间是否够,如果够就进行覆盖。
2、复制算法
使用案例:新生代的幸存者区
将存活的空间分为两块,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,最后完成垃圾回收。
优点:
- 没有标记与清除过程,实现简单,运行高效
- 复制过去之后保证控空间的连续性,不会出现碎片问题
缺点:
- 需要两倍的内存空间
- 复制后需要改变大量栈帧中引用对象的指向地址
- 存活对象越多效率越低
3、标记-整理(压缩)算法
- 标记:和清除算法一样,从根节点开始标记所有被引用对象
- 压缩:将所有存活对象压缩到内存的一段,按顺序排放
从效果上讲与标记-清除算法相似,增加了碎片整理的部分
优点:
- 消除了标记-清除算法当中,内存区域分散的缺点,我们需要给新对象分配内存时,VM只需要持有一个内存的起始地址即可。
- 消除了复制算法当中,内存减半的高额代价。
缺点:
- 从效率上来说,标记-整理算法要低于复制算法。
- 移动对象的同时,如果对象被其他对象引用,则还需要调整用的地址。
- 移动过程中,需要全程暂停用户应用程序。即:STW
整理算法
随机整理
对象的移动方式和它们初始的对象排列及引用关系无关
双指针回收算法
头尾指针向中间靠拢,头指针标记待清理内存区域,尾指针标记有效对象区域
尾指针指向的有效对象向头指针指向的待清理内存区域覆盖
第一次遍历后对象的内存地址发生了变化,进行第二次遍历修改GC root指向的内存地址
总结
任意顺序整理实现简单,且执行速度快,但任意顺序可能会将原本相邻的对象打乱到不同的高速缓存行或者是虚拟内存页中(理解为打乱到内存各个地方),会降低赋值器的局部性。 包括他只能处理固定大小的对象,一旦对象大小不固定,就会增加其他的逻辑。
线性整理
将具有关联关系的对象排列在一起
根据GC Root链进行整理,相关的对象整理在一起,整理成一块块小区域,但是会造成内存碎片
滑动整理
将对象“滑动”到堆的一端,从而“挤出”垃圾,可以保持对象在堆中原有的顺序
Lisp2整理算法
第一次变量,scan指针向右移动,计算有效对象的个数,free指针就移动对应的次数
总结
所有现代的标记-整理回收器均使用滑动整理,它不会改变对象的相对顺序,也就不会影响赋值器的空间局部性。复制式回收器甚至可以通过改变对象布局的方式,将对象与其父节点或者兄弟节点排列的更近以提高赋值器的空间局部性。
限制
整理算法的限制,如任意顺序算法只能处理单一大小的对象,或者针对大小不同的对象需要分批处理;整理过程需要2次或者3次遍历堆空间;对象头部可能需要一个额外的槽来保存迁移的信息。
对比
三、算法思想
分代收集算法
不同生命周期的对象采取不同的收集方式,以便提高回收效率
young区就是分代收集思想的实现
- 年轻代
年轻代对象生命周期短、存活率低、回收频繁,所以考虑复制算法。 - 老年代
区域较大,对象生命周期长,存活率高,回收不频繁,一般采用标记-清除和标记-压缩算法混合的方式
分代回收三大假说
弱分代假说:绝大多数对象朝生夕死
强分代假说:活得越久的对象,也就是熬过很多次垃圾回收的对象是越来越难以消亡的
跨代引用假说:既然有跨代引用,其实虚拟机就要扫描整个老年代去发现是否存在跨代引用的。这是一个比较原始的想法。基于跨代引用假说,我们有理由不扫码整个老年代,因为跨代引用极其的少,而应该维护一个全局的数据结构,在这个数据结构中记录哪些是被老年代的对象引用的,在后面发生Minor GC时,这些引用了新生代的
老年代对象就会被加入到GC Roots进行扫描。
增量收集算法
垃圾回收时会经历STW,所有线程挂起,暂定一切工作等待垃圾回收,严重影响用户体验或系统稳定。增量收集是让垃圾收集线程与应用线程交替执行。垃圾收集线程只收集一小片区域的内存空间,接着切换到应用程序线程。
Parallel Scavenge
缺点
增加线程切换和上下文切换的消耗会造成垃圾回收的总体成本上升,造成系统吞吐量下降。
分区算法
堆空间越大一次GC时间越长,有关GC产生的停顿也就越长。根据目标的停顿时间每次合理回收若干个小区间,