java中的GC

概述

在上一章中,咱们了解到程序计数器、虚拟机栈、本地方法栈等线程私有的区域的生命周期都是跟随线程的生命周期的变化而变化的,而java堆和方法区等线程共享的区域的生命周期咱们就没有介绍了。html

所以,本章的主要内容就是介绍java堆和方法区等线程共享的区域上管理,即java堆和方法区上GC。咱们将从如下几个方面进行展开:java

  • 什么样的对象是垃圾对象(对象回收的原则)
  • 回收的原理
  • 分析与调优的工具

要特别注意,本章节主要讨论的是java堆和方法区上的GC。web

什么样的对象可以回收

要想了解GC,首先要了解什么是垃圾对象,或者说是什么样的对象才符合垃圾回收的原则?所谓的垃圾就是在jvm中用不到的内存区域,由于内存资源老是有限的,用不到的内存必定要从新被操做系统收集整理后再次使用。因此,垃圾回收的本质就是对内存空间的从新整理,把符合特定条件的内存区域从新回收,以供操做系统后期再次使用。算法

而线程私有的内存空间由线程本身管理,那线程共享的区域的管理就须要额外的功能模块来管理了,这个额外的功能模块就是垃圾回收器,又由于线程共享的区域上存储的主要内容是java的类对象和方法。所以咱们就要研究什么样的对象才可以被回收。浏览器

研究对象是否被回收,只须要研究对象是否被使用便可。换句话来讲,就是研究对象是否被引用。目前来讲,判断对象是否被引用有两种方式:安全

引用计数法:为每个对象都建立一个引用计数的属性,若是有其余对象引用这个对象,就为这个对象的引用计数的属性加一,引用释放时计数就减一,计数为0的时候就能够被回收了。这种方式没法解决对象之间相互引用的问题。固然java中也不会使用这种方式来判断对象是否存活了。服务器

可达性分析法:引入GCroot概念,若是在GCroot和一个对象之间没有可达路径,那么对象就是不可达的。要注意的是:不可达对象不等价于可回收对象,不可达对象变成回收对象至少要通过两次标记过程,两次标记以后仍然是可回收对象,那将面临回收。多线程

在Java语言中,GC Roots包括:并发

  • 虚拟机栈中引用的对象。
  • 方法区中类静态属性实体引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI引用的对象。

TODO: 可达性分析法的具体实现过程oracle

因为java虚拟机规范中,没有要求虚拟机在方法区实现垃圾收集,所以方法区的垃圾收集是根据具体的虚拟机实现来肯定的。在HotSpot中,方法区就是永久区。永久区的垃圾回收对象是废弃常量和无用的类。

判断一个类是否无用比较苛刻,要同时知足如下3个方面的条件:

  • 该类全部的实例都已经回收,也就是 Java 堆中不存在该类的任何实例
  • 加载该类的 ClassLoader 已经被回收
  • 该类对应的 java.lang.Class 对象没有任何地方呗引用,没法在任何地方经过反射访问该类的方法

垃圾回收

垃圾回收由两部分组成,一部分是GC算法,另外一部分是实现GC算法的回收器。

算法基础

本小节主要研究垃圾回收算法的种类、具体实现原理、适用对象、优缺点等内容。

  • 标记-整理算法

    • 算法分为“标记”和“清除”两个阶段:首先标记出全部须要回收的对象,在标记完成后统一回收掉全部被标记的对象
    • 它是最基础的算法,由于后续全部的算法都是在此算法基础上改进而来的
    • 有两个缺点:一个是效率问题,标记和清除的效率都不高;另外一个是空间问题,标记清除后悔产生大量不连续的内存碎片,碎片过多或可由于在分配大对象内存空间时找不到足够连续空间而不得不提早触发垃圾收集动做
  • 复制算法

    • 它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,而后再把已使用过的内存空间一次清理掉
    • 这种算法的代价是要内存空间的浪费,由于有一半空间一直在空闲状态,此外,对于长生存期的对象来讲,复制会致使效率下降
    • IBM研究代表,新生代中98%的对象是“朝生夕死”的,所以也没有必要按照1:1的比例来划份内存空间
    • HopSpot中Eden和Servivor的大小比例是8:1,即每次垃圾回收后,新生代可用空间为总容量的90%(80%+10%),这样就只有10%的内存空间被浪费了;
    • 固然,还有一个分配担保,意思就是,若是某次垃圾回收有多余10%的对象,那多余的对象能够由老年代进行担保,即经过担保机制,进入老年代
  • 标记-清除算法

    • 根据老年末的特色,使用标记-清除算法,标记清除跟标记整理的第一个步骤同样,都是标记过程,只不过第二步骤是,将全部存活对象向一端移动,而后清除掉边界之外的内存
  • 分代回收算法

    • 把堆分为新生代和老年代,而后根据各个年代的特色采用最适当的收集算法
    • 在新生代中,因为只有少许对象存活,就选用复制算法;老年代中对象存活率高、没有额外空间对其分配担保,就使用标记-清除算法或标记-整理算法

回收器

在早起的jdk版本中,咱们关注的是清理jvm中全部的垃圾,所以产生了单线程的Serial收集器和Serial Old收集器,这保证了清理时的稳定性与高效性,但这样一样会带来一下问题,如因为清理线程独占系统资源形成用户程序线程暂停而引发全局停顿的问题;后来,咱们开始关注系统的吞吐量(即用户应用程序运行时间与总运行时间的比值,吞吐量从某种意义上也体现了全局停顿时间的概念),咱们认为要提升系统的吞吐量,所以产生了Parallel Scavenge收集器和Parallel Old收集器;再后来,咱们把关注点转移到减小全局停顿时间上,所以产生了跨时代的CMS收集器。

此外,因为在垃圾收集过程当中,会产生全局停顿的现象,这就引出咱们评判一个垃圾收集器性能好坏的评判标准:咱们在评判一个垃圾收集器性能好坏时,本质上讲,咱们是在讨论这个垃圾收集器在收集垃圾时所产生的全局停顿时间的长短,即全局停顿时间越短,垃圾收集器的性能越好。可是这句话不是绝对的,咱们只是提供了一种观察垃圾收集器性能好坏的方法。

下面的内容,咱们是从分代的角度去讲述垃圾收集器的种类,固然读者也能够从垃圾收集器的发展阶段中的关注点来划分。本小节中也将主要讲解各类回收器的种类、适用范围、实现原理、优缺点、参数控制等内容。

上面的图很是重要! 说明: 若是两个收集器之间存在连线说明他们之间能够搭配使用。

新生代收集器

  • Serial收集器

    • 采用的是复制算法,
    • 只会使用一个CPU或者是一个线程去完成垃圾收集工做,而且在进行垃圾收集的同时,必须暂停其余全部的工做线程,直至垃圾收集结束
    • 使用-XX:+UseSerialGC参数控制
    • 是Client模式下默认的新生代的垃圾收集器
    • 优势
      • 单线程的垃圾收集效率较高
      • 稳定性高
      • 多适用到单核
      • 全局停顿时间较少
  • ParNew收集器

    • 是serial收集器的多线程版本
    • 是许多运行在server模式下的虚拟机中首选的新生代收集器
    • 默认开启和CPU数目相同的线程数,能够经过-XX:ParallelGCThreads参数来限制垃圾收集器的线程数
  • Parallel Scavenge收集器

    • 采用的是复制算法
    • 它重点关注的是程序要达到一个可控制的吞吐量(即运行代码时间/(运行代码时间+垃圾收集时间)),所以也叫作吞吐量收集器
    • 使用-XX:MaxGCPauseMillis参数来控制最大垃圾收集停顿时间
    • 使用-XX:GCTimeRatio参数来直接设置吞吐量大小
    • 使用-XX:+UseAdaptiveSizePolicy参数来开启GC自适应的调节策略,这个也是与Par New收集器的重要区别

三个新生代收集器所采用的收集算法都是复制算法,而且它们的功能也是逐步完善的。ParNew收集器在Serial收集器的基础上面加上了多线程的功能,Parallel Scavenge收集器又在ParNew收集器的基础上加了控制吞吐量的控制功能。

老年代收集器

  • Serial Old收集器

    • 采用标记-整理算法
    • client模式下的默认的老年代垃圾收集器
    • 在server模式下,有两大用途:
      • 在jdk1.5后搭配Parallel Scavenge收集器使用
      • 做为CMS收集器的后备预案
  • Parallel Old收集器

    • 采用标记-整理算法
    • 是Parallel Scavenge收集器的老年代版本,言外之意,Parallel Old收集器也一样具备保证吞吐量的功能
  • :star: CMS收集器

    • 全称:Concurrent mark sweep(CMS)
    • 使用标记-清除算法
    • 是一种以获取最短回收停顿时间为目标的收集器
    • 整个过程分为四个步骤:
      • 初始标记(initial mark):只是标记GCRoots能直接关联的对象,要暂停全部的工做线程
      • 并发标记(concurrent mark):进行GCRoots跟踪的过程,和用户线程一块儿工做,不须要暂停工做线程
      • 从新标记(remark):修正因用户线程继续运行而致使标记变更的那一部分对象的标记记录,仍须要暂停全部的工做线程
      • 并发清除(concurrent sweep):清除GCRoots不可达对象,和用户线程一块儿工做,不须要暂停工做线程
    • 优势:并发收集、低停顿
    • 缺点:对CPU资源敏感、没法收集浮动垃圾、会产生大量空间碎片(能够经过-XX:+CMSFullGCsBeforeCompaction参数设置执行多少次不压缩FullGC后,跟着来一次带压缩的。如:这个参数的值为2,就是在执行2次不压缩的FullGC以后,紧接着会再执行1次压缩的FullGC)等

:star: 通用收集器

G1收集器(又称:Garbage—First)是目前技术发展的最前沿成果之一,它既能够做用到新生代又能够做用到老年代,所以它也被称为通用收集器。

  • 相对于CMS垃圾收集器,它具备如下特色:

    • 基于标记-整理算法,再也不产生内存碎片
    • 能够很是精确的控制停顿时间,在不牺牲吞吐量的前提下,实现了低停顿垃圾回收
  • 使用范围

    • 面向服务器
    • 针对配备多颗处理器及大容量内存的机器
  • G1收集器的几个重要内容

    • G1堆分配

      • G1收集器的java堆的内存布局与其余收集器具备很大的差异,它是将整个java堆划分为多个大小相等的独立区域(Region),虽然还保留了新生代和老年代的概念,可是新生代和老年代已经再也不物理隔阂了,他们都是一部分(能够不连续)Region的集合。它是将整个java堆划分为大约2000个大小相等的独立区域(Region),每一个独立区域的大小在1Mb到32Mb直接,能够经过-XX:G1HeapRegionSize参数(该参数的默认值并非必定的,它是根据java堆初始化时大小决定的,取值范围为:1M到32M,且要是2的指数)来设置。
      • 图示:
      • 在上图中,咱们注意到还有一些Region标明了H,它表明Humongous,这表示这些Region存储的是巨大对象(humongous object,H-obj),即大小大于等于region一半的对象。剩下的区域就是未被分配的空闲区域。
    • 新生代GC

      • 新生代收集跟ParNew相似,当新生代占用达到必定比例的时候,开始触发收集。
      • 图示:
      • 被圈起的绿色部分为新生代的区域(region),通过Young GC后存活的对象被转移到一个或者多个区域空闲中,这些被填充的区域将是新的新生代;当新生代对象的年龄(逃逸过一次Young GC年龄增长1)已经达到某个阈值(ParNew默认15),被转移到老年代的区域中。
      • 回收过程是停顿的(STW,Stop-The-Word);回收完成以后根据Young GC的统计信息调整Eden和Survivor的大小,有助于合理利用内存,提升回收效率。
      • 回收的过程多个回收线程并发收集。
    • 老年代GC

      • 和CMS相似,G1收集器收集老年代对象会有短暂停顿。
      • 包括如下几个阶段:
        • 初始化标记阶段:这一阶段会捎带着进行一次新生代的GC,在日志“GC pause (young)(inital-mark)”部分进行展现。
        • 根区域扫描阶段:程序运行过程当中会回收survivor区(存活到老年代),这一过程必须在新生代的GC以前完成。
        • 并发标记阶段:在整个堆中进行并发标记(和应用程序并发执行),此过程可能被新生代的GC中断。在并发标记阶段,若发现区域中的全部对象都是垃圾,那个这个区域会被当即回收(图中打X)。同时,并发标记过程当中,会计算每一个区域的对象活性(区域中存活对象的比例)。
        • 从新标记阶段:会有短暂停顿(STW)。再标记阶段是用来收集 并发标记阶段 产生新的垃圾(并发阶段和应用程序一同运行);G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)
        • 复制或清除阶段:多线程清除失活对象,会有STW。G1将回收区域的存活对象拷贝到新区域,清除Remember Sets,并发清空回收区域并把它返回到空闲区域链表中。
        • 复制/清除后阶段:回收区域的活性对象已经被集中回收到深蓝色和深绿色区域。
      • 图示:

安全点和全局停顿

安全点

GC的停顿主要来源于可达性分析上,程序执行时并不是在全部地方都能停顿下来开始GC,只有在到达安全点时才能暂停。

安全点的选定基本上是以程序“是否具备让程序长时间执行的特征”为标准进行选定的——由于每条指令执行的时间都很是短暂,程序不太可能由于指令流长度太长这个缘由而过长时间运行,“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,因此具备这些功能的指令才会产生安全点。

接下来的问题就在于,如何让程序在须要GC时都跑到安全点上停顿下来,大多数JVM的实现都是采用主动式中断的思想。

主动式中断的思想是当GC须要中断线程的时候,不直接对线程操做,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就本身中断挂起,轮询标志的地方和安全点是重合的,另外再加上建立对象须要分配内存的地方。

全局停顿

// TODO

几种回收器组合使用的原理图

  • Serial收集器和SerialOld收集器

  • ParNew收集器与SerialOld收集器

  • Parallel Scavenge收集器和Parallel Old收集器

  • G1收集器

GC日志分析与调优工具

日志分析

  • MinorGC日志格式

  • FullGC日志格式

调优工具

调优工具分为两类,一类是jdk自带的,一类是第三方的。

  • jps:JVM Process Status Tool,显示指定系统内全部的HotSpot虚拟机进程。

  • jstat:JVM statistics Monitoring是用于监视虚拟机运行时状态信息的命令,它能够显示出虚拟机进程中的类装载、内存、垃圾收集、JIT编译等运行数据。

  • jmap:JVM Memory Map命令用于生成heap dump文件

  • jhat:JVM Heap Analysis Tool命令是与jmap搭配使用,用来分析jmap生成的dump,jhat内置了一个微型的HTTP/HTML服务器,生成dump的分析结果后,能够在浏览器中查看

  • jstack:用于生成java虚拟机当前时刻的线程快照。

  • jinfo:JVM Configuration info 这个命令做用是实时查看和调整虚拟机运行参数。

  • jconsole:Java Monitoring and Management Console是从java5开始,在JDK中自带的java监控和管理控制台,用于对JVM中内存,线程和类等的监控

  • jvisualvm:jdk自带全能工具,能够分析内存快照、线程快照;监控内存变化、GC变化等。

  • MAT,Memory Analyzer Tool,一个基于Eclipse的内存分析工具,是一个快速、功能丰富的Java heap分析工具,它能够帮助咱们查找内存泄漏和减小内存消耗

  • GChisto:一款专业分析gc日志的工具

线程、工做内存与主内存

// TODO

参考资料

[0] : Jvm 系列(三):GC 算法 垃圾收集器

[1] : Java虚拟机(JVM)你只要看这一篇就够了!

[2] : JVM看这一篇就够了

[3] : JAVA核心知识点整理

[4] : 《深刻理解Java虚拟机——JVM高级特性与最佳实践》

[5] : Java Hotspot G1 GC的一些关键技术

[6] : Getting Started with the G1 Garbage Collector

[7] :JVM GC参数以及GC算法的应用

相关文章
相关标签/搜索