GC算法和垃圾收集器

一、概述:
垃圾收集器(Garbage Collection,GC)。运行时区域的程序计数器、虚拟机栈、本地方法栈3个区域随线程而生,随线程而灭。所以这几个区域不需要过多考虑回收问题,因为方法结束和线程结束时,内存自然就跟随着回收了。 gc主要回收的区域是堆和方法区,因为这部分的内存的分配和回收都是动态的

二、判断对象存活状态
1. 引用计数法
给对象添加一个计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。
但是主流的Java虚拟机里面没有选用引用计数算法来管理内存,主要原因是它 很难解决对象之间的相互循环引用的问题
2. 可达性分析算法
基本思想是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜素,搜索走的路径称为 引用链(Reference Chain), 当一个对象到GC Roots没有任何引用链相连时,证明此对象是不可用的
在Java语言中, 可作为GC Roots的对象包括下面几种:
①虚拟机栈(栈帧中的本地变量表)中引用的对象
②方法区中类静态属性引用的对象
③方法区中常量引用的对象
④本地方法栈中JNI(即Native方法)引用的对象

三、垃圾收集算法
1 .标记-清除(Mark-Sweep)算法
算法分为“标记”和“清除”两个阶段:
首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。
不足
①效率问题,标记和清除两个过程的效率都不高
②空间问题:标记清除之后会产生大量 不连续的内存碎片,空间碎片太多可能会导致以后再程序运行过程中需要 分配较大的对象时,无法找到足够的连续内存而不得不 提前触发另一次垃圾收集动作
2 .复制(Copying)算法
为了解决效率问题,“复制”算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样就使得每次都是对整个半区进行内存回收,内存分配时不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存就可以,简单高效。
代价:将内存缩小为了原来的一半,浪费了一半的空间。
现在商用虚拟机都采用 复制算法来回收新生代,原因是有研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。Eden:Survivor0:Survivor1=8:1:1(这里引申出一道面试题:新生代为什么会分区?--就是因为新生代采用的复制算法),每次新生代可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被“浪费”。注意,这里说的98%的对象可回收只是一般场景下的数据,不能保证每次回收都只有小于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行 分配担保(Handle Promotion)
注意:复制算法在对象存活率较高时就要进行较多的复制操作,效率会变低。如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有的对象都100%存活的极端情况,老年代一般不直接用这种算法。

3 .标记-整理(Mark-Compact)算法
根据老年代的对象存活率较高的特点,提出了“标记-整理”算法,标记过程与“标记-清除”算法一样,区别在于后续步骤不是直接对可回收对象进行清理,而是 让所有存活的对象都像一端移动,然后直接清理掉端边界以外的内存
4. 分代收集算法
当前商业虚拟机的垃圾收集算法都采用“分代收集”(Generational Collection)算法,这种算法就是根据对象存活周期的不同将内存划分为几块。一般是把Java堆分为新生代和老年代,根据各个年代的特点采用最适合的收集算法。
新生代中,每次垃圾收集时都发现会有大批对象死去,只有少量存活,所以适合采用 复制算法,只需要付出少量存活对象的复制成本就可以完成收集。
老年代中,对象存活率较高、没有额外空间对它进行分配担保,就必须使用“ 标记-清除”或“标记-整理”算法进行回收。
四、垃圾收集器
如果说收集算法是内存回收的方法论,那么垃圾收集器就是内存回收的具体实现。不同的厂商、不同的版本的虚拟机提供的垃圾收集器可能会有很大差别,《深入理解Java虚拟机》这本书中讨论的收集器基于JDK1.7Update 14之后的HotSpot虚拟机,该虚拟机包含的所有收集器如图所示:
1. Serial收集器
Serial收集器是最基本、发展历史最悠久的收集器,在JDK1.3.1之前是虚拟机新生代收集的唯一选择。
是个 单线程的收集器,注意这里的“单线程”的意义并不仅仅说明它只会使用CPU或一条收集线程去完成垃圾收集工作,而是在它进行垃圾收集时,必须暂停其他所有的工作线程,直到它收集结束,也就是“Stop The World”。
Serial收集器依然是 虚拟机运行在Client模式下的默认新生代收集器,它简单而高效(与其他收集器的单线程比)
2.ParNew收集器
ParNew收集器是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为包括Servial收集器可用的所有控制参数(如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、收集算法、Stop The World、对象分配规则、回收策略等都和Serial收集器完全一样。 运行在Server模式下大的虚拟机中首选的新生代收集器
注意:除了Serial收集器外,目前只有它能与CMS收集器配合工作。
ParNew收集器使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可以使用-XX:+UseParNewGC选项来强制指定它
3.Parallel Scavenge收集器
Parallel Scavenge收集器是一个新生代收集器,使用复制算法,并行的多线程收集器,
和ParNew最大的区别是:自适应调节策略
Parallel Scavenge收集器的目的是达到一个可控制的吞吐量(Througput),吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,
即吞吐量=运行用户代码的时间/(运行用户代码的时间+垃圾收集时间)。
Parallel Scavenge收集器提供了两个参数控制吞吐量:
-XX:MaxGCPauseMillis :最大垃圾收集停顿时间
-XX:GCTimeRation :吞吐量的大小
除了上述两个参数外,还提供了一个参数:-XX:+UseAdaptiveSizePolicy,这是个开关参数,当这个参数打开后,就不需要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象大小(-XX:PretenureSizeThreshold)等细节参数了,虚拟机会根据当前系统的运行情况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC 自适应的调节策略(GC Ergonomics)

4.Serial Old收集器
Serial Old是Serial收集器的老年代版本,同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。如果在Server模式下,那么它主要还有两大用途:
①用在JDK1.5以及之前的版本中与Parallel Scavenge收集器搭配使用
②作为CMS收集器的后背预案,在并发收集发生Concurrent Mode Failure时使用。

5.Parallel Old收集器
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器在JDK1.6中开始提供的。

6.CMS收集器
CMS(Concurrent Mark Sweep)收集器是一种以 获取最短回收停顿时间为目标的收集器。基于“ 标记-清除”算法。
整个过程分为4个步骤:
①初始标记(CMS initail mark): Stop The World,标记GC Roots能直接关联到的对象,速度快,t1
②并发标记(CMS concurrent mark):进行GC Roots Tracing过程,时间长 t2
③重新标记(CMS remark): Stop The World,修正并发标记期间因用户进程继续运作而导致的标记 产生变动的部分 t3
④并发清除(CMS concurrent sweep) 时间长
t1 < t3 < t2
整个过程中耗时最长的并发标记和并发清除过程收集器都可以与用户线程一起工作,所以从总体上来说,CMS收集器的内存回收过程是与用户线程一起并发执行的。
CMS是一款优秀的收集器,主要优点:并发收集、低停顿。但 CMS还是有缺点,如下:
①CMS收集器对CPU资源非常敏感。在并发阶段,它虽然不会导致用户线程停顿,但是会因为占用了一部分线程(或者说CPU资源)而导致应用程序变缓,总吞吐量降低。
②CMS无法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure“失败而导致另一次Full GC的产生。因为CMS并发清理阶段用户线程还运行着,伴随程序运行自然就还会有新的垃圾不断产生,这部分垃圾出现在标记过程之后,CMS无法再当次收集中处理掉他们,只好留到下一次GC时再清理掉。这一部分垃圾就被称为“ 浮动垃圾”。因为垃圾收集阶段用户线程还需要执行,所以还需要预留有足够的内存空间给用户线程使用。
在JDK1.5的默认配置下,CMS收集器当老年代增长不是太快,可以适当调高参数-XX:CMSInitiationOccupancyFraction的值来提高触发百分比,降低内存回收次数从而获得更好的性能
在JDK1.6中,CMS收集器的启动阈值已经提升至92%。要是CMS运行期预留的内存无法满足程序需要,就会出现一次“Concurrent Mode Failure”失败,虚拟机会启动后备预案:临时启用Serial Old收集器来重新进行老年代的垃圾收集,这样停顿时间就会变长了。所以参数-XX:CMSInitiatingOccupanyFraction设置太高很容易导致大量“Concurrent Mode Failure”失败,性能反而降低。
③因为CMS是基于“标记-清除”算法实现的,意味着收集结束时会有大量的空间碎片产生
空间碎片过多时,将会给大对象分配带来很大麻烦,会出现老年代还有很大的剩余空间,但是无法找到足够大的连续空间来分配当前对象,不得不提前触发一次Full GC。为了解决这个问题,CMS收集器提供了一个 -XX:+UseCMSCompactAtFullCollection开关参数(默认开启),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片问题没有了,但是停顿时间变长了。
虚拟机还提供了另外一个参数 -XX:CMSFullGCsBeforeCompaction,这个参数用于设置执行多少次不压缩的Full GC后,然后来一次带压缩的(默认值是 0,表示每次进入Full GC时都进行碎片整理)

7.G1收集器
G1(Garbage-First)收集器是当今收集器技术发展最前沿成果之一,它是一款面向服务端应用的垃圾收集器。HotSpot开发团队赋予它的使命是未来卡伊替换掉JDK1.5中发布的CMS收集器。
与其他收集器相比,G1具备的 特点
①并行与并发
G1能充分利用多CPU、多核环境下的硬件优势,缩短Stop-The-World停顿的时间
②分代收集
虽然G1收集器可以不需要其他收集器配合就能独立管理整个GC堆,但还保留分代的概念,采用不同的方式去处理新创建的对象和已经存活了一段时间、熬过多次GC的旧对象以获取更好的收集效果。
③空间整合
G1从整体来看是基于“标记-整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法实现的。这两种算法意味着G1运作期间不会产生内存空间碎片,收集后能提供规整可用内存。这种特性有利于程序长时间运行,分配大对象时不会因为无法找到连续内存空间而提前触发下一次GC。
④可预测的停顿
这是G1相对于CMS的另一大优势,G1和CMS共同关注降低停顿时间,但G1除了关注低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定一个长度为M毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎是实时Java的垃圾收集器特征了。

在G1之前的其他收集器进行收集的范围都是整个新生代或者老年代,而G1不再是这样,使用G1收集器时,Java堆的内存布局就于其他收集器有很大差别,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,它们都是一部分Region(不需要连续)的集合。

G1收集器为何能建立可预测的停顿时间模型?因为它可以有计划地避免在整个Java堆中进行全区域的垃圾回收。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需要时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region(这就是Garbage-First名称的由来)。这种使用Region划分内存空间以及优先级的区域回收方式,保证了G1收集器在有限的时间内可以获取尽可能高的收集效率。

关于G1收集器中的引用:Region之间的对象引用以及其他收集器中的新生代和老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。G1每个Region都有一个与之对应的Rememebered Set,虚拟机发现程序在对Reference类型的数据进行写操作时,会产生一个
Write Barrier 暂时中断写请求,检查Reference引用的对象是否处于不同的Region之中(在分代的例子就是检查是否老年代中的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set之中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

如果不计算维护Remembered Set的操作,G1收集器的步骤如下:
①初始标记(Initial Marking)
标记一下GC Roots能直接关联到的对象,并且修改TAMS(Next Top at Mark Start)的值,让下一阶段用户程序并发运行时,能在正确可用的Region中创建对象,这个阶段需要停顿线程,但耗时很短。
②并发标记(Concurrent Marking)
从GC Roots开始对堆中对象进行可达性分析,找出存活的对象,该阶段耗时较长,但是可与用户程序并发执行
③最终标记(Final Marking)
为了修正在并发标记期间因为用户程序继续运作而导致标记产生变动的那一部分标记记录,虚拟机将这段时间对象变化记录在线程Remembered Set Logs里面,最终标记阶段需要把Remembered Set Logs的数据合并到Remembered Set中,该阶段需要停顿线程,但是可以并行执行。
④筛选回收(Live Data Counting and Evacuation)
首先对各个Region的回收价值和成本进行排序,根据用户所期望的GC停顿时间来制定回收计划,其实这个阶段也可以做到与用户程序一起并发执行,但因为只回收一部分Region,时间是用户可控的,而且停顿用户线程将大幅度提高收集效率。
备注:以上的内容是周志明老师的书《深入理解Java虚拟机》中内容,自己整理出来的。


















猜你喜欢

转载自blog.csdn.net/dam454450872/article/details/79826918