垃圾回收
垃圾回收
判断一个对象是否可被回收
1. 引用计数
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效减 1。引用计数为 0 的对象可被回收
两个对象出现循环引用,此时计数器永远不为 0,导致无法回收
正因为循环引用的存在,因此 Java 虚拟机不适用引用计数算法
2. 可达性分析
通过 GC Root 作为起始点进行搜索,能够到达的对象都是存活的,不可达的对象可被回收
Java 虚拟机用该算法判断对象是否可被回收,一般包含以下内容
- 虚拟机栈引用的对象
- 本地方法栈中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中的常量引用的对象
3. 方法区的回收
主要是堆常量池的回收和类卸载
类卸载的条件很多,需要满足以下三个条件,并且满足了也不一定被卸载
- 该类所有实例都已经被回收,也就是堆中不存在该类实例
- 加载该类的 ClassLoader 已经被回收
- 该类对应的 Class 对象没有在任何地方被引用
可以通过 -Xnoclassgc 参数控制是否堆类进行卸载
4. finalize()
finalize() 类似 c++ 的析构函数,用来做关闭外部资源等工作。但是 try-finally 等方式可以做得更好,并且该方法运行代价高昂,不确定性大,无法保证各个对象的调用顺序,因此最好不使用
当一个对象可被回收时,如果需要执行该对象的 finalize() 方法,那么就有可能通过在该方法中让对象重新被引用,从而实现自救。自救只能进行一次,如果回收的对象之前调用了 finalize() 方法自救,后面回收时不会调用 finalize() 方法。
引用类型
无论是通过引用计数算法还是可达性算法,判定对象是否可被回收都与引用有关
Java 具有四种强度不同的引用
1. 强引用
被强引用关联的对象不会被回收
1 | Object obj = new Object(); |
2. 软引用
被软引用关联的对象只有在内存不够时被回收
1 | Object obj = new Object(); |
3. 弱引用
被弱引用关联的对象一定会被回收,也就是说它只能存活到下一次垃圾回收发生之前。
1 | Object obj = new Object(); |
4. 虚引用
又称为幽灵引用或者幻影引用。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用取得一个对象。
1 | Object obj = new Object(); |
垃圾回收算法
1. 标记-清除
将存活对象进行标记,然后清理掉未被标记的对象
不足:
- 标记和清除过程效率不高
- 会产生大量不连续的内存碎片,导致无法给大对象分配内存
2. 标记-整理
让所有存活的对象都向一端移动,然后直接清理掉端边界外的内存
3. 复制
将内存一分为二,每次只使用其中一块,当这一块内存用完了就将还存活的对象复制到另一块上面,然后再把使用过的内存空间进行一次清理。
主要不足是只使用了内存的一半。
现在的商业虚拟机都采用这种收集算法来回收新生代,但是并不是将新生代划分为大小相等的两块,而是分为一块较大的 Eden 空间和两块较小的 Survivor 空间,每次使用 Eden 空间和其中一块 Survivor。在回收时,将 Eden 和 Survivor 中还存活着的对象一次性复制到另一块 Survivor 空间上,最后清理 Eden 和使用过的那一块 Survivor。
4. 分代收集
根据对象存活周期将内存划分为几块,不同块采用适当的收集算法
一般将堆分为新生代和老年代
- 新生代使用 复制算法
- 老年代使用 标记-清除 或 标记-整理
垃圾收集器
CMS 收集器
CMS(Concurrent Mark Sweep),Mark Sweep 指的是标记-清除算法
四个流程
- 初始标记:仅标记 GC Root 能直接关联到的对象,需要停顿
- 并发标记:进行 GC Root Tracing 的过程,在整个回收过程中耗时最长,不需要停顿
- 重新标记:修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象标记记录,需要停顿
- 并发清除:不需要停顿
在整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,不需要进行停顿。
以下缺点:
- 吞吐量低: 低停顿时间是以牺牲吞吐量为代价的,导致 CPU 利用率不够高。
- 无法处理浮动垃圾,可能出现 Concurrent Mode Failure。浮动垃圾是指并发清除阶段由于用户线程继续运行而产生的垃圾,这部分垃圾只能到下一次 GC 时才能进行回收。
- 标记 - 清除算法导致的空间碎片,往往出现老年代空间剩余,但无法找到足够大连续空间来分配当前对象,不得不提前触发一次 Full GC。
G1 收集器
G1(Garbage-First),它是一款面向服务端应用的垃圾收集器,在多 CPU 和大内存的场景下有很好的性能
其它收集器进行收集的范围都是整个新生代或者老年代,而 G1 可以直接对新生代和老年代一起回收
G1采用了分区(Region)的思路,将整个堆空间分成若干个大小相等的内存区域,每次分配对象空间将逐段地使用内存。因此,在堆的使用上,G1并不要求对象的存储一定是物理上连续的,只要逻辑上连续即可;每个分区也不会确定地为某个代服务,可以按需在年轻代和老年代之间切换。启动时可以通过参数-XX:G1HeapRegionSize=n可指定分区大小(1MB~32MB,且必须是2的幂),默认将整堆划分为2048个分区
通过引入 Region 的概念,从而将原来的一整块内存空间划分成多个的小空间,使得每个小空间可以单独进行垃圾回收。这种划分方法带来了很大的灵活性,使得可预测的停顿时间模型成为可能。通过记录每个 Region 垃圾回收时间以及回收所获得的空间(这两个值是通过过去回收的经验获得),并维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的 Region。
每个 Region 都有一个 Remembered Set,用来记录该 Region 对象的引用对象所在的 Region。通过使用 Remembered Set,在做可达性分析的时候就可以避免全堆扫描。
流程
- 初始标记
- 并发标记
- 最终标记 :修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程,但是可并行执行
- 筛选回收 : 首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率
特点:
- 空间整合:整体看是基于「标记-整理」算法实现,从局部(两个 Region 之间) 基于「复制」算法实现,这意味着不会产生空间碎片
- 可预测的停顿:能让使用者明确指定在一个长度为 M 毫秒的时间片内,消耗在 GC 上的时间不超过 N 毫秒
内存分配策略
1. 对象优先分配在 Eden 区
当 Eden 区空间不够时,发起 Minor GC
2. 大对象直接进入老年代
-XX:PretenureSizeThreshold ,大于此值的对象直接在老年代分配,避免在 Eden 区和 Survivor 区之间的大量内存复制。
3. 长期存活的对象进入老年代
为对象定义年龄计数器,对象在 Minor GC 依然存活会移动到 S区,年龄增加 1 岁,增加到一定年龄就移动到老年代中
-XX:MaxTenuringThreshold 用来定义年龄的阈值
4. 动态对象年龄判断
虚拟机并不是永远地要求对象的年龄必须达到 MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 中相同年龄所有对象大小的总和大于 Survivor 空间的一半,则年龄大于或等于该年龄的对象可以直接进入老年代,无需等到 MaxTenuringThreshold 中要求的年龄
5. 空间分配担保
在发生 Minor GC 之前,虚拟机先检查老年代最大可用的连续空间是否大于新生代所有对象总空间,如果条件成立的话,那么 Minor GC 可以确认是安全的
如果不成立的话虚拟机会查看 HandlePromotionFailure 设置值是否允许担保失败,如果允许那么就会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小,如果大于,将尝试着进行一次 Minor GC;如果小于,或者 HandlePromotionFailure 设置不允许冒险,那么就要进行一次 Full GC
6. Full GC 条件
对于 Minor GC,其触发条件非常简单,当 Eden 空间满时,就将触发一次 Minor GC。而 Full GC 则相对复杂,有以下条件
1. 调用System.gc()
只是建议虚拟机执行 Full GC,但是虚拟机不一定执行
2. 老年代空间不足
老年代空间不足的常见场景为前文所讲的大对象直接进入老年代、长期存活的对象进入老年代等
为了避免以上原因引起的 Full GC,应当尽量不要创建过大的对象以及数组。除此之外,可以通过 -Xmn 虚拟机参数调大新生代的大小,让对象尽量在新生代被回收掉,不进入老年代。还可以通过 -XX:MaxTenuringThreshold 调大对象进入老年代的年龄,让对象在新生代多存活一段时间
3. 空间分配担保失败
使用复制算法的 Minor GC 需要老年代的内存空间作担保,如果担保失败会执行一次 Full GC
4. jdk 1.7 及以前永久代空间不足
5. Concurrent Mode Failure
执行 CMS GC 的过程中同时有对象要放入老年代,而此时老年代空间不足(可能是 GC 过程中浮动垃圾过多导致暂时性的空间不足),便会报 Concurrent Mode Failure 错误,并触发 Full GC
详解 G1 和 CMS
todo