1. 概要
GC(Garbage Collection)
Java与C/C++等语言最大的技术区别:C语言申请/释放空间malloc/free,C++申请/释放空间new/delete,而Java是自动化的垃圾回收机制(GC)
栈:栈中的生命周期是跟随线程,所以一般不需要关注
堆:堆中的对象是垃圾回收的重点
方法区/元空间:这一块也会发生垃圾回收,不过这块的效率比较低,一般不是我们关注的重点
官网链接https://docs.oracle.com/en/java/javase/13/gctuning/parallel-collector1.html#GUID-5A7866BE-59DF-44AD-B51A-274DE3F2BF59
2.判断对象的存活
(1)可达性分析(Java中使用)
来判定对象是否存活的。这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
作为GC Roots的对象包括下面几种:
- 当前虚拟机栈中局部变量表中的引用的对象
- 当前本地方法栈中局部变量表中的引用的对象
- 方法区中类静态属性引用的对象
- 方法区中的常量引用的对象
(2)引用计数法
给对象添加一个引用计数器,当对象增加一个引用时计数器加 1,引用失效时计数器减 1。引用计数为 0 的对象可被回收。(Python在用,但主流虚拟机没有使用)
优点:快,方便,实现简单。
缺陷:对象相互引用时(A.instance=B同时B.instance=A),很难判断对象是否该回收。
finalize可以完成对象的拯救,但是JVM不保证一定能执行。
3.各种引用(Reference)
强引用
一般的Object obj = new Object() ,就属于强引用。
(如果有GCroots的强引用)垃圾回收器绝对不会回收它,当内存不足时宁愿抛出 OOM 错误,使得程序异常停止,也不会回收强引用对象。
软引用 SoftReference
垃圾回收器在内存充足的时候不会回收它,而在内存不足时会回收它。
弱引用 WeakReference
垃圾回收器在扫描到该对象时,无论内存充足与否,都会回收该对象的内存。
虚引用 PhantomReference
幽灵引用,最弱,被垃圾回收的时候收到一个通知
如果一个对象只具有虚引用,那么它和没有任何引用一样,任何时候都可能被回收。
虚引用主要用来跟踪对象被垃圾回收器回收的活动
4.分代回收
当前商业虚拟机的垃圾回收器,大多遵循“分代收集”的理论来进行设计,这个理论大体上是这么描述的: 1、 绝大部分的对象都是朝生夕死。
2、 熬过多次垃圾回收的对象就越难回收。 根据以上两个理论,朝生夕死的对象放一个区域,难回收的对象放另外一个区域,这个就构成了新生代和老年代。
5.GC 分类
a、 新生代回收(MinorGC/YoungGC)
指只是进行新生代的回收。
特点: 发生在新生代上,发生的较频繁,执行速度较快
触发条件: Eden区空间不足,空间分配担保
b、 老年代回收(MajorGC/OldGC)
指只是进行老年代的回收。
目前只有 CMS 垃圾回收器会有这个单独的回收老年代的行为。
(MajorGC 定义是比较混乱,有说指是老年代,有的说是做整个堆的收集,这个需要你根据别人的场景来定,没有固定的说法)
c、 整堆回收(FullGC)
收集整个 Java 堆和方法区(注意包含方法区)
特点:主要发生在老年代上(新生代也会回收),较少发生,执行速度较慢
触发条件:
- 调用 System.gc()
- 老年代区域空间不足
- 空间分配担保失败
- JDK 1.7 及以前的永久代(方法区)空间不足
- CMS GC处理浮动垃圾时,如果新生代空间不足,则采用空间分配担保机制,如果老年代空间不足,则触发Full GC**
6.垃圾回收算法
(1) 复制算法(Copying)
优点
- 简单高效,不会出现内存碎片问题
缺点
- 内存利用率低,只有一半
- 存活对象较多时效率明显会降低
新生代中的对象90%是“朝生夕死”的,所以一般来说回收占据10%的空间够用了,所以并不需要按照1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor[1]。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。
HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。
*新生代Eden from to 8:1:1
Java中大部分对象朝生夕死。10%
10%的对象不需要回收 10%(from) + 10%(to)(预留)
*
(2)标记-清除算法(Mark-Sweep)
优点
- 利用率百分之百
缺点
- 标记和清除的效率都不高(比对复制算法)
- 会产生大量的不连续的内存碎片
(3)标记-整理算法(Mark-Compact)
优点
- 利用率百分之百
- 没有内存碎片
缺点
- 标记和清除的效率都不高
- 效率相对标记-清除要低
7.垃圾回收器
并行:垃圾收集的多线程的同时进行。
并发:垃圾收集的多线程和应用的多线程同时进行。
简单的垃圾回收器
(1)单线程收集
Serial/Serial Old
最古老的,单线程,独占式,成熟,适合单CPU 服务器
-XX:+UseSerialGC 新生代和老年代都用串行收集器
-XX:+UseParNewGC 新生代使用ParNew,老年代使用Serial Old
-XX:+UseParallelGC 新生代使用ParallerGC,老年代使用Serial Old
(2)多线程收集
ParNew
和Serial基本没区别,唯一的区别:多线程,多CPU的,停顿时间比Serial少
-XX:+UseParNewGC 新生代使用ParNew,老年代使用Serial Old
除了性能原因外,主要是因为除了 Serial 收集器,只有它能与 CMS 收集器配合工作。
Parallel Scavenge(ParallerGC)/Parallel Old
关注吞吐量的垃圾收集器,高吞吐量则可以高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。
所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那有吞吐效率就是99%。
CMS垃圾回收器
收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用集中在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。CMS收集器就非常符合这类应用的需求。
-XX:+UseConcMarkSweepGC ,一般新生代使用ParNew,老年代的用CMS
从名字(包含“Mark Sweep”)上就可以看出,CMS收集器是基于“标记—清除”算法实现的,它的运作过程相对于前面几种收集器来说更复杂一些。
Concurrent Mark Sweep 标记清除算法
- 1.初始标记
仅仅只是标记一下 GC Roots 能直接关联到的对象,速度很快,需要停顿(STW -Stop the world)。
- 2.并发标记
从GC Root 开始对堆中对象进行可达性分析,找到存活对象,它在整个回收过程中耗时最长,不需要停顿。
- 3.重新标记
为了修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,需要停顿(STW)。这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。
- 4.并发清除
不需要停顿。
CMS中的问题
- CPU敏感 (在并发标记阶段用户线程执行的过程中gc线程也在运行,抢占cpu)
- 浮动垃圾 (并发清理阶段用户线程也在运行,此阶段有可能会产生垃圾)
- 内存碎片 (回收算法采用标记清除算法,会产生内存碎片)
优点
由于整个过程中耗时最长的并发标记和并发清除过程收集器线程都可以与用户线程一起工作,所以,从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
缺点
CPU资源敏感:因为并发阶段多线程占据CPU资源,如果CPU资源不足,效率会明显降低。
浮动垃圾:由于CMS并发清理阶段用户线程还在运行着,伴随程序运行自然就还会有新的垃圾不断产生,这一部分垃圾出现在标记过程之后,CMS无法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。
由于浮动垃圾的存在,因此需要预留出一部分内存,意味着 CMS 收集不能像其它收集器那样等待老年代快满的时候再回收。
在1.6的版本中老年代空间使用率阈值(92%)
如果预留的内存不够存放浮动垃圾,就会出现 Concurrent Mode Failure,这时虚拟机将临时启用 Serial Old 来替代 CMS。
会产生空间碎片:标记 - 清除算法会导致产生不连续的空间碎片
并发垃圾回收器 G1
-XX:+UseG1GC 使用G1垃圾回收器
内部布局改变
G1 把堆划分成多个大小相等的独立区域(Region),新生代和老年代不再物理隔离。
算法:标记—整理 (old,humongous) 和复制回收算法(survivor)。
GC模式
- Young GC
选定所有新生代里的Region。通过控制新生代的region个数,即新生代内存大小,来控制young GC的时间开销。(复制回收算法)
- Mixed GC
选定所有新生代里的Region,外加根据global concurrent marking统计得出收集收益高的若干老年代Region。在用户指定的开销目标范围内尽可能选择收益高的老年代Region。
Mixed GC不是full GC,它只能回收部分老年代的Region。如果mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用serial old GC(full GC)来收集整个GC heap。所以我们可以知道,G1是不提供full GC的。
全局并发标记(global concurrent marking)
- 初始标记
仅仅只是标记一下GC Roots 能直接关联到的对象,并且修改TAMS(Nest Top Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可以的Region中创建对象,此阶段需要停顿线程(STW),但耗时很短。
- 并发标记
从GC Root 开始对堆中对象进行可达性分析,找到存活对象,此阶段耗时较长,但可与用户程序并发执行。
- 最终标记
为了修正在并发标记期间因用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程的 Remembered Set Logs 里面,最终标记阶段需要把 Remembered Set Logs 的数据合并到 Remembered Set 中。这阶段需要停顿线程(STW),但是可并行执行。
- 筛选回收
首先对各个 Region 中的回收价值和成本进行排序,根据用户所期望的 GC 停顿时间来制定回收计划。此阶段其实也可以做到与用户程序一起并发执行,但是因为只回收一部分 Region,时间是用户可控制的,而且停顿用户线程将大幅度提高收集效率。
特点
空间整合:不会产生内存碎片
算法:标记—整理 (humongous) 和复制回收算法(survivor)。
可预测的停顿:
G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这也就是Garbage-First名称的来由)。这种使用Region划分内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。
G1把内存“化整为零”的思路
Stop The World现象
GC收集器和我们GC调优的目标就是尽可能的减少STW的时间和次数。