概述
- 垃圾: 指的是在运行程序中没有任何指针指向的对象
- 垃圾回收目的: 为了及时清理空间使得程序可以正常运行
- 垃圾回收机制: JVM采取的是自动内存管理,即JVM负责对象的创建以及回收,将程序员从繁重的内存管理释放出来,更加专注业务的开发
- 垃圾回收区域: 频繁收集Young区(新生代),较少收集Old区(老年代),基本不动永久代/元空间
- 垃圾回收算法
- 标记阶段
- 清除阶段
- 分代收集算法
- 增量收集算法
- 分区算法
-
垃圾回收finalization机制
- java提供了对象终止(finalization)机制允许开发人员提供对象被销毁前的自定义处理逻辑
- 当垃圾对象被回收之前一定会调用finalize()方法
- 不要主动调用对象的finalize方法,交由垃圾回收机制执行,原因如下
- 极端情况下,若没发生GC,则finalize将不会执行
- finalize方法中可以使得对象复活
- finalize方法影响GC性能
- 由于finalize方法的存在,JVM中的对象一般处于三种可能的状态,且只有不可触及的对象才会被垃圾回收
- 可触及的: 从根节点开始,可以到达这个对象
- 可复活的: 对象的引用都被释放,但有可能在finalize方法中复活
- 不可触及的: 对象没有在finalize方法中复活,即进入不可触及状态,不可触及状态的对象不可能被复活,因为finalize方法只会被调用一次
- System.gc:会显示触发Full GC,但无法保证垃圾收集器实时调用,开发人员一般不会主动调用
- 内存溢出: 没有可用的内存,并且垃圾收集器无法提供更多的内存(即无法有效回收内存空间),没有空闲的内存情况原因如下
- Java虚拟机堆内存设置不足(可通过-Xms、-Xmx来调整)
- 代码中创建了大量大对象,并且长时间无法被垃圾收集器收集
- 内存泄漏:对象不会被程序用到了,但GC又不能进行回收;内存泄漏不会立刻引起程序崩溃,但它会逐渐蚕食内存空间,直至内存耗尽,最终出现OOM;常见内存泄漏情况
- 单例模式: 单例生命周期和应用程序一样长,若单例程序持有对外部对象的引用,那该外部对象在程序正常运行过程中永远也不会被回收
- 一些提供close的资源未关闭:数据连接,网络连接和io连接以及报表workbook对象必须手动close,否则不会被回收
- STW:stop the world,指的是GC事件发生过程中应用程序会产生停顿(任何GC都会产生STW),且停顿产生时整个应用程序线程都会被暂停,GC完成后会恢复应用程序,频繁中断会让用户体验不好,所以我们需要尽可能缩短STW的时间
- 并发和并行对比
- 并发:多个事件在同一时间段内发生(CPU时间片段极速切换);并发的多个任务之间是互相抢占资源的
- 并行:多个事件在同一时间点同时发生;并行的多个任务不会互相抢占资源
- 只有在多CPU或者一个CPU多核情况下才会发生并行,否则看似同时发生的事情都是并发执行的
- 安全点:程序在运行时并非在所有地方都能停顿下来进行GC,只有在特定位置才可以,这些位置称为安全点(safe point),若安全点过少可能导致GC等待时间太长,过多可能导致性能降低,所以我们通常选择一些执行时间较长的指令作为安全点,比如方法调用、循环跳转、异常跳转等
- 安全区域:在一段代码片段中,对象的引用关系不会发生变化,在这个区域中的任何位置开始GC都是安全的,也可以看做被扩展的安全点
- java中四种对象引用
- 强引用、软引用、弱引用、虚引用共4种引用,强度依次逐渐减弱;除了强引用,其他3种引用类均包含在java.lang.ref包下
- 强引用(StrongReference): 程序中普遍存在的引用关系(程序中99%都是强引用),如Object obj = new Object(),只要引用关系存在,垃圾收集器永远不会回收掉,强引用是造成内存溢出的主要原因
- 软引用(SoftReference): 内存不足即回收,在JVM内存不足时,进行垃圾回收时才进行二次回收(一次回收是针对不可达对象);主要用于高速缓存场景
- 弱引用(WeakReference): 发现即回收,无论JVM内存是否充足,在进行垃圾回收时对只被弱引用关联的对象进行回收
- 虚引用(PhantomReference): 对象回收跟踪,对象是是否有虚引用对其生命时间无任何影响,虚引用的目的就是在该对象被回收时发出系统通知,即跟踪垃圾回收过程
垃圾回收算法
标记阶段-引用计数算法(Python采用的算法,Java未使用)
引用计数算法比较简单,通过对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况,比如对于一个对象A,只要有一个对象引用A,则A的引用计数器就加1,当引用失效时就减1,当对象A的引用计数器值为0,即表示对象A不可能再被使用,可进行回收
- 优点
- 实现简单,垃圾对象便于辨识
- 判定效率较高,回收没有延迟性
- 缺点:
- 需要单独存储计数器,增加了存储空间的开销
- 每次赋值需要更新计数器,增加了时间开销
- 最严重的是无法处理循环引用的情况(导致Java垃圾回收器没有使用该算法)
- 扩展: 人工智能中Python则使用该算法进行垃圾回收,它解决循环引用问题的策略
- 手动解除,在合适时机解除引用关系
- 使用弱引用,weakref是python提供的标准库,旨在解决循环引用
标记阶段-可达性分析算法/根搜索算法/追踪性垃圾收集(Java采用))
可达性分析算法是以根对象集合GC Roots为起点,按照从上至下的方式搜索被根对象集合中所连接的对象是否可达;如果该对象没有任何引用链相连,则是不可达,意味着该对象已经死亡可以标记为垃圾对象;可达性分析算法有效的解决在引用计数算法中循环引用的问题,防止内存泄漏的发生.
GC Roots包括的类或者对象
- 虚拟机栈中引用的对象:各个方法中使用到的参数、局部变量(即栈帧中局部变量表中的引用对象)
- 本地方法栈引用的对象
- 方法区中类静态属性引用的对象: java类静态变量
- 方法区中常量引用的对象: 字符串常量池的里的引用
- 同步锁synchronized持有的对象
清除阶段-标记-清除(Mark-Sweep)算法
当堆中有效空间被耗尽的时候,就会停止用户线程(STW),然后进行标记和清除操作;
- 标记:垃圾回收器Collector从GC Root开始遍历,标记所有被引用的对象,一般会在对象头中记录为可达对象
- 清除:垃圾回收器Collector对堆内存从头到尾进行线性遍历,如果发现某个对象在它的对象头中没有标记为可达对象,则将其视为垃圾进行回收(注意:清除并非真的置空,而是把需要清除的对象地址保存在空闲地址列表中,下次有新对象需要存储时,会判断该垃圾所在的内存位置是否能够存储,若能则覆盖原有垃圾)
缺点
- 效率不算高
- 进行GC需要停止整个应用程序,导致用户体验差
- 空闲内存不连续会产生内存碎片,需要JVM维护一个空闲列表
清除阶段-复制(Copy)算法
该算法核心思想是将可用的内存空间分为两块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存角色,最后完成垃圾回收(复制算法适用于复制没有存在很多存活的对象,即新生代)
优点
- 没有标记和清除过程,实现简单,运行高效
- 复制以后保证空间的连续性,不会出现“碎片”问题
缺点
清除阶段-标记-压缩(Mark-Compact)算法
由于标记清除算法会产生碎片化,所以设计者在此基础上进行了改进诞生了标记压缩算法,所以标记压缩算法最终效果=标记-清除算法+内存碎片整理,即标记压缩算法也可以称为标记-清除-压缩算法
标记压缩过程
- 标记阶段和标记清除算法一样,从根节点开始标记所有被引用的对象
- 压缩/整理:将所有存活的对象压缩到内存的一端,按顺序排列存储
- 最后清理边界外所有内存空间
优点
- 消除了标记-清除算法中内存区域碎片化的缺点,我们给新对象分配内存空间时,JVM只需要持有一个内存起始地址即可
- 消除了复制算法中内存减半的高额代价
缺点
- 效率上标记-压缩算法低于复制算法
- 压缩/整理过程中,若对象被其他对象引用,则还需要调整引用的地址
- 需要全称暂停用户应用你程序,即STW
三种垃圾清除算法对比
分代收集算法(CMS垃圾回收器)
由于不同的对象生命周期是不一样的,因此不同生命周期的对象可以采取不同的收集方式,以便提高回收效率;目前几乎所有的GC都是采用分代收集算法执行垃圾回收的,Hotspot中基于分代概念,对于新生代和老年代各自特点就采用了不同的算法
- 新生代:对象生命周期短,存活率低,回收频繁;采用复制算法回收效率最高,因为复制算法只和当前存活对象多少有关,且对于内存利用率不高的问题,hotspot也通过survivor区域进行缓解
- 老年代:对象生命周期长,存活率高,回收不及新生代频繁;一般采用标记-清除或者标记-压缩和标记-清除算法混合实现
分代收集案例
以hotspot中CMS垃圾回收器为例,CMS是基于Mark-Sweep算法实现,对于对象的回收效率很高.而对于碎片问题,CMS又采用了基于Mark-Compact算法的Serial old回收器作为补偿措施(当内存回收不佳时利用Serial old执行Full GC进行垃圾回收)
增量收集算法
基本思想
如果一次性将所有垃圾进行处理,需要造成系统长时间停顿,极大影响用户体验;因此我们可以让垃圾收集线程和应用程序线程交替执行,每次垃圾收集线程只收集一小片区域内存空间,接着切换到应用程序线程.依次反复执行知道垃圾收集完成。增量收集算法基础仍是标记清除和复制算法,它对线程间冲突进行了妥善处理,允许垃圾收集线程以分阶段的方式完成标记-清除和复制工作.
分区算法(G1垃圾回收器)
为了更好的控制GC产生的停顿时间,分区思想是将一块大的内存区域分割成多个小块,根据目标停顿时间,每次合理的回收若干个小区间region,而不是整个堆空间,从而减少一次GC所产生的STW
分代算法思想是按照对象生命周期长短划分成两个部分;而分区算法思想是将整个堆空间划分成连续不同的region;其中每个region独立使用,独立回收,这种方式可以控制一次回收一定数量的region,已达到更好的控制GC产生的停顿时间
垃圾回收器
GC核心性能指标
- 吞吐量:运行用户线程执行的时间占总运行时间的比例(总运行时间=用户线程执行时间+GC时间);适用于服务器端程序
- 暂停时间STW:执行垃圾回收时用户线程暂停的时间,即延迟时长;适用于交互式程序,如web应用
GC垃圾回收器的目标就是在保证最大吞吐量的情况下,最大化的降低STW
GC分类
- 按垃圾回收线程分类
- 串行垃圾回收器(同一时间段只允许一个CPU用于执行垃圾回收操作)
- 并行垃圾回收器(同一时间段允许多个CPU(或者单CPU多核)用于执行垃圾回收操作)
- 按工作模式分类
- 并发式垃圾回收器:垃圾回收线程和用户线程并发交替执行,减少STW
- 独占式垃圾回收器:只允许垃圾回收线程运行,停止所有用户线程直到垃圾回收结束
- 按工作内存区间
垃圾回收器和JVM有紧密的联系,不同的JVM之间的GC有一定的区别,基于hotspot虚拟机有7种经典的垃圾收集器
- 串行GC: Serial、Serial Old
- 并行GC: ParaNew、Parallel Scavenge、Parallel Old
- 并发GC: CMS、G1
GC组合关系
- 其中serial old作为CMS出现“Concurrent Mode Failure”的后备方案(即会执行FGC回收可用内存空间)
- 红色虚线:JDK8/JDK9后废弃/移除了serial GC搭配CMS GC以及ParaNew 搭配Serial old使用
- 绿色虚线:JDK14弃用Parallel Scavenge GC搭配SerialOld GC,以及移除了CMS回收器
由于Java使用场景很多,所以需要针对不同场景提供不同的垃圾回收器以实现更高垃圾收集的性能
垃圾回收器说明
Serial 回收器(串行回收)
ParaNew回收器(并行回收)
- Serial GC是以单线程方式回收新生代,而ParaNew则是它的多线程版本,且只能回收新生代
- 同样采用复制算法和STW机制
- 很多JVM运行在Server模式下新生代默认垃圾回收器
-
ParaNew收集器适合多CPU环境,可以更快速完成垃圾收集提升程序的吞吐量
-
-XX:+UserParaNewGC 指定年轻代采用的收集器,即新生代使用Serial GC,不影响老年代
Parallel回收器(并行回收且追求高吞吐量)
CMS回收器(追求低延迟)
- jdk5版本中hotspot推出了强交互应用场景下的划时代的垃圾回收器,实现了垃圾线程和用户线程同时工作
- 尽可能缩短垃圾收集时的STW
- 采用标记-清除算法,也存在STW
- 由于Parallel Scavenge无法和CMS配合使用(底层框架不同),所以后来推出了ParaNew适配CMS
CMS工作原理
CMS执行流程主要包含4个过程,初始标记、并发标记、重新标记、并发清理(其中只有初始标记和重新标记存在STW)
- 初始标记:也称为initial-mark,该阶段会STW,且仅仅是标记GC Roots能直接关联到的对象,标记完成则恢复之前被暂停的所有用户线程,由于直接关联对象比较小,所以该阶段标记速度很快
- 并发标记:通过初始标记的对象开始遍历整个对象图的过程,该阶段耗时长但不需要停顿用户线程,可以并发执行用户线程和垃圾回收线程
- 重新标记:由于并发标记阶段中用户线程和垃圾线程同时执行,可能存在一些标记的对象产生变动,所以需要进行修正,该阶段时长比初始时间长但远远小于并发标记时长
- 并发清理:清理删除掉标记阶段判断的已经死亡的对象,释放内存空间.由于采用的标记-清除算法不涉及移动内存空间,所以该阶段可以和用户线程并发执行
优点
缺点
- 采用的标记-清除算法会产生内存碎片
- 降低系统的吞吐量: 并发阶段垃圾线程会占用一定的资源,所以总的吞吐量会减少
- CMS无法处理浮动垃圾: 并发阶段可能产生新的垃圾对象,CMS无法及时回收这些垃圾,只能在下一次GC时回收
注意
- 由于在垃圾收集阶段用户线程没有中断,所以在CMS回收郭过程中,必须确保应用程序拥有足够的内存空间.因此当堆内存使用率达到某一阈值时,便开始进行回收,以确保在CMS期间依然有足够的内存空间支持程序运行;在CMS期间预留的内存无法满足应用程序需求时,就会出现“concurrent Mode failure"失败,这时虚拟机将启动后备预案,临时启用Serial old 收集器重新进行老年代的垃圾回收,此时STW耗时就比较长了
- 由于采用标记-清除算法,所以内存会产生碎片化,在为新对象分配空间时将无法使用指针碰撞技术,而只能选择空闲列表执行内存的分配
G1回收器(区域分代化)
为了适应现在不断扩大的内存和不断增加的处理器数量,进一步降低暂停时间并且兼顾良好的吞吐量。官方给G1设定的目标就是在延迟可控的情况下获得尽可能高的吞吐量,称其为“全功能收集器”
- G1是一个并行回收器,它把堆内存分割成代表Eden,Survivor,old的region
- G1 GC避免在整个堆区进行垃圾收集,G1跟踪各个Region垃圾的价值(内存空间大小)以及回收所需的时间,在后台维护一个有限列表,每次根据允许的收集时间,优先回收价值最大的region
- G1主要针对配备多核CPU及大容量内存的机器,兼具高吞吐量和低延迟
- G1是JDK9以后的默认垃圾回收器,且CMS在JDK9中被标记为废弃(JDK14已移除)
优点
- 并行与并发
- 并行性:G1在回收期间,可以有多个GC线程同时工作,有效利用多核计算能力
- 并发性:G1可以和用户线程交替工作
- 分代收集:G1仍然区分年轻代和老年代,同时不要求它们都必须是连续等
- 空间整合:将内存划分一个个region,内存回收以region为基本单位,region之间是复制算法,整体上采用标记-压缩算法,避免内存碎片化,对于大堆时G1优势更加明显
- 可预测的停顿时间模型:由于分区以及每次根据允许的收集时间,优先回收价值最大的region,获取最高的收集效率
缺点
G1相比CMS,在用户程序运行过程中,需要占用额外执行负载(比如:G1需要通过记忆集Rset存储其他region的引用进而不必全堆扫描)
相比CMS,如下场景更适合G1
- 超过50%的Java堆被活动数据占用
- 对象分配频率或年代提升频率变化很大
- GC停顿时间过长(0.5-1s)
- 经验上来看,在6-8G之间,G1和CMS表现差不多,而小于此区间CMS更优秀,否则G1更优秀
常见问题
Serial GC、Parallel GC、CMS区别
- 如果追求最小化使用内存和并行开销,选择Serial GC
- 如果追求组嗲话应用程序的吞吐量,选择Parallel GC
- 如果追求最小化的GC中断或停顿时间,请选择CMS GC
MinorGC、MajorGC、FullGC区别
JVM在进行GC时,并非每次都对新生代、老年代、方法区区域一起回收的,大部分时候回收都是指新生代,而针对Hotspot VM的实现,按照回收区域分为Partial GC部分收集和Full GC整堆回收;
- 部分收集又分为新生代收集和老年代收集以及混合收集
- 新生代收集:Minor GC/YGC,对新生代的Eden和S0以及S1的垃圾回收
- 老年代收集:Major GC/Old GC,对老年代区域的垃圾回收,目前只有CMS GC会有单独收集老年代行为
- 混合收集: Mixed GC,对新生代以及部分老年代垃圾收集,目前只有G1 GC会有这种方式
- 整堆收集: Full GC,收集整个Java堆和方法区的垃圾收集,因为方法区几乎不执行GC,所以我们常常将Major GC和Full GC视为同一个含义
垃圾回收条件
- 新生代收集触发条件
- 当新生代中的Eden区域满的时候触发YGC
- Survivor区不会触发GC;而且YGC非常频繁且速度很快
- YGC会引发STW,直到垃圾回收完成用户线程才恢复运行
- 老年代收集触发条件
- 老年代空间不足时会发生Major GC
- 通常在发生Major GC前会进行一次YGC
- Major GC速度一般比YGC慢10倍以上,STW时间更长
- 整堆收集触发条件
- 调用System.gc(),系统建议执行Full GC,但不一定执行
- 老年代空间不足
- 通过YGC进入老年代的平均大小大于老年代的可用内存
- Eden和S0存货对象复制到S1时,S1空间不能够存放该对象,则会把对象转存到老年代,且此时老年代可用内存大小小于该对象大小
- 方法区空间不足
注意:我们开发中针对JVM调优,就是减少Full GC进而减少STW