JVM内存垃圾收集

概述

垃圾收集(Garbage Collection)简称GC,这项技术最早诞生于1960年MIT的Lisp语言,Lisp是真正使用内存动态分配和垃圾收集技术的语言。当Lisp还在胚胎时期时,人们就在思考GC需要完成的三件事:

  • 哪些内存需要回收?
  • 什么时候回收?
  • 如何回收?

经过半个多世纪的发展,GC技术已经相当成熟。我们熟悉的Java也是使用内存动态分配和垃圾收集技术的语言,Java的内存分配和回收由JVM管理。JVM将它所管理内存区域被划分成程序计数器、虚拟机栈、本地方法栈、堆、方法区五个部分,其中程序计数器、虚拟机栈、本地方法栈三个区域随线程而生,随线程而灭:栈中的栈帧随着方法的进入和退出而有条不紊地执行者出栈和入栈操作。每一个栈帧中分配多少内存基本上是在类结构确定下来时就已知的,因此这个区域的内存分配和回收具有确定性。不需要考虑垃圾收集的问题。而堆和方法区则不一样,这部分区域的内存分配和回收都是动态的,因此JVM的垃圾收集主要是针对这部分区域。

哪些内存需要回收?

堆中存放着Java程序运行的所有对象实例,JVM对堆进行回收前,首先要确定哪些对象还“存活”着,这些对象占用的内存不需要回收,哪些已经“死去”(即不可能再被任何途径使用的对象),这些对象占用的内存需要回收。

判定对象是否存活的算法有两种:

引用计数算法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器值减1;任何时刻计数器为0的对象就是不可能再被使用的。引用计数算法实现简单,判定效率高,但是它很难解决对象之间相互循环应用的问题。因此主流的JVM都没有选用引用计数算法来管理内存。

可达性分析算法

这个算法的基本思路就是通过一系列的称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。如下图所示,对象object5、object6、object7虽然相互关联,但它们到GC Roots是不可达的,所以它们将会被判定为可回收的对象。

在Java语言中,可作为GC Roots的对象包括下面几种:

虚拟机栈中引用的对象

方法区中类静态属性引用的对象

方法区中常量引用的对象

本地方法栈中引用的对象

 

垃圾收集算法

垃圾收集算法解决的是如何进行垃圾回收的问题。

下面所介绍的垃圾收集算法都是针对堆内存的垃圾回收。为了更好的进行垃圾回收,堆内存可以细分为新生代老年代。JVM根据分代特点有着不同的垃圾回收策略。

标记清除算法

标记-清除(Mark-Sweep)算法,如它的名字一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收掉所有被标记的对象。之所以说它是最基础的收集算法,是因为后续的收集算法都是基于这种思路并对其缺点进行改进而得到的。

它的主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高;另外一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致,当程序在以后的运行过程中需要分配较大对象时无法找到足够的连续内存而不得不提前触发另一次垃圾收集动作。

复制算法

“复制”(Copying)的收集算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。

这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则导致效率降低。

现在的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究表明,新生代中的对象98%是“朝生夕死”的,所以并不需要按照1:1的比例来划分内存空间,而是将新生代分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中存活的对象一次性地复制到另外一块Survivor空间上,最后清理到Eden和刚才用过的Survivor空间。Hotspot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只用10%的内存会被“浪费”掉。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够时,需要依赖其他内存(这里指老年代)进行分配担保(Handle Promotion)。即当另外一块Survivor空间没有足够内存存放上一次新生代收集后存活下来的对象时,这些对象将通过分配担保机制进入老年代。

标记-整理算法

复制收集算法在对象存活率较高时就要执行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

根据老年代的特点,有人提出了另外一种“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存

分代收集算法

当前的商业虚拟机的垃圾收集都采用“分代收集”算法,这种算法并没有什么新思想,只是根据对象存活周期的不同将Java堆内存分为新生代和老年代,根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集都发现有大量对象死去,只有少量存活,那就使用复制算法。而老年代中对象存活率高,没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法。

垃圾收集器

如果说垃圾收集算法是垃圾回收的方法论,那么垃圾收集器则是垃圾回收的具体实现。下面介绍几种不同的垃圾收集器。

Serial收集器

Serial收集器是最基本、历史最悠久的收集器,曾经(在JDK1.3.1之前)是虚拟机新生代收集的唯一选择。它是单线程的收集器,它进行垃圾收集时,必须暂停其他所有的工作线程(Sun将其称为“Stop The World”)。它的优点是:简单而高效,对于运行在Client模式下的虚拟机来说是一个很好的选择

ParNew收集器

ParNew收集器是Serial收集器的多线程版本,它除了多线程收集以外,其他与Serial收集器相比并没有太多创新之处,但它是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的原因是,处理Serial收集器外,目前只有它能与CMS收集器配合工作。

Parallel Scavenge收集器

Parallel Scavenge收集器也是一个新生代收集器,也是使用复制算法。也是使用多线程收集,它的特点在于它的关注点和其他收集器不同,其他收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而它的目标是达到一个可控制的吞吐量。这里的吞吐量定义如下

吞吐量=运行用户代码时间/(运行用户代码时间+垃圾收集时间)

该收集器还有一个特点是自适应调节,就是根据根据当前系统运行情况收集性能监控信息,动态调整参数以提供最合适的停顿时间和最大的吞吐量。

Serial Old收集器

Serial Old是Serial收集器的老年代版本,它同样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义也是被Client模式下的虚拟机使用。

Parallel Old收集器

Parallel Old收集器是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK1.6中才开始提供的,它可以和Parallel Scavenge组合使用,形成“吞吐量优先”的收集器组合。

CMS收集器

在JDK1.5时期,HotSpot推出了一款在强交互应用中几乎可称为有划时代意义的收集器——CMS收集器(Concurrent Mark Sweep),该收集器是HotSpot虚拟机中第一款真正意义上的并发(Concurrent)收集器,它第一次实现了垃圾收集线程与用户线程(基本上)同时工作。

CMS收集器的目标是获取最短回收停顿时间,适合于重视服务响应速度的应用场景。它是老年代收集器,使用“标记-清除”算法。它的运作过程相对于其他收集器来说更复杂一些,整个过程分为4个步骤,包括:

  • 初始标记(CMS initial mark),标记一下GC Roots能直接关联到的对象,速度很快。
  • 并发标记(CMS concurrent mark),进行GC Roots Tracing的过程
  • 重新标记(CMS remark),修正并发标记期间因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录。花费的时间也很短,一般会比初始标记稍长一些。
  • 并发清除(CMS concurrent sweep)

其中初始标记和重新标记两个步骤仍然需要|“Stop The World”。这个过程中,耗时最长的是并发标记和并发清除,由于这两个过程可以与用户线程一起工作,所以,总体而言,CMS收集器的内存回收过程是和用户线程一起并发执行的。下图是CMS收集器运行示意图。

CMS有3明显的缺点:

  • CMS收集器对CPU资源非常敏感

CMS默认启动的回收线程数是(CPU数量+3)/4,当CPU在4个以上时,并发回收时垃圾收集线程不少于25%,并且随着CPU数量的增加而下降。但是当CPU不足4个(比如2个)时,垃圾收集线程占用的CUP资源比重就会过大,可能导致用户程序的执行速度下降。

  • CMS收集器无法处理浮动垃圾

由于CMS并发清理阶段用户线程还在执行,伴随着程序运行自热会有新的垃圾产生,这部分垃圾无法在当次收集中清理掉,只好留到下一次GC时再清理。这部分垃圾就称为“浮动垃圾”。由于垃圾收集阶段用户线程还需要运行,需要预留有足够的内存空间给用户线程使用,因此CMS收集器不像其他收集器那样等到老年代几乎被填满了再进行收集,需要预留一部分空间出来。JDK1.5中,默认当老年代使用了68%时垃圾收集就会被激活。可以通过-XX:CMSInitiatingOccupancyFraction参数的值提高触发百分比。在JDK1.6中,该值已默认设置为92%。如果CMS运行期间预留的内存空间无法满足程序运行需求,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案,临时启用Serial Old收集器重新进行老年代的垃圾收集,这会导致停顿时间增加。因此上述参数值并不是越高越好。

  • 清除后会产生大量空间碎片

由于CMS使用的是“标记-清除”算法,因此收集结束后会有大量空间碎片产生。空间碎片多时,将会给大对象分配带来麻烦,往往当老年代还有很大空间剩余,但是无法找到足够大的连续空间来分配当前对象,不得不提前出发一次Full GC。为了解决这个问题,CMS收集器提供了一个+XX:UseCMSCompactAtFullCollection开关参数,用于在CMS收集器顶不住要进行Full GC时开启内存碎片的合并整理过程,内存整理的过程是无法并发的,空间碎片的问题没有了,但停顿时间不得不变长。虚拟机还提供另外一个参数-XX:CMSFullGCsBeforeCompaction,这个参数用于设置执行多少次Full GC后进行一次碎片整理,默认值是0,表示每次执行Full GC时都会进行碎片整理。、

G1收集器

G1(Garbage-First)收集器是当今收集器技术发展的最前沿成果之一,从JDK1.7开始提供。G1是一款面向服务端应用的垃圾收集器,HotSpot赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与其他收集器相比,G1具备如下特点。

  • 并行与并发:进行垃圾收集时,Java程序可以继续运行。
  • 分代收集:G1可以不需要其他收集器配合就可以独立管理整个GC堆。
  • 空间整合:G1从整体来看是基于“标记-整理”算法实现的收集器,从局部看是基于“复制”算法实现的。无论如何,这两种算法都意味着G1运作期间不会产生空间碎片。
  • 可预测的停顿:这是G1相对于CMS的另一大优势,降低停顿时间是G1和CMS共同的关注点,但G1处理追求低挺顿外,还能建立可预测的停顿时间模型,能让使用者指定在长度为M毫秒的时间片段内,垃圾收集所消耗的时间不超过N毫秒。

G1将整个Java堆划分成多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代不再是物理隔离的了,他们都是一部分Region的集合。

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

G1收集器的运作大致可以分为以下几个步骤:

  • 初始标记
  • 并发标记
  • 最终标记
  • 筛选回收

下面表格是上述收集器的主要信息对比

收集器 运行线程 作用分代 所用算法 特点
Serial 单线程 新生代 复制算法 简单高效
ParNew 多线程 新生代 复制算法 能与CMS配合使用
Parallel Scavenge 多线程 新生代 复制算法 吞吐量优先
Serial Old 多线程 老年代 标记-整理算法 可作为CMS的后备方案
Parallel Old 多线程 老年代 标记-整理算法 JDK1.6开始提供,吞吐量优先
CMS 多线程 老年代 标记-清除算法 垃圾收集可与用户线程并发执行
G1 多线程 新和老 标记-整理算法 可预测停顿时间

以上收集器各有特点,没有孰优孰劣之分,在具体使用中应该根据应用场景选择最合适的收集器。HotSpot虚拟机中对这7种收集器均有提供,并且提供参数供用户根据自己的应用特点组合出各个年代所使用的收集器。下图是JDK1.7Update14之后的HotSpot虚拟机中的收集器示意图

(注:如果两个收集器之间存在连线,就说明它们可以搭配使用)

发布了30 篇原创文章 · 获赞 30 · 访问量 8254

猜你喜欢

转载自blog.csdn.net/IT_GJW/article/details/80334474