浅析JVM的垃圾回收器

JVM的GC经过多年的发展,大家对Minor GC、major GC的理解并不完全一致,所以我不打算在本文中使用这个概念。我把GC大概分为一下4类:

  • Young GC:只是负责回收年轻代对象的GC;
  • Old GC:只是负责回收老年代对象的GC;
  • Full GC:回收整个堆的对象,包括年轻代、老年代、持久带;
  • Mixed GC:回收年轻代和部分老年代的GC (G1);

因为笔者目前使用G1还是比较少的,所以本文不打算将G1。

垃圾回收器算法

目前主流垃圾回收器都采用的是可达性分析算法来判断对象是否已经存活,不使用引用计数算法判断对象时候存活的原因在于该算法很难解决相互引用的问题。

标记-清除算法(Mark-Sweep)

标记-清除算法由标记阶段和清除阶段构成。标记阶段是把所有活着的对象都做上标记的阶段;清除阶段是把那些没有标记的对象,也就是非活动对象回收的阶段。通过这两个阶段,就可以令不能利用的内存空间重新得到利用。

从标记-清除算法我们可以看出,该算法不涉及对象移动,但是可能会产生内存碎片化问题。空间碎片太高可能会导致程序运行时需要分配较大内存时候,无法找到足够的连续内存,需要其他垃圾回收帮助回收内存。

复制算法(Copying)

复制算法内存空间分为两块区域:From、to,每次只使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收。

上面那种复制算法有一半的空间是浪费的。所以在Java新生代把内存区域分为Eden空间、from、to空间3个部分,from和to空间也称为survivor 空间,用于存放未被回收的对象。对象开始都是Eden生成;当回收时,将Eden和from中存活的对象移动到to区域中。

复制算法存在空间浪费的情况,始终都要保持一个Survivor是空闲的,并且在GC的时候要是存活对象大小超过了Survivor中的大小,就需要另外的策略存储存活对象。

目前open JDK新生代回收策略就是采用的复制算法,其中Eden和Survivor的默认配置为8:1

标记-压缩算法(Mark-Compact)

标记-压缩算法由标记阶段和压缩阶段构成。标记阶段标记-清除算法中的标记阶段完全一样,压缩阶段是让所有存活的对象向一端移动。这样空闲内存都在另外一端,属于连续空间,不存在内存碎片化问题,但是会产生对象移动。

分代算法(Generational GC)

根据对象的不同生命周期分别管理, JVM 中将对象分为我们熟悉的新生代、老年代和永久代分别管理。这样做的好处就是可以根据不同类型对象进行不同策略的管理,例如新生代中对象更新速度快,就会使用效率较高的复制算法。老年代中内存空间相对分配较大,而且时效性不如新生代强,就会常常使用Mark-Sweep-Compact (标记-清除-压缩)算法。

各种算法性能比较

常见的垃圾回收器

垃圾回收器分类

总体上可以把Java的垃圾回收器分为3类:

  • 串行垃圾回收器(Serial Garbage Collector)
  • 并行垃圾回收器(Parallel Garbage Collector)
  • 并发标记扫描垃圾回收器(CMS Garbage Collector)

Java垃圾回收器主要有6种,各自优缺点以及组合关系如下:

其中的连线表示young gc和old gc可以搭配使用

垃圾回收器选择策略:

  • 客户端程序:Serial + Serial Old;
  • 吞吐率优先的服务端程序(比如:计算密集型):Parallel Scavenge + Parallel Old;
  • 响应时间优先的服务端程序:ParNew + CMS。

目前很大一部分的Java应用都集中在互联网的服务器端,这类应用尤其关系服务的响应时间,希望应用暂停时间更短,所以基本上使用的都是ParNew + CMS,这也是我司默认使用的配置。

CMS垃圾回收器

在启动JVM参数加上 -XX:+UseConcMarkSweepGC ,这个参数表示对于老年代的回收采用 CMS。

CMS执行过程

CMS 的回收过程主要分为下面的几个步骤:

  • 初始标记(Initial Mark)
  • 并发标记(Concurrent marking)
  • 并发预清理(Concurrent pre-preclean)
  • 重新标记(Final Remark)
  • 并发清理(Concurrent sweep)
  • 并发重置(Concurrent reset)

 CMS日志解析

标准的CMS日志如下:

2018-11-10T18:23:27.531+0800: 1495270.652: [GC (CMS Initial Mark) [1 CMS-initial-mark: 2008820K(2510848K)] 2038212K(4398336K), 0.0231086 secs] [Times: user=0.01 sys=0.00, real=0.03 secs] 

2018-11-10T18:23:27.554+0800: 1495270.675: [CMS-concurrent-mark-start]

2018-11-10T18:23:27.644+0800: 1495270.765: [CMS-concurrent-mark: 0.090/0.090 secs] [Times: user=0.34 sys=0.03, real=0.09 secs] 

2018-11-10T18:23:27.644+0800: 1495270.765: [CMS-concurrent-preclean-start]

2018-11-10T18:23:27.654+0800: 1495270.775: [CMS-concurrent-preclean: 0.010/0.010 secs] [Times: user=0.02 sys=0.00, real=0.01 secs] 

2018-11-10T18:23:27.655+0800: 1495270.775: [CMS-concurrent-abortable-preclean-start]

2018-11-10T18:23:32.305+0800: 1495275.425: [CMS-concurrent-abortable-preclean: 4.623/4.650 secs] [Times: user=7.01 sys=1.01, real=4.65 secs] 

2018-11-10T18:23:32.307+0800: 1495275.427: [GC (CMS Final Remark) [YG occupancy: 847369 K (1887488 K)]1495275.427: [Rescan (parallel) , 0.0902177 secs]1495275.518: [weak refs processing, 0.0514433 secs]1495275.569: [class unloading, 0.0256119 secs]1495275.595: [scrub symbol table, 0.0074695 secs]1495275.602: [scrub string table, 0.0015014 secs][1 CMS-remark: 2008820K(2510848K)] 2856190K(4398336K), 0.1806988 secs] [Times: user=0.68 sys=0.00, real=0.18 secs] 

2018-11-10T18:23:32.488+0800: 1495275.609: [CMS-concurrent-sweep-start]

2018-11-10T18:23:33.660+0800: 1495276.781: [CMS-concurrent-sweep: 1.172/1.172 secs] [Times: user=1.89 sys=0.24, real=1.17 secs] 

2018-11-10T18:23:33.661+0800: 1495276.782: [CMS-concurrent-reset-start]

2018-11-10T18:23:33.667+0800: 1495276.788: [CMS-concurrent-reset: 0.006/0.006 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]

初始标记(CMS Initial Mark)

  • 该阶段进行可达性分析,标记GC ROOTS能直接关联到的对象。该阶段会暂停应用。
  • 2008820K – 当前老年代使用情况;
  • (2510848K) – 老年代可用容量;
  • 2038212K – 当前整个堆的使用情况;
  • (4398336K) – 整个堆的容量;
  • .0231086 secs] [Times: user=0.01 sys=0.00, real=0.03 secs] – 时间计量;

并发标记(CMS-concurrent-mark)

并发标记就需要标记出 GC ROOTS 关联到的对象的引用对象有哪些。比如说 A -> B (A 引用 B,假设 A 是 GC Roots 关联到的对象),那么这个阶段就是标记出 B 对象, A 对象会在初始标记中标记出来。

并发预清理(CMS-concurrent-preclean)

这个阶段主要并发查找在做并发标记阶段时从年轻代晋升到老年代的对象或老年代新分配的对象(大对象直接进入老年代)或被用户线程更新的对象,来减少重新标记阶段的工作量。

重新标记(CMS Final Remark)

由于在并发标记和并发预清理这个阶段,用户线程和GC 线程并发,假如这个阶段用户线程产生了新的对象,总不能被 GC 掉吧。这个阶段就是为了让这些对象重新标记。该阶段也会暂停应用

  • YG occupancy: 847369 K (1887488 K)]– 年轻代当前占用情况和容量;
  • Rescan (parallel) , 0.0902177 secs – 这个阶段在应用停止的阶段完成存活对象的标记工作;
  • weak refs processing, 0.0514433 secs – 第一个子阶段,随着这个阶段的进行处理弱引用;
  • class unloading, 0.0256119 secs– 第二个子阶段(that is unloading the unused classes, with the duration and timestamp of the phase);
  • scrub symbol table, 0.0074695 secs– 最后一个子阶段(that is cleaning up symbol and string tables which hold class-level metadata and internalized string respectively)
  • 2008820K(2510848K)]– 在这个阶段之后老年代占有的内存大小和老年代的容量;
  • 2856190K(4398336K)– 在这个阶段之后整个堆的内存大小和整个堆的容量;
  • 0.1806988 secs – 这个阶段的持续时间;
  • [Times: user=0.68 sys=0.00, real=0.18 secs] – 同上;

并发清理(CMS-concurrent-sweep)

这个阶段的目的就是移除那些不用的对象,回收他们占用的空间并且为将来使用。注意这个阶段会产生新的垃圾,新的垃圾在此次GC无法清除,只能等到下次清理。这些垃圾有个专业名词:浮动垃圾。

并发重置(CMS-concurrent-reset)
CMS清除内部状态,为下次回收做准备。

注意:CMS虽然是老年代算法,但也是需要扫描新生代区域的。

CMS算法降级

cms存在着内存碎片化问题:申请内存时,虽然总内存大于申请内存,但是没有连续内存大于申请内存,导致内存申请失败。CMS提供了机制(CMS GC降级到Full GC)来解决该问题。Full GC使用的算法是mark-sweep-compact(类似于Serial垃圾回收器),他的作用域在整个堆的对象,包括年轻代、老年代、持久代,但compaction是可选的。其中参数CMSFullGCsBeforeCompaction=N表示每隔N次真正的full GC才做一次压缩(而不是每N次CMS GC就做一次压缩,目前JVM里没有这样的参数),CMSFullGCsBeforeCompaction默认值是0,也就是每次full GC都会进行内存压缩。这个尽量使用默认值,不然内存碎片化可能会更严重些。

那么配置的CMS GC啥时候会触发Full gc呢?主要有下面几种情况触发Full Gc:

  1. 旧生代空间不足:java.lang.outOfMemoryError:java heap space;
  2. Perm空间满:java.lang.outOfMemoryError:PermGen space;
  3. CMS GC时出现promotion failed(当进行 Young GC 时,有部分新生代代对象仍然可用,但是S0或S1放不下,因此需要放到老年代,但此时老年代空间无法容纳这些对象) 和concurrent mode failure(当 CMS GC 正进行时,此时有新的对象要进行老年代,但是老年代空间不足造成的);
  4. 统计得到的minor GC晋升到旧生代的平均大小大于旧生代的剩余空间;
  5. 主动触发Full GC(System.gc()、jmap等)。

如何识别是执行的是CMS GC还是 Full GC呢?主要是根据GC log,CMS GC会在日志中标记出各个执行阶段,但是要是执行Full GC只会显示full次数加1。

CMS相关参数

-XX:CMSInitiatingOccupancyFraction=N 和-XX:+UseCMSInitiatingOccupancyOnly

这两个设置一般配合使用, 目的在于降低CMS GC频率或者增加频率。

-XX:CMSInitiatingOccupancyFraction=N 是指设定CMS在对内存占用率达到N%的时候开始进行CMS GC。

-XX:+UseCMSInitiatingOccupancyOnly 只是用设定的回收阈值(上面指定的N%),如果不指定,JVM仅在第一次使用设定值,后续则自动调整.

-XX:+CMSScavengeBeforeRemark

这个参数表示CMS GC前启动一次ygc,目的在于减少old区域对ygc区域的引用,降低remark时的开销,一般CMS的GC耗时80%都在remark阶段

-XX:+UseCMSCompactAtFullCollection和-XX:CMSFullGCsBeforeCompaction=N

这两个参数要配合使用,其中CMSFullGCsBeforeCompaction上面已经讲解过了。

CMS 的缺点

  1. 会产生空间碎片。CMS 垃圾回收器采用的基础算法是 Mark-Sweep,没有内存整理的过程,所以经过 CMS 收集的堆会产生空间碎片。
  2. 对CPU资源非常敏感。为了让应用程序不停顿,CMS 线程需要和应用程序线程并发执行,这样就需要有更多的 CPU,同时会使得总吞吐量降低。
  3. CMS无法处理浮动垃圾,所以一般需要更大的堆空间。因为CMS 在标记阶段应用程序的线程还是在执行的,那么就会有堆空间继续分配的情况,为了保证在 CMS 回收完堆之前还有空间分配给正在运行的应用程序,必须预留一部分空间。

以上就是浅析JVM的垃圾回收器的详细内容,更多关于jvm 垃圾回收器的资料请关注我们其它相关文章!

(0)

相关推荐

  • JVM的垃圾回收算法工作原理详解

    怎么判断对象是否可以被回收? 共有2种方法,引用计数法和可达性分析 1.引用计数法 所谓引用计数法就是给每一个对象设置一个引用计数器,每当有一个地方引用这个对象时,就将计数器加一,引用失效时,计数器就减一.当一个对象的引用计数器为零时,说明此对象没有被引用,也就是"死对象",将会被垃圾回收. 引用计数法有一个缺陷就是无法解决循环引用问题,也就是说当对象A引用对象B,对象B又引用者对象A,那么此时A,B对象的引用计数器都不为零,也就造成无法完成垃圾回收,所以主流的虚拟机都没有采用这种算法

  • 浅谈jvm中的垃圾回收策略

    java和C#中的内存的分配和释放都是由虚拟机自动管理的,此前我已经介绍了CLR中GC的对象回收方式,是基于代的内存回收策略,其实在java中,JVM的对象回收策略也是基于分代的思想.这样做的目的就是为了提高垃圾 回收的性能,避免对堆中的所有对象进行检查时所带来的程序的响应的延迟,因为jvm执行GC时,会stop the word,即终止其它线程的运行,等回收完毕,才恢复其它线程的操作.基于分代的思想是:jvm在每一次执行垃圾收集器时,只是对一小部分内存 对象引用进行检查,这一小部分对象的生命周

  • JVM的垃圾回收机制详解和调优

    文章来源:matrix.org.cn 作者:ginger547 1.JVM的gc概述 gc即垃圾收集机制是指jvm用于释放那些不再使用的对象所占用的内存.java语言并不要求jvm有gc,也没有规定gc如何工作.不过常用的jvm都有gc,而且大多数gc都使用类似的算法管理内存和执行收集操作. 在充分理解了垃圾收集算法和执行过程后,才能有效的优化它的性能.有些垃圾收集专用于特殊的应用程序.比如,实时应用程序主要是为了避免垃圾收集中断,而大多数OLTP应用程序则注重整体效率.理解了应用程序的工作负荷

  • JVM垃圾回收算法的概念与分析

    前言 在JVM内存模型中会将堆内存划分新生代.老年代两个区域,两块区域的主要区别在于新生代存放存活时间较短的对象,老年代存放存活时间较久的对象,除了存活时间不同外,还有垃圾回收策略的不同,在JVM中中有以下回收算法: 标记清除 标记整理 复制算法 分代收集算法 有了垃圾回收算法,那JVM是如果确定对象是垃圾对象的呢?判断对象是否存活JVM也会有几套自己判断算法了: 引用记数 可达性分析 有了垃圾回收和判断对象存在这两个概念后,再来逐步分析它们. JVM是如何判断对象是否存活的? 要是让开发人员来

  • 快速理解Java垃圾回收和jvm中的stw

    Java中Stop-The-World机制简称STW,是在执行垃圾收集算法时,Java应用程序的其他所有线程都被挂起(除了垃圾收集帮助器之外).Java中一种全局暂停现象,全局停顿,所有Java代码停止,native代码可以执行,但不能与JVM交互:这些现象多半是由于gc引起. GC时的Stop the World(STW)是大家最大的敌人.但可能很多人还不清楚,除了GC,JVM下还会发生停顿现象. JVM里有一条特殊的线程--VM Threads,专门用来执行一些特殊的VM Operation

  • JVM教程之内存管理和垃圾回收(三)

    JVM内存组成结构 JVM栈由堆.栈.本地方法栈.方法区等部分组成,结构图如下所示: 1)堆 所有通过new创建的对象的内存都在堆中分配,其大小可以通过-Xmx和-Xms来控制.堆被划分为新生代和旧生代,新生代又被进一步划分为Eden和Survivor区,最后Survivor由From Space和To Space组成,结构图如下所示: 新生代.新建的对象都是用新生代分配内存,Eden空间不足的时候,会把存活的对象转移到Survivor中,新生代大小可以由-Xmn来控制,也可以用-XX:Surv

  • 从JVM的内存管理角度分析Java的GC垃圾回收机制

    一个优秀的Java程序员必须了解GC的工作原理.如何优化GC的性能.如何与GC进行有限的交互,因为有一些应用程序对性能要求较高,例如嵌入式系统.实时系统等,只有全面提升内存的管理效率 ,才能提高整个应用程序的性能.本篇文章首先简单介绍GC的工作原理之后,然后再对GC的几个关键问题进行深入探讨,最后提出一些Java程序设计建议,从GC角度提高Java程序的性能.     GC的基本原理     Java的内存管理实际上就是对象的管理,其中包括对象的分配和释放.     对于程序员来说,分配对象使用

  • 浅析JVM垃圾回收的过程

    JVM垃圾回收的算法很多,但是不管是哪种算法,在进行GC时大致的流程都是差不多的,主要有以下3个过程: 1. 枚举根节点 这个过程主要是找到所有的GC Roots对象,这些对象一般发生在JVM虚拟机栈栈帧.常量池中的静态对象.方法区中静态类属性引用.本地方法栈中引用的对象.这个过程会发生STW,所有的线程均运行到安全区域(Safe Region)才开始执行. 通常有两种算法: 引用计数法:每个对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就+1:当引用失效时,计数器值就-1:任何时刻

  • JVM垃圾回收原理解析

    概述 Java运行时区域中,程序计数器,虚拟机栈,本地方法栈三个区域随着线程的而生,随线程而死,这几个区域的内存分配和回收都具备确定性,不需要过多考虑回收问题.而Java堆和方法区则不一样,一个接口的多个实现类需要的内存不一样,一个方法的多个分支需要的内存可能也不一眼,我们只有在运行期,才能知道会创建的对象,这部分的内存分配和回收,是垃圾回收器所关注的.垃圾回收器需要完成三个问题:那些内存需要回收:什么时候回收以及如何回收. 那些垃圾需要回收 垃圾回收的基本思想是考察一个对象的可达性,即从根节点

  • JVM的7种垃圾回收器(小结)

    垃圾回收算法和垃圾回收器 对于JVM的垃圾回收算法有复制算法.标记清除.标记整理. 用阳哥的话就是:这些算法只是天上飞的理念,是一种方法论,但是真正的垃圾回收还需要有落地实现,所以垃圾回收器应运而生. JVM回收的区域包括方法区和堆,jvm对于不同区域不同的特点采用分代收集算法,比如因为所有的对象都是在Eden区进行分配,并且大部分对象的存活时间都不长,都是"朝生夕死"的,每次新生代存活的对象都不多,所以新采取复制算法:而jvm默认是新生代的对象熬过15次GC才能进入老年代,所以老年代

  • 详解Java内存管理中的JVM垃圾回收

    一.概述 相比起C和C++的自己回收内存,JAVA要方便得多,因为JVM会为我们自动分配内存以及回收内存. 在之前的JVM 之内存管理 中,我们介绍了JVM内存管理的几个区域,其中程序计数器以及虚拟机栈是线程私有的,随线程而灭,故而它是不用考虑垃圾回收的,因为线程结束其内存空间即释放. 而JAVA堆和方法区则不一样,JAVA堆和方法区时存放的是对象的实例信息以及对象的其他信息,这部分是垃圾回收的主要地点. 二.JAVA堆垃圾回收 垃圾回收主要考虑的问题有两个:一个是效率问题,一个是空间碎片问题.

随机推荐