专栏原创出处:github-源笔记文件 (opens new window) ,github-源码 (opens new window),欢迎 Star,转载请附上原文出处链接和本声明。
Java JVM-虚拟机专栏系列笔记,系统性学习可访问个人复盘笔记-技术博客 Java JVM-虚拟机 (opens new window)
# 一、前言
本节内容主要内容为:
为什么收集器要分代
如何分代
内存分配策略
内存回收策略
常见的垃圾回收算法及应用场景分析
因垃圾收集器不断发展,该内容只是针对常见的收集器分代模式介绍,具体以收集器实现细节为主。
# 二、分代收集
1)为什么要分代
- 绝大多数对象都是朝生夕灭的。(年轻代)
- 熬过越多次垃圾收集过程的对象就越难以消亡。(老年代)
- 跨代引用相对于同代引用来说仅占极少数。(跨代引用较少)
- 垃圾收集器可以每次只回收其中某一个或者某些部分的区域。(按需收集)
- 针对不同的区域安排不同的的垃圾收集算法(标记-复制算法、标记-清除算法、标记-整理算法)。
2)如何分代
针对上面分代的经验法则,「多数商业虚拟机」在 Java 堆划分出不同的区域(年轻代、老年代)。
- 年轻代 ,又划分为 Eden 和 Survivor 2 个区域
- 老年代
3)分代收集种类
在虚拟机中一般根据 GC 作用及位置划分为以下几种:
- 部分收集(Partial GC):指目标不是完整收集整个 Java 堆,其中又分为:
- 年轻代收集(Minor GC/Young GC):只是年轻代的垃圾收集。
- 老年代收集(Major GC/Old GC):只是老年代的垃圾收集。目前只有 CMS 收集器会有单独收集老年代的行为。
- 混合收集(Mixed GC):收集整个年轻代以及部分老年代的垃圾收集。目前只有 G1 收集器会有这种行为。
- 整堆收集(Full GC):收集整个 Java 堆和方法区的垃圾。
请注意「Major GC」这个说法现在有点混淆,需按上下文区分到底是指老年代的收集还是整堆收集。
# 三、内存分配策略
大多数情况下,对象在年轻代 Eden 区中分配
大对象直接进入老年代
HotSpot 虚拟机提供了-XX:PretenureSizeThreshold 参数,指定大于该设置值的对象直接在老年代分配,这样做的目的就是避免在 Eden 区及两个 Survivor 区之间来回复制,产生大量的内存复制操作长期存活的对象将进入老年代
对象通常在 Eden 区里诞生,如果经过第一次 Young GC 后仍然存活,并且能被 Survivor 容纳的话,该对象会被移动到 Survivor 空间中,并且将其对象年龄设为 1 岁。对象在 Survivor 区中每熬过一次 Young GC,年龄就增加 1 岁,当它的年龄增加到一定程度(默认为 15),就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold 设置。动态对象年龄判定
为了能更好地适应不同程序的内存状况,HotSpot 虚拟机并不是永远要求对象的年龄必须达到-XX:MaxTenuringThreshold 才能晋升老年代,如果在 Survivor 空间中相同年龄所有对象大小的总和大于 Survivor 空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到-XX:MaxTenuringThreshold 中要求的年龄。
# 四、内存回收策略
1)Young GC
年轻代中的 Eden 区分配满的时候触发。注意 Young GC 中有部分存活对象会晋升到老年代,所以 Young GC 后老年代的占用量通常会有所升高
2)Old GC
目前只有 CMS 收集器会有单独收集老年代的行为
3)Full GC
在发生 Young GC 之前,虚拟机必须先检查老年代最大可用的连续空间是否大于年轻代所有对象总空间
- 如果条件成立,那这一次 Young GC 可以确保是安全的
- 如果条件不成立,则虚拟机会先查看-XX:HandlePromotionFailure 参数的设置值是否允许担保失败
- 如果允许担保失败,继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小
- 如果大于,将尝试进行一次 Young GC
- 如果小于,改为进行一次 Full GC
- 如果不允许担保失败,改为进行一次 Full GC
- 如果允许担保失败,继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小
元空间分配时没有足够空间时,触发 Full GC
System.gc 方法、heap dump 时带 GC,默认触发 Full GC
# 五、收集算法
# 标记-清除算法
- 「标记」首先标记出所有需要回收的对象,也可以反过来,标记存活的对象。标记过程就是对象是否属于垃圾的判定过程。
- 「清理」清理回收
主要优点:
- 不需要移动对象
主要缺点:
- 执行效率不稳定,如果 Java 堆中包含大量对象,其中大部分是需要被回收的,这时必须进行大量标记和清除的动作
- 存空间的碎片化问题,标记、清除之后会产生大量不连续的内存碎片,内存碎片太多可能会导致当以后在程序运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作
# 标记-复制算法
- 「内存切分」它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。
- 「复制-清理」当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。
主要优点:
- 分配内存时也就不用考虑有空间碎片的复杂情况
主要缺点:
- 算法将会产生大量的内存间复制的开销
- 可用内存缩小为了原来的一半
老年代对象存活率高,一般不会采用此算法
# 标记-整理算法
- 「标记」首先标记出所有需要回收的对象,也可以反过来,标记存活的对象。标记过程就是对象是否属于垃圾的判定过程。
- 「整理」让存活的对象都向内存空间一端移动(将存活的与要死亡对象的分开)
- 「清理」清理边界以外的内存
主要优点:
- 分配内存时也就不用考虑有空间碎片的复杂情况
主要缺点:
- 移动存活对象需要更新引用「Stop The World」
关于移动对象的影响:
- 从延迟时间来看,不移动对象停顿时间会更短,甚至可以不需要停顿。
- 从整个程序的吞吐量来看,移动对象会更划算。
# 算法示意图
# 算法总结与应用场景
标记-清除 | 标记-复制 | 标记-整理 | |
---|---|---|---|
速度 | 中等 | 最快 | 最慢 |
空间使用率 | 100% | 50% | 100% |
空间碎片 | 会 | 不会 | 不会 |
移动对象 | 否 | 是 | 是 |
年轻代中的对象存活率较低,一般使用复制算法,因为其时间开销与存活对象的大小成正比,如果存活对象很少,它就非常快;而且年轻代本身应该比较小,就算需要 2 倍空间也只会浪费不太多的空间
老年代被对象存活率可能会很高,而且假定可用剩余空间不太多,这样复制算法就不太合适,于是更可能选用另两种算法,特别是不用移动对象的清除算法
无论哪种算法,内存碎片的问题需要解决,而且仅有「清除算法」会产生内存碎片
- 因此收集器可能会搭配其他的收集器来进行内存整理(CMS 搭配 Serial Old 进行 Full GC)
- 或者在 Full GC 时进行整理。HotSpot VM 的 Full GC 时使用整理算法实现(待完全确认)
具体收集器使用算法以实际为主,该处只是做应用场景分析
# 总结
因为程序对象大多数时候朝生夕灭、少数情况越活越久、活的时间长的对活的时间短的依赖较少我们可以分代管理他们
针对不同的分代对象,我们可以使用不同的特征算法来收集
垃圾回收算法本身比较复杂,感兴趣的朋友可以深入研究
算法记忆的建议(d/c/s):
- 标记-清除,find-delete
- 标记-复制,find-copy
- 标记-整理,find-sort
# 参考
- 《深入理解 Java 虚拟机:JVM 高级特性与最佳实践(第 3 版)》周志明 著
- Major GC 和 Full GC 的区别是什么?触发条件呢?R 的回答 (opens new window)
- 并发垃圾收集器(CMS)为什么没有采用标记-整理算法来实现?R 的回答 (opens new window)
← 对象的死亡过程 JDK11-7个垃圾收集器 →