垃圾回收机制
概述
Java虚拟机内存的动态分配和垃圾收集机制已经非常成熟.程序计数器,虚拟机栈,本地方法区的生命周期是随着线程共生死的,所以这三个区域的垃圾回收都不会存在问题.但是Java堆和方法区,只有在程序运行的时候才知道会创建哪些对象,这部分内存的分配和回收都是动态的.
判断对象可以被回收
引用计数法
- 定义
- 给对象添加一个引用计数器,被引用时就+1,引用失效时就减一.当引用计数为0时被回收掉.
- 缺陷
- 对于循环引用的对象无法进行回收.
可达性分析
- 定义
- 通过一系列”GC Roots”的对象作为起点,从这些节点开始往下搜索,搜索所走过的路叫做引用链.当然一个对象没有任何引用链相连,那么就会被GC回收掉.
- GC Roots
- 虚拟机栈(栈桢中本地变量表)中的引用对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中的JNI(即一般说的Native方法)引用的对象
引用
- 定义
- JDK1.2之前
- 如果reference存储的数值代表的另一个内存的起始地址,就称这块内存代表的一个引用.
- 缺陷: 一个对象只有两个状态,一个被引用,一个未被引用.但是当内存空间足够空闲,对于有些对象其实也可以保留.直到内存空间紧张时再回收这些对象.
- JDK1.2之后
- 对引用进行了扩充,将引用分为强引用(Strong Reference),软引用(Soft Reference),弱引用(Weak Reference),虚引用(Phantom Reference).四种引用强度依次减弱.
- 强引用: 程序代码中普遍存在的,如
Object obj = new Object()
.如果是强引用,垃圾收集器永远不会回收掉被引用的对象. - 软引用: 描述一些还有用但非必须的对象.软引用的对象,在系统将要发生内存溢出的情况之前,将会把这些对象列入回收范围之内,进行第二次回收,如果还不够,则会抛出OOM.提供
SoftReference
类来实现 - 弱引用: 也是描述一些还有用但非必须的对象,它的强度比软引用的弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前.提供
WeakReference
类来实现 - 虚引用: 也被称为幽灵引用或者幻影引用.唯一的作用就是让这个对象被垃圾收集器回收时收到一个系统通知
PhantomReference
类来实现.
生存还是死亡
即使在可达性分析算法中不可达的对象,也不一定会被回收,只是出于准备被回收的阶段,真正宣告”死亡”的对象至少要经历两次标记过程.如果发现对象在可达性分析后发现没有与GC Roots相连的引用链,那么他将被第一次标记并进行一次筛选,筛选的条件是该对象是否有必要执行finalize()
方法,当对象没有覆盖finalize()
方法或者虚拟机已经调用过finalize()
方法,虚拟机认为这两种方法都”没有必要执行”.
如果这个对象被判定为有必要执行finalize()
方法,那么这个对象将会被放入一个叫做F-Queue的队列中,稍后会由虚拟机自动创建的,低优先级的Finalizer
线程去执行它.所谓的执行是指虚拟机会触发这个方法,但并不承诺会等待它运行结束(原因是如果finalize()
方法执行缓慢或者发生了死循环,会导致F-Queue队列中的对象永久的等待,最终导致整个内存回收系统崩溃).finalize()
方法是对象逃脱死亡的最后一次机会,稍后GC将对F-Queue中的对象进行小规模的标记,如果对象要在finalize()
方法中拯救自己——重新与引用链上的任何对象建立关联即可,譬如把自己(this关键字)赋值给某个类或者对象的成员变量,那么就会在第二次标记时移出F-Queue队列;若在第二次标记时没有成功逃脱,则该对象就被真正的回收了.
对象执行过finalize()
方法后就不会再被执行.
- 建议
- 这个方法不要使用,因为不确定因素太多,可以用try/catch/finally来关闭资源.
回收方法区
永久带垃圾回收包括两个部分内容:
- 废弃的常量(字符串,类(接口),方法,字段的符号引用)
- 回收常量和Java堆中回收对象类似.
- 无用的类(需要同时满足三个要求)
- 该类所有的实例已经被回收
- 加载该类的ClassLoad已经被回收
- 该类对应的java.lang.Class没有在任何地方引用,无法在任何地方通过反射访问该类的方法
无用的类并不一定会被回收,虚拟机提供了-Xnoclassgc
参数来控制这一行为,使用-verbose:class
以及-XX:TrackClassLoading
(需要Product版的虚拟机使用),-XX:TrackClassUnLoading
(需要FastDebug版的虚拟机支持)查看类加载和类卸载的信息.
垃圾收集算法(偏理论)
标记-清除算法
- 定义
- 首先标记出需要回收的对象,然后统一回收被标记的对象.在”生存还是死亡”章节已经详细说明.
- 缺陷
- 效率问题: 标记和清除效率都不高
- 空间问题: 清除后会产生不连续的内存片段,在需要分配较大对象时不得不触发垃圾收集动作.
复制算法
- 定义
- 将内存按容量分为两块,只是用其中一块,当内存将要存满时,将存活的对象复制到另一块内存中,并清除该内存区域.
- 优点
- 实现简单
- 运行高效
- 缺陷
- 将可用内存缩小为原来的一半.
- 当内存存活率高的时候,效率会变低.
- 需要有额外空间进行担保.
复制算法的优化
- 定义
- IBM的专门研究表明,新生代中98%的对象都是”朝生夕死”,所以将内存划分为一块较大的Eden空间和两块较小的Survivor空间.当回收时,将Eden和Survivor中存活的对象一次性复制到另一块Survivor上,然后清理用过的内存.默认Eden:Survivor=8:1,这样,只有10的内存空间被浪费.如果回收的对象大于Survivor内存空间大小(10%),需要依其他内存空间(老年区)进行分配担保.
标记-整理算法
- 定义
- 根据老年代的特点(对象存活时间久)提出的.他与标记-清除算法相似,只是不是直接对可回收的对象进行清理,而是让所有存活的对象移至一端,然后清理边界以外的内存.
分代收集算法
- 定义
- 没有什么新思想,只是根据存活周期的不同将内存分为几块.一般是把Java堆分成新生代和老年代.可以根据不同的年龄代采取不同的收集算法.新生代一般使用复制算法,而老年代一般使用标记-清除算法或者标记-整理算法.
HotShot的算法实现(方法论)
枚举根节点
从可达性分析中从GC Roots节点找引用链这个操作为例
- 引用多: 可作为GC Roots节点主要在全局性的引用(例如常量或类静态属性)与执行上下文(例如栈桢中的本地变量表)中,现在很多应用仅仅方法区就有几百兆,如果要逐个检查这个里面的引用,必然会消耗很多时间.
- GC停顿: 可达性分析操作必须保证在一个确保一致性的快照(整个分析期间,整个执行系统就像被冻结在某个时间点上,不可以出现分析过程中,整个对象的引用还在不断的变化,不满足该点的话,分析结果的准确性就不能得到保证)中进行.这点将会导致GC进行时必须停顿所有的Java的执行线程(Sun公司称其为Stop The World)的其中一个重要原因.
由于HotShot是准确式GC,所以并不需要一个不漏的检查完所有的全局性应用和执行上下文的引用位置,它使用一组称为OopMap的数据结构来存放,类加载完成的时候,HotShot就把对象内的什么偏移量上是什么类型的数据计算出来,在JIT编译过程中,也会在特定的位置记录下栈和寄存器中哪些位置是引用.
安全点(Safepoint)
可能导致引用变化或者说OopMap内容变化的指令特别多,如果对每一条指令都生成一个OopMap,那么将需要大量的额外空间,这样GC的成本会非常高.
- 定义
- 实际上HotShot的确也没有为每条指令都生成OopMap,之前也提到是在特定位置记录了这些信息,这些位置称为安全点(Safepoint).程序执行时并非在所有地方都能停顿下来GC,只有到达安全点才能暂停.
- 选定
- 既不能太少以至于GC等待时间过长,也不能过于频繁,增加运行时的负荷.其选择是基于“是否让程序长时间执行的特征”为标准进行选定的——因为每条指令执行的时间都非常短,程序不太可能因为指令流太长这个原因而过长时间运行,”长时间执行”的最明显特征就是指令序列复用,例如方法调用,循环跳转,异常跳转等,所以具有这些功能的指令才会产生Safepoint.
- 如何在GC发生时让所有线程(这里不包括执行JNI调用的线程)都”跑”到最近的安全点上再停顿下来.以下有两种方案
- 抢先式中断: 不需要线程的执行代码的主动配合,GC发生时,首先会把所有的线程全部中断,如果发现有线程中断的地方不在安全点上,就恢复线程,让他”跑”到安全点上.
- 主动式中断: 当GC需要中断线程时,不直接对线程操作,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就自己中断挂起.轮询标志的地方与安全点是重合的,另外再加上创建对象所有需要分配内存的地方.
安全区域(Safe Region)
Safepoint似乎解决了如何进入GC的问题,但是程序”不执行”(没有分配CPU时间)的时候,例如线程处于sleep或者blocked状态,这个时候线程无法响应JVM的中断请求,”走”到安全的地方挂起.JVM显然也不太可能等到线程重新被分配CPU时间.在这个背景下,安全区域诞生了.
- 定义
- 在一段代码片段之中,引用关系不会发生变化.在这个区域的任何地方开始GC都是安全的.
- 流程
- 线程执行到Safe Region中的代码时,首先标识自己已经进入了Safe Region,那样,在这段时间里JVM要发起GC时,就不用管标识自己为Safe Region状态的线程了
- 在线程要离开Safe Region时,他要检查系统是否已经完成了根节点枚举(或是整个GC过程),如果没有完成,就必须等待直到收到可以安全离开Safe Region的信号为止
垃圾收集器(JDK1.7 Update 14)
Java虚拟机对于垃圾收集器应该如何实现没有任何规定,所以不同的厂商不同的版本虚拟机所提供的垃圾收集器都可能会有很大的差别.
上图是7种用于不同分代的收集器,如果两个收集器之间存在连线,就说明他们可以搭配使用.没有最好的垃圾收集器,也没有万能的垃圾收集器,所以需要选择对应场景最优的垃圾收集器.
先解释并发和并行的区别
- 并发(Concurrent): 指用户线程与垃圾收集线程同时执行(但不一定是并行的,可能是交替执行),用户程序再继续运行,而垃圾收集程序运行于另一个CPU上
- 并行(Parallel): 指多条垃圾线程共同工作,但此时用户线程任处于等待状态
新生代垃圾收集器
Serial收集器
- 特点
- 复制算法
- 最基本,发展历史最悠久的收集器.在JDK1.3.1之前是虚拟机新生代的唯一选择
- 单线程的收集器,与其他单线程垃圾收集器相比,简单高效,没有与其他线程的交互.但是在进行垃圾收集时,必须暂停其他所有线程,知道他收集结束
- 虚拟机运行Client模式下默认的新生代垃圾收集器
- 尽可能缩短用户进程的停顿时间
ParNew收集器
- 特点
- 复制算法
- 许多Server的首选
- 多线程并行的收集器
- 其余与Serial一致
- 除了Serial收集器,只有它能和CMS收集器搭配使用
- 尽可能缩短用户进程的停顿时间
Parallel Scavenge收集器(吞吐量优先收集器)
- 特点
- 复制算法
- 多线程并行的收集器
- 控制吞吐量(CPU用于运行用户代码时间/CPU总消耗时间,CPU总消耗时间=CPU用于运行用户代码时间+垃圾收集时间)
- 根据用户配置参数动态的改变
- 修改参数
-XX:MaxGCpausemillis
最大垃圾收集时间(大于0的毫秒数)- 不是越小越好,太小会增加触发GC的次数,比如10秒收集一次,每次100ms,现在变成5秒收集一次,每次70ms,虽然垃圾收集时间减少了,但是吞吐量也减少了.减少GC时间是以吞吐量和新生代空间换取的
- 减少停顿时间适合用户交互使用
-XX:GCTimeRatio
吞吐量大小(大于0,小于100的整数)- 默认值是99,就是允许最大1%(即1/(1+99))的垃圾收集时间
- 增大吞吐量时候后台运算任务
-XX:+UserAdaptSizePolicy
自动配置新生代大小(-Xmn
),Eden与Survivor比例(-XX:SurvivorRadio
),晋升老年代对象年龄(-XX:PretenureSizeThreshold
)等细节参数.称为GC自适应配置的调节策略
老年代垃圾收集器
Serial Old收集器
- 特点
- Serial的老年代收集器,使用”标记-整理算法”
- 单线程收集器
- Client模式默认的收集器
- 在Server模式有两个用途
- 在JDK1.5之前和Parallel Scavenge搭配使用
- CMS的备选方法,在并发收集发生ConcurrentModelFailure时使用
Parallel Old收集器(JDK1.6后才提供的)
诞生背景
- 在Parallel Scavenge收集器在jdk1.6之前只能与Serial Old搭配使用,但是由于Serial Old是单线程处理,无法充分利用躲CPU的处理能力,在老年代很大而且硬件比较高级的环境中,这种组合未必有ParNew + CMS给力
特点
- Parallel Scavenge的老年代收集器,使用”标记-整理算法”
- 多线程收集器
- 为Parallel Scavenge定制
CMS收集器(Current Mark Sweep)
- 特点
- 多线程并发收集器
- 从名字可以看出CMS使用的”标记-清除算法”
- 以获取最短回收停顿时间为目的的收集器(比较符合B/S系统和注重用户体验的服务)
- 有Remembered Set(RSet)的概念,在老年代中有一块区域用来记录指向新生代的引用.这是一种point-out,在进行Young GC时,扫描根时,仅仅需要扫描这一块区域,而不需要扫描整个老年代
- 处理步骤
- 初始标记(CMS initial mark)
- 并发标记(CMS concurrent mark)
- 重新标记(CMS remark)
- 并发清除(CMS concurrent sweep)
- 解析
- 初始标记和重新标记任然需要”Stop The World”
- 初始标记仅仅是标记一下GC Roots能关联到的对象,速度很快.
- 并发标记阶段就是进行GC Roots Tracing的过程
- 重新标记是为了修复并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间比初始标记阶段稍长一点,远比并发标记阶的时间短
- 缺陷
- CMS对于CPU资源比较敏感
- 当CPU较少时,对导致用户性能降低,默认启动的回收线程数(CPU数量+3)/4.虚拟机提供一种称为”增量式并发收集器”(Incremental Current Mark Sweep/i-CMS)的CMS变种,就是标记和清理的时候和用户线程交替运行.实际证明效果一般.
- CMS无法收集浮动垃圾(Floating Garbage)
- 定义: 由于CMS并发清理阶段用户线程还在运行,伴随着程序运行自然还会有新的垃圾不断产生,这一部分垃圾出现在标记之后CMS无法在当次集中处理掉,只好等下一次.由于垃圾收集阶段,用户程序还在运行,所以要预留足够大的内存给用户线程使用.在JDK1.5的默认情况下,CMS在老年代使用了68%为启动阈值.在JDK1.6以后升到92%.
- 修改参数:
-XX:CMSInitiatingOccupancyFraction
修改启动阈值.该值设置过高会导致预留内存过小,要是CMS运行期间预留的内存无法满足程序需要,就会出现一次”Concurrent Model Failure”失败.失败会导致一次Full GC,并启动后备方案:临时启用Serial Old收集器重新进行老年代垃圾收集. - CMS收集结束会产生大量的内存碎片
- 由于CMS使用的是”标记-清除算法”,在给大对象分配空间时,可能会由于无法找到足够大的连续空间来分配,导致提前触发一次Full GC.
- 修改参数:
-XX:+UseCMSCompactAtFullCollection
(默认开启): 用于CMS顶不住要进行Full GC时进行碎片的合并整理.该过程是无法并发的,停顿时间也会变长.-XX:CMSFullGCsBeforeCompaction
(默认值0): 执行多少次不压缩的Full GC后进行一次带压缩的,默认为0,即每次都会压缩合并整理内存.
G1收集器
- 特点
- 并行与并发
- G1能充分利用多CPU,多核环境来缩短”Stop-The-World”,其他部分任然通过并发方式让java程序继续运行
- 分代收集
- 虽然可以不需要其他收集器配合就能独立管理整个GC堆,但它可以采用不同的方式去处理新创建的对象和已经存活了一顿时间,熬过多次GC的旧对象以获得更好的收集效果.
- 空间整合
- 从整体看是基于”标记-整理”的算法实现的,从局部(两个Region之间)看是基于”复制”算法实现的.这两种算法都不会产生内存碎片.
- 可预测的停顿
- 这是G1相对于CMS另一大优势,可以建立可预测的停顿时间模型,能让使用者明确指定一个长度为M毫秒的时间内,消耗在垃圾收集的时间不都超过N毫秒.
- 结构
- 之前的收集器新生代和老年代的内存范围都是独立的,而G1不再是这样.它将整个Java堆分成多个大小相等的独立区域(Region),虽然还有新生代和老年代的概念,但是他们不再是物理隔离的了.
- 工作原理
- G1收集器之所以能建立可预测的停顿时间模型,是因为它可以有计划地避免在整个Java堆中进行全区域的垃圾收集.G1跟踪各个Region里面垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的时间,优先回收价值最大的Region,保证了G1收集器在有限的时间内可以尽可能高的收集效率.
- 实现问题(普遍存在,只是G1更突出)
- 把Java堆分成多个Region后,垃圾收集就可以以Region为单位进行了?Region不可能是孤立的.一个对象分配在某个Region中,他并非只能被本Region中的其他对象使用,而是可以与整个Java堆任意的对象发生引用关系.那在做可达性判断对象是否存活时,岂不是还得扫描整个Java堆才能保证准确性?
- 解决办法
- 在G1收集器中,Region之间的对象引用以及其他的手机器中的新生代与老年代之间的对象引用,虚拟机都是使用Remenbered Set来避免全堆扫描的.G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个Write Barrier暂时中断写操作.检查Reference引用的对象是否处于不同的Region中(在其他收集器就是检查老年代中对象是否引用了新生代中的对象)
- 如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中.当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏.
- 如果不计算维护Remembered Set的操作.G1收集器的运作步骤
- 初始标记(Initial Marking)(Stop The World)
- 并发标记(Concurrent Marking)
- 最终标记(Final Marking)(Stop The World)
- 筛选回收(Live Data Counting and Evacuation)
- 开启选项
-XX:+UseG1GC
- G1内存模型
- 分区(Region)
- 将Java堆分为大小相等的若干区域,不要求存储对象物理空间连续,逻辑连续就可以.分区也不确定为哪个年代服务,可以按需分配.Region被标记了E、S、O和H.其中H(humongous)存储的是巨型对象.当新建对象大小超过Region大小一半时,会在新的一个或者多个Region上分配.
- 配置参数:
-XX:G1HeapRegionSize
来配置每个分区的大小.值必须为2的幂(1MB~32MB),默认分为2048个区域. - 卡片(Card)
- 每个Region内部分为若干个(128字节~512字节)的Card,标识堆内存最小可用内存粒度,分配对象会占用物理空间连续的若干Card,Card有Card Table进行维护.每次内存回收都是对制定分区的Card进行处理.
- Card Table
- 一个字节数组,由Card的下标来标识每个分区的内存地址.默认情况下,每个Card都未必引用,当一个空间被引用时,这个地址空间对应的下标就会变为0,此外RSet也会将这个数组的下标记录下来.
- GC模式
- Young GC(Stop The World)
- 当所有eden region被耗尽无法申请内存时,就会触发一次Young GC,这种触发机制和之前的Young GC差不多,执行完一次Young GC,活跃对象会被拷贝到Survivor Region或者晋升到Old Region中,空闲的Region会被放入空闲列表中,等待下次被使用。
- 设置参数
-XX:MaxGCPauseMillis
: 设置G1收集过程目标时间,默认值200ms.-XX:G1NewSizePercent
: 新生代最小值,默认值5%-XX:G1MaxNewSizePercent
: 新生代最大值,默认值60%
- Mixed GC
- 当越来越多的对象晋升到老年代Old Region时,为了避免堆内存被耗尽,虚拟机会触发一个混合的垃圾收集器Mixed GC.该算法不是Old GC,因为该过程除了回收整个Young Region,并且回收了部分Old Region.
- 设置参数
-XX:InitiatingHeapOccupancyPercent
: 设置当老年代占用超过这个堆大小的百分比时,触发Mixed GC,与CMS的XX:CMSInitiatingOccupancyFraction
类似.
- Full GC
- 如果对象内存分配速度过快,mixed gc来不及回收,导致老年代被填满,就会触发一次Full GC,G1的Full GC算法就是单线程执行的Serial Old,会导致异常长时间的暂停时间,应优化参数,尽可能的避免Full GC.
参考资料
周志明. 深入理解JVM虚拟机