二、垃圾回收算法
三、垃圾标记阶段
- 堆里存放着几乎所有的Java对象实例,在GC执行之前,首先需要区分出内存中哪些是存活对象,哪些是已死亡对象
- 只有被标记死亡的对象,GC才会在执行时,释放掉其所占用的内存空间
- 当一个对象已经不再被任何的存活对象继续引用时,可以判断为已经死亡
- 包含引用计数算法和可达性分析算法
1. 引用计数算法
1. 每个对象保存一个人整型的引用计数器属性,用于记录对象被引用的情况
1.1 对于一个对象A,只要有任何一个对象引用了A, 则A的引用计数器加1
1.2 当引用失效时,引用计数器减1
1.3 对象A的引用计数器的值为0,则表示对象A不可能再被使用,就可以回收
2. 优点: 实现简单,垃圾对象方便辨识,判定效率高,回收没有延迟
3. 缺点:
3.1 需要单独的字段存储计数器,增加 存储空间的开销
3.2 每次赋值需要更新计数器,伴随着加法和减法,增加了 时间开销
3.3 致命缺陷: 无法处理循环引用的问题, 最终java的垃圾回收器中没有使用该算法
package com.nike.erick.d07;
public class Demo02 {
public static void main(String[] args) {
ErickService service01 = new ErickService();
ErickService service02 = new ErickService();
service01.reference = service02;
service02.reference = service01;
service01 = null;
service02 = null;
System.gc();
}
}
class ErickService {
private int[] arr = new int[5 * 1024 * 1024];
public Object reference;
}
2. 可达性算法
- 根搜索算法,追踪性垃圾收集器
- 简单,执行高效,能有效解决引用计数算法中的循环引用问题,防止内存泄漏的发生
- Java中选择的就是可达性算法
GC Roots: 一组必须活跃的引用
- 以GC Roots为起始点,从上到下的方式, 搜索被根对象集合所链接的目标对象是否可达
- 内存中国的存活对象都会被根对象集合直接或间接的链接, 搜索走过的路径 《引用链》
- 目标对象没有任何引用链相连,则是不可达的,对象已死
- 只有被根对象集合直接或间接链接的对象才是存活对象
2.1 哪些元素可以作为根元素
- 如果一个指针,指向了堆内存的对象,但是该指针又不存在堆中,那么该指针就是一个根 元素
- 虚拟机栈中:各个线程被调用的方法中使用的参数,局部变量中的引用等
- 本地方法栈: 引用的对象
- 方法区中: 类静态属性引用的对象
- 方法区中:常用引用的对象,比如Strign常量池
- 所有被同步锁synchronized持有的对象
同时,如果考虑比如新生代的垃圾回收,那么其他堆空间的对象,也必须作为GC Root的一部分,从而判断
新生代的对象是否是垃圾
- 如果要使用可达性分析算法来判断内存是否可回收,分析工作必须在一个能保障一致性的快照中进行
- 因此GC必须 Stop The World 的一个重要原因,枚举根节点时必须要停顿
3. 分析工具- 分析GC Roots
3.1 JProfile- IDEA插件
四、清除阶段
1. 标记-清除算法
当堆中的有效空间被耗尽的时候,就会STW,然后进行两项工作,标记和清除
- 标记: Collector从引用根节点开始遍历,标记所有 《被引用的对象》,一般是在对象的Header
中记录为可达对象
- 清除: Collector对堆内存从头到尾进行线性的遍历,如果发现某个对象在Header中没有标记为可达对象
则将其回收
优点:
1 . 实现比较简单
缺点:
1. 效率不算高
2. 进行GC的时候,需要STW,导致用户体验差
3. 这种方式清理出来的空闲内存是不连续的,产生内存碎片。需要维护一个空闲列表
并不是真的空, 而是把需要清除的对象地址保存在空闲的地址列表中
下次有新对象需要加载的时候,判断垃圾的位置空间是否足够
如果足够,就存放。 如果不够, 则 oom
2. 复制算法
将活着的内存空间分为两块, 每次只使用其中一块
1. 垃圾回收时,将正在使用的内存中的存活对象复制到未被使用的内存快中
2. 清除正在使用的内存快中的所有对象
3. 交换两个内存的角色,最后完成垃圾回收
优点:
a. 没有标记和清除过程,实现简单,运行高效
b. 复制过去以后保证空间的连续性,不会出现 内存碎片 的问题
缺点:
a. 需要两倍的内存空间
如果系统中的存活对象很多,复制算法需要复制的存活对象数量并不会很大,或者说非常低才行
- 比较适合于朝生夕死的区域
3. 标记-压缩算法
1. 效率低,但是不会存在内存碎片问题
五、收集思想
1. 分代收集
- 每种算法都有好有坏,如何选择一个最合适的算法?
- 不同的对象的生命周期是不一样的
- 不同生命周期的对象可以采取不同的收集方式,以便提高回收效率
- 一般是把Java堆分为新生代和老年代,从而根据不同年代的特点,使用不同的回收算法,以便提高垃圾回收的效率
目前几乎所有的GC都是采用分代收集(Generational Collecting)算法执行垃圾回收的
- 区域相对老年代小,对象生命周期短,存活率低,回收频繁
- 使用复制算法, 同时使用hotspot的两个survivor的设计来缓解内存问题
- 区域较大,对象生命周期长,存活率高,回收不会很频繁
- 可以使用标记-清除算法 或 标记-压缩算法
2. 增量收集算法
- 垃圾回收过程中,程序出于STW状体,如果垃圾回收时间长,则会导致应用程序的长时间卡顿
- 可以让垃圾收集线程和应用线程交替执行
- 每次垃圾收集线程只收集一小片区域的内存空间,切换到应用程序线程
- 反复执行,直到垃圾收集完成
总体来说, 增量收集算法的基础仍然是 标记-清除和复制算法
但是允许垃圾收集线程以分阶段的方式完成标记,清理或复制工作
缺点:
- 垃圾回收,间断性执行了应用程序,所以减少了系统的停顿时间
- 但是线程切换和上下文转换的消耗,使得垃圾回收的总体成本上升,造成系统吞吐量的下降
3. 分区算法
- 相同条件下,堆空间越大,一次GC需要的时间就越长,产生的STW也就越大
- 将一块大的内存区域分割为多个小块,根据目标的停顿时间,每次合理的回收若干个小区间
而不是整个堆空间,从而减少一次GC所产生的停顿
- 分代算法按照对象的生命周期划分为两部分
- 分区算法将整个堆空间划分为多个连续的不同的小区间 region
- 每个小区间都独立使用,独立回收