JVM全文
GC是什么,为什么要有GC
- GC,garbage collection,针对内存管理的一种垃圾回收机制,能够及时回收垃圾,释放空闲空间
- 垃圾,在运行程序中没有任何指针指向的对象,这个对象就是要被回收的垃圾
- 如果不及时清理内存中的垃圾,这些垃圾对象所占的内存空间一直保存在应用程序结束,无法被其他对象使用,导致内存溢出
- 内存溢出,没有空闲内存,且垃圾回收器也无法提供更多内存
- 内存泄漏,逻辑上并不需要这个对象了,但是这个对象根据可达性分析,仍在引用链中,被其他对象引用着
- 为什么需要GC
- 释放没用的对象,如果不进行垃圾回收,内存迟早都会消耗完;
清除内存里的记录碎片,碎片整理将占用的内存移到堆的一端,以便JVM将整理出的内存分配给新的对象- 应付越来越庞大、复杂的业务需求,没有GC就不能保证应用程序的正常进行,但是经常造成
STW的GC跟不上实际需求,需要不断堆GC优化
GC回收对象的判定方式
引用计数算法
- Java没有使用
- 对每一个对象都保存一个
整型的引用计数器属性,用于记录对象被引用的情况- 优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟
- 缺点:增加存储空间的开销,需要单独的字段存储计数器;增加时间开销,每次赋值都需要更新计数器,伴随加法和减法;无法处理循环引用的情况
- 完善:使用了软/弱/虚引用来解决
具体过程
- 对于一个对象A,只要有任何一个对象引用了A,A的引用计数器+1
- 当引用失效,引用计数器-1
- 只要对象A的引用计数器值==0,表示A不可能再被使用,可进行回收,提高吞吐量
可达性分析
- Java/C#使用
- 解决引用计数器中循环引用的问题,防止内存泄漏
- 需要STW机制,分析工作必须在能保障一致性的快照中进行,也就导致java进行GC必须发生
Stop The World 停下用户线程- 优点:解决循环引用
- 缺点:必须STW
判定过程
- 以根对象
GCRoots集合(一组必须活跃的引用),按照从上至下的方式搜索根对象集合所连接的目标对象是否可达- 内存中的存活对象都会被根对象集合直接或间接连接着,搜索走过的路径称为
引用链- 如果目标对象没有任何引用链相连,则不可达,意味着该对象已经死亡,标记为垃圾对象
- 只有能够被根对象集合直接或者间接连接的对象才是存活对象
判定的条件
- 两次标记
- 可达性分析是找出可达的对象
- 第一标记,根据可达性分析反向标记出了,不可达的对象,但是这个对象有两种情况
可复活的/不可触及的- 一次筛选,判断obj是否有必要执行
finalize(),只有一次执行机会。- 如果没有重写
finalize(),或者已经被虚拟机调用过,则为不可触及的,那么直接可以准备被回收;- 如果重写
finalize(),且还未执行,obj就会被插入到F-Queue队列中,由Finalize线程(终结器引用)触发发其finalize()执行,- 二次标记,针对
F-Queue队列中对象,如果执行了finalize(),没有出现被引用的情况,那么就会被回收;如果出现被引用的情况,标记已经执行过finalize(),移除队列,且下一次如果为不可达,则不能执行finalize(),直接为不可触及
GCRoots
- Root采用栈方式存放变量和指针,如果
一个指针保存了对内存里面的对象,但是自己又不放在堆内存中,那就是一个Root
- 虚拟机栈中引用对象,局部变量表中的引用,如各个线程被调用的方法中使用到的参数、局部变量
- 本地方法栈内
JNI Java Native Interface(本地方法)引用对象- 方法区的
类静态属性引用的对象,如引用类型静态变量;方法区中常量池的引用- 字符串常量池中的引用
- 被同步锁持有的对象
- 针对回收的区域不同,有其他对象临时性的加入,当对新生代进行
局部回收Partial GC,老年代的一些对象,也会指向新生代的对象,将这个关联的区域计入GC Roots集合中- G1中每个Region的RSet部分
- 虚拟机内部引用,基本数据类型对应的包装类,一些常驻的异常对象(
NullPointerException、OutOfMemoryError),系统类加载器- 反映java虚拟机内部情况的JMXBean(java程序管理)、JVMTI(虚拟机管理)中的注册回调、本地代码缓存
垃圾回收器,各自优缺点、应用场景
- 最小化使用内存和并行开销 Serial GC 串行
- 最大化应用程序的吞吐量 Parallel GC 并行
- 最小化GC的中断或停顿时间 CMS GC 并发
- 并发和并行 G1
Serial 回收器
- 串行回收、STW机制
- 新生代采用复制算法、老年代采用标记-压缩算法
- Client模式下默认的垃圾收集器
- 只使用一个CPU或者一个线程完成垃圾工作,在收集过程中会暂停所有用户线程
- 优点:对于单CPU的条件下,没有线程交互的开销
- 缺点:收集工作进行时必须暂停其他所有正在工作的线程,会造成用户线程停顿,暂停时间长,吞吐量低
- 适用场景:运行在Client模式下的虚拟机(用户桌面)
- Serial Old在Server模式的作用为,配合ParNew GC,作为老年代垃圾回收器;作为CMS的后备方案,当CMS进行并发标记,预留的内存无法满足程序需要时,调用Serial Old
ParNew 回收器
- Serial多线程版本,并行回收、STW机制
- 只能处理新生代,复制算法
- JDK6之前,Server模式下新生代默认垃圾回收器
- 老年代使用SerialOld,新生代收集频繁,并行执行高效;老年代,回收次数少,串行减少资源开销
- 优点:在多CPU环境下,利用物理硬件优势,快速完成垃圾收集
- 缺点:ParNew收集器在单线程或CPU数量较少的情况下,由于存在线程交互的开销,效率并不能百分百保证完全超越Serial收集器
- 适用场景:多CPU环境下,是运行在Server模式下的虚拟机的首选新生代收集器
Parallel 回收器
- 吞吐量优先,并行回收,总停顿时间小,STW机制
- 新生代采用复制算法、老年代采用标记-压缩算法
- 拥有自适应调节策略,调节内存分配,达到最优的策略,优先最大吞吐量下,调整堆大小,影响暂停时间
- 堆小->暂停时间小,但是收集频繁,总时间长->延迟低;堆大->暂停时间长,收集不频繁,总时间短->吞吐量高
- JDK7之后默认组合,互相激活
- 优点:高吞吐量
- 缺点:无法保证暂停时间
- 适用场景:充分利用多CPU的硬件资源,新生代和老年代都可以进行并行收集;服务端
CMS 回收器
基本介绍
- 并发性标记清除垃圾收集器,在工作过程中,有部分时间可以跟用户线程交替执行,实现垃圾线程和用户线程同时工作
- 针对老年代,标记清除算法、标记时有STW机制
- 优点:低延迟、尽可能缩短暂停时间,达到快速响应
- 缺点:产生内存碎片;对CPU资源敏感,并发阶段占用线程资源;无法清除浮动垃圾,并发标记过程中,不能标记用户线程新产生的垃圾
- 适用场景:互联网站或B/S系统的服务端(希望系统停顿时间短,响应速度快,面向用户)、小内存
过程
- 初始标记,Initial-Mark,所有用户线程STW,
仅仅只是标记出GCRoots能直接关联到的对象,需要标记的对象很少,一旦标记完成后就恢复用户线程,执行速度非常快,STW时间短- 并发标记,Concurrent-Mark,从GCRoots的直接关联对象开始
遍历整个对象图的过程(可达性分析),耗时长,但不需要停顿用户线程,并发执行- 重新标记,Remark,为了修正标记期间,
因用户程序运行导致标记产生变动的那一部分对象的标记记录,怀疑是垃圾,需要确定是否为垃圾,进行STW,STW时间比初始标记稍长- 并发清除,Concurrent-Sweep,清理清除掉标记阶段判断的已经死亡的对象,释放内存空间,
不需要移动存活对象,并发执行
两个暂停时间
- 初始标记,最开始的标记,保证GCRoots可以先找到一个对象,且要保证一致性
- 重新标记,修正并发标记期间的对象结果,有些对象可能在并发标记过程中发生变化
并发清除的特点
- 采用标记清除算法,产生内存碎片,再分配空间时无法采用指针碰撞,需要
维护一个空闲列表- 要保证用户线程还能继续执行,运行的资源不受影响,不能随意移动堆中对象的地址
并发执行发生问题
- 由于垃圾收集阶段用户线程没有中断,回收过程中,需要
确保用户线程有足够的内存可用,CMS不能等到老年代被填满后才进行收集,当堆内存使用率达到某一阈值,开始回收- CMS运行时预留的内存无法满足程序需要,出现
Concurrent Mode Failure,虚拟机启用后备方案,临时启用Serial Old收集器进行老年代的垃圾收集
G1 回收器
基本
- 区域化分代式,在延迟可控的情况下提高吞吐量,目标保证低延迟
- 将堆空间分为数个Region空间,每个空间都是
Eden、Survivor、Old及Humongous的某个角色,并且在GC过程中,角色可以换- 针对Region是复制算法,整体上是标记-整理算法
- 针对年轻代和老年代
- 优点:并行与并发兼备;分代收集,并且根据Region的回收价值,根据预设的暂停时间,收集部分价值高的Region;空间整合,避免碎片化;可预测的停顿时间模型,可以根据指定的暂停时间,收集价值最高的部分,避免全局停顿
- 缺点:内存占用高,额外执行负载较高,需要维护一个Reigon列表,记录空的Reion;每个Reion中还有一个RSet,记录Region空间中对象被引用的情况;还需要记录各Region的价值大小
- 适用场景:大内存、多处理器的服务端应用;需要低延迟,并具有大堆的应用;有大量活动数据,对象分配频率或年代提升频率很快
Region特点
- 新生代和老年代不再是物理隔离,逻辑上连续,都是Region的一部分集合所组成的
- 不再有S0/S1的区别,只有算法上的From/To
- Humongous,存储超大对象,多个连续的Region;如果超过1.5个Region,会直接放到H中
- 每个Region都是规整的,采用指针碰撞分配
- 每个Region都有TLAB,供线程私有
- 每一个Region都有一个RSet,记录Region对象被其他Region对象引用的情况,避免全局扫描
Remembered Set特点
- 记录Region对象被其他Region对象引用的情况,避免全局扫描
- 每一个Region都有一个RSet,每次引用类型数据写操作时(A中的引用指向了B),都会产生一个
Write Barrier暂时中断操作- 检查将要
写入的引用指向的对象是否和该引用类型数据是否在不同的Region,其他收集器检查老年代对象是否引用了新生代对象- 如果不同,通过
CardTable把相关引用信息(在B中记录A的区域)记录到引用指向对象所在的Region对应的RSet中- 当进行垃圾收集时,在GCRoots的枚举范围加入RSet,避免全局扫描
流程
- 总共三部分,年轻代GC、老年代并发标记及混合回收,当最差情况,进行Full GC
- Eden用尽触发年轻代GC,STW机制,多线程执行,复制算法
- 当堆空间使用率默认达到45%触发老年代并发标记,包括三个SWT的阶段,初始标记、再次标记(修正)、独占清理(计算各区域的回收价值)
- 老年代并发标记完成后,进行混合回收,根据配置的暂停时间和回收目标,回收最有价值的老年代Reigon和年轻代区域;允许10%的空间被浪费,因为回收价值不高
- Full GC,独占式、高强度、单线程;没有足够的
to-space和老年代保存Eden的数据;并发处理完成前,空间耗尽;最大时间暂停太短,导致规定时间之内无法完成垃圾回收
选择合适的垃圾收集器
- JVM给了三种选择:
串行收集器、并行收集器、并发收集器 - 串行处理器
- 适用情况:数据量比较小(100M左右);单处理器下并且对响应时间无要求的应用
- 缺点:只能用于小型应用
- 并行
- 适用情况:
对吞吐量有高要求,多CPU、对应用响应时间无要求的中、大型应用。举例:后台处理、科学计算。- 缺点:垃圾收集过程中应用响应时间可能加长,停顿时间长
- 并发
适用情况:
对响应时间有高要求,多CPU、对应用
GC搭配使用的情况
- 单CPU Serial New + Serial Old
- Serial GC + CMS JDK8移除
- ParNew + Serial Old JDK8移除
- ParNew + CMS(JKD14移除)+ Serial Old (MSC 后备方案,如果CMS回收失败,就使用这个)
- Parallel New + Serial Old JDK14移除
- Parallel New + Parallel Old G1前使用,JDK8及之前
- G1 兼备年轻代合老年代
JVM GC的算法
- 目前采用的G1垃圾回收器,对于一个Region采用复制算法,但是整体为压缩-整理算法
可达性分析算法
- 见上面
标记-清除算法
- 清除,并不是真的置空,而是把
需要清除的对象地址保存在空闲的地址列表里,下次有新的对象需要加载时,判断垃圾的位置是否够,如果够,那么就存放,覆盖掉垃圾对象的数据
执行过程
- 当堆中有效空间被耗尽,会停止整个程序
STW Stop The World- 标记,
Collector从引用根节点开始便利,标记所有被引用的对象,在对象的Header中记录为可达对象- 清除,
Collector对堆内存从头到尾进行线性便利,如果发现某个对象的Header没有标记为可达对象,将其回收
缺点
- 用递归的方式遍历,标记节点,遍历在引用链中的对象;清除阶段,需要将全部的对象都遍历一边,效率不算高
- 在GC时,停止整个应用程序
STW,用户体验差- 清除出来的空闲区域是不连续的,产生内存碎片,需要维护一个空闲列表
复制算法
- 可以使用
指针碰撞来为对象分配内存- 将内存空间分成两块,
每一次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未被使用的内存块中,之后清除正在使用的内存块中的所有对象,交换两个内存的角色,from区和to区,解决碎片化
优点
- 没有标记和清除过程,实现简单,运行高效
- 复制过去以后保证空间连续性,不会出现碎片化的问题
缺点
- 需要两倍的内存空间
- 对于G1这种分拆成大量region(对象的区域)的GC,复制而不是移动,需要
调整对象引用的引用地址,意味着GC需要维护region之间的对象引用关系,提高内存占用和时间开销- 系统中
存活对象很多,复制算法会提高内存占用和时间开销,复制算法需要复制的存活对象并不会太多;对应的,新生代的对象大部分都是朝生夕死的,回收性价比很高,避免了存活对象很多的极端情况,因此在新生代中采用复制算法,s0/s1中的对象不断在GC的过程中交换,并增加对象的年龄计数
标记-压缩算法
- 标记-整理,内存碎片化的整理
- 针对大部分对象是存活对象的情况,符合老年代垃圾回收的特性
- 指针碰撞,如果内存空间以规整和有序的方式分布,即
已用和未用的内存都各自一边,彼此之间维系着一个记录下一次分配其实点的标记指针,当为新对象分配内存时,只需要通过修改指针的偏移量,将新对象分配在第一个空闲内存位置上即可
执行过程
- 与标记-清除算法一致,从根节点开始标记所有被引用对象
- 将所有的存货对象压缩到内存的一段,按顺序排放,清理边界外的所有空间
- 标记的存货对象被整理之后,当需要给新对象分配内存时,JVM只需要
持有一个内存的起始地址(分割已存和未存的空间)
与标记-清除算法的差别
- 清除算法是一种非移动的回收算法
- 压缩算法是移动式的,是否移动回收后的存货对象是一项优缺点并存的风险策略
优点
- 消除了标记-清除算法中,内存区域碎片化
- 消除了复制算法中,内存减半的高额代价
缺点
- 效率低于前两个
- 移动对象的同时,如果对象被其他对象引用,还需要调整引用地址
- 移动过程中,需要全程暂停用户应用程序
STW
三个算法比较
- 执行效率来说,复制算法>清除>压缩
- 压缩算法,比清除算法多了整理内存的阶段;相对于复制算法,整理比直接复制要慢
- Mark阶段的开销
与存活对象数量成正比- Sweep阶段的开销
与所管理区域的大小成正相关- Compact阶段的开销
与存活对象的数量成正比
分代回收算法
- 不同的对象的生命周期是不一样的,采取不同的收集方式
- 根据各个年代的特点使用不同的回收算法
- Http请求中的Session对象、线程、Socket连接,生命周期比较长
- String对象,生命周期比较短
- 年轻代,区域相对老年代比较小,生命周期短、存活率低、回收频繁
用复制算法的回收整理,速度最快,复制算法的效率
只和当前存活对象大小有关
- 老年代,区域较大,对象生命周期长、存活率高;一般是由标记-清除或者
标记-清除与标记-整理混合实现
增量收集算法
- 实时垃圾收集算法
- 让垃圾收集线程和应用程序线程交替执行,垃圾收集线程只收集一小片的内存空间,接着切换到应用程序线程,反复切换
- 基础仍是传统的标记清除和复制算法,通过
对线程间冲突的妥善处理,允许垃圾收集线程以分阶段的方式完成标记、清理或复制工作- 优点:由于在垃圾回收过程中,间断性执行了应用程序代码,减少系统的停顿时间
- 缺点:由于线程切换和上下文转换的消耗,会导致垃圾回收的总体成本上升,造成系统吞吐量的下降
分区算法
- 针对
G1垃圾收集器- 为了控制GC产生的停顿时间,
将一块大的区域分割成多个小块,根据目标停顿时间,每次合理地回收若干个小区间,减少一次GC所产生的停顿- 将整个堆空间划分成
连续的不同小区间,有的放eden,有的放s0/s1,有的放old,有的放humongous 超大对象- 每一个小区间都独立使用,独立回收,可以控制一次回收多少个小区间
如何选择合适的垃圾收集算法
- 根据每个区域回收的特点
- 频繁收集年轻代
- 较少收集老年代
- 基本不动方法区
- 根据应用程序内存提高的频率
- 内存提高频率高,如果碎片化问题严重,容易导致内存溢出
- 内存提高频率低,用清除算法即可,无需增加开销去整理内存,修改引用
- 根据物理内存
- 物理内存足够大,可以采用复制算法,直接复制
- 物理内存小,标记清除/整理
触发垃圾回收的情况
- 年轻代
- Eden满
- 进行老年代回收时,会先对年轻代进行回收
- 老年代
- 老年代满
- 超大对象放不下年轻代直接放在老年代
- Eden->Survivor的对象,放不下Survivor,直接放在老年代,但是老年代也放不下
- Survivor中,From->To,但是To放不下,直接放在老年代,但是老年代放不下
- 方法区
方法区满,类加载不进去,调用Full GC,包括新生代和老年代也进行GC
System.gc()和runtime.gc()
- 显式触发
Full GC,同时对老年代和新生代进行回收 - 附带一个免责声明,无法保证对垃圾收集器的调用
- 调用场景:性能基准测试时,会在运行之间调用
- 底层调用本地方法
public static void gc() {
//效果一致
Runtime.getRuntime().gc();
}
//Runtime.getRuntime().gc(); 调用本地方法
public native void gc();
- 强制调用失去引用的对象的
finalize()方法
System.runFinalization();