JVM GC杂谈之理论入门

时间:2021-05-02 21:53:06

GC杂谈之理论入门


JVM堆布局介绍

​ JVM堆被划分成两个不同的区域:新生代 ( Young )、老年代 ( Old )。新生代 ( Young ) 又被划分为三个区域:Eden、From Survivor、To Survivor,其中两个Survivor区的大小一致。

​ \(堆新生代老年代老年代java堆 = 新生代(Young) + 老年代(Old) = 老年代(Old) + Eden + From Survivor + To Survivor\)

​ 特殊参数说明:

JVM参数 默认值 说明
–XX:NewRatio 2 老年代(Old):新生代的值(Young),默认为2的情况下:老年代占整个堆的2/3,新生代占1/3
–XX:SurvivorRatio 8 Eden:一个Survivor区的值,默认为8的情况下:Eden区占整个新生代的8/10,FromSurvivor和ToSurvivor各占1/10

GC的分类

  • Minor GC(Young GC): 发生在新生代的垃圾收集工作。新生代几乎是所有对象出生的地方(当然存在例外,如果对象内存分配的时候发现新生代空间不够的时候会将对象直接分配在老年区)。

    Java中大部分对象不需要长久存活,因此新生代是GC发生最频繁的地方。简单的GC流程大致是:

    JVM GC杂谈之理论入门

  • Full GC(Major GC):发生在老年代的垃圾收集动作。没有Minor GC那么频繁。且耗时比Minor GC要久的多。


两种GC的触发条件

  • Minor GC:

    新生代GC的触发情况很简单,就是当在新生代Eden或者某个Survivor区创建对象内存不够的时候,就会尝试Minor GC。

  • Full GC:

    • old空间不足:如果Eden区不足以分配足够的内存给即将创建的大对象,那么大对象会在Old区创建。此时如果Old区内存也不足,那么就会触发Full GC
    • Young区晋升到Old区的空间不足:在进行Young GC之前,会判断这次Young GC是否安全,这里所谓的安全是当前老年代的剩余空间可以容纳之前 Young GC晋升对象的平均大小,或者可以容纳Young的全部对象,如果结果是不安全的,就不会执行这次 Young GC,转而执行一次Full GC
    • PermGen Space空间不足
    • 执行 System.gc()、jmap -histo:live 、jmap -dump
    • Young GC出现Promotion Faliure:当对象的GC年龄达到阈值时,或者To区放不下时,会把该对象复制到 Old区,如果Old区空间不足时,会发生Promotion Faliure,并接下去触发Full GC

如何判断对象是否存活

​ GC-Garbage Collection。垃圾回收,也就是回收"垃圾"对象。那么如何判断一个对象是否是垃圾对象呢?

当一个对象不被程序中的任何对象引用的时候,我们可以认为该对象是垃圾对象,可以被收集掉。具体算法包括:

引用计数法

​ 每个对象都有一个引用计数器,当有对象引用它时,计数器+1;当引用失效时,计数器-1;任何时刻计数器为0时就是不可能再被使用的。

​ 下图中左图是对象的引用关系,中图有一个引用失效,右图是清理引用计数器=0的对象后。

JVM GC杂谈之理论入门

​ 缺点:

  • 引用和去引用都伴随着加法和减法,影响性能

  • 对于循环引用的对象无法进行回收

    关于循环引用的对象无法回收的分析参照下图:

    JVM GC杂谈之理论入门

根搜索

​ 根搜索算法的出现,就是为了解决循环引用的时候引用计数法无法判断对象是否可以被回收的问题。

其算法的核心在于,只要对象没有一条到根对象的可达路径,就可以被回收。

​ 那么什么对象才可以认为是根对象呢?

  • 虚拟机栈的栈帧的局部变量表所引用的对象
  • 本地方法栈的JNI所引用的对象
  • 方法区的静态变量和常量所引用的对象

JVM GC杂谈之理论入门


GC算法

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

JVM GC杂谈之理论入门

​ 分为两个阶段:

  • 标记: 标记出所有存活对象(根对象的所有可达对象)

  • 清除: 统一回收所有没有存活的对象

    存在的两个问题:

  • 标记和清除的效率都不高(标记和清除都需要对堆中的所有对象进行遍历),且在GC的时候需要停止整个应用(即发生STOP THE WORLD)

  • 被标记的对象在内存中的位置可能不连续,回收之后可能会导致空间碎片太多

Copying(复制)算法

JVM GC杂谈之理论入门

​ 在标记-清除算法上做的简单优化,主要思路是:将可用内存分为相等大小的两份(活动区间和空闲区间),每次只使用其中的一份(活动区间)。在进行GC的时候,首先将活动区间的存活对象依次复制到空闲区间,然后再全量清除活动区间即可。每次GC之后,活动区间和空闲区间就会交换过来。

​ 这种GC算法的优点有:

  • GC的时候效率较高:复制存活对象到空闲区间的时候,只需移动堆指针分配合适大小内存即可。清除的时候对活动区间可以做全量清除。

  • 可以保证内存连续性: 复制存活对象到空闲区间的时候严格按照在活动区间的内存位置先后按顺序连续分配内存

    当然,这种GC算法的缺点也很明显:

  • 50%的内存浪费:必须要有一块与活动区间同等大小的空闲区间,用来做GC复制操作

  • 对象存活率高的情况下,会存在大量的重复存活对象的复制

Mark-Compact(标记/整理、标记/压缩)算法

​ 针对前两个GC算法的改进。标记的过程和Mark-Sweep算法一致。第二个整理的步骤为:移动所有存活的对象,且按照内存地址次序依次排列,然后将末端内存地址以后的内存全部回收。

JVM GC杂谈之理论入门

Mark-Sweep-Compact(标记-清理-压缩/整理)算法

​ 该算法在Mark-Compact算法的基础上进一步做了改进。结合使用了Mark-Sweep和Mark-Compact算法。并不是每次标记清理都会执行压缩,而是多次GC之后,才会执行一次压缩。这种实现,减少了移动对象的成本。

JVM分代收集算法

​ 严格的来讲,这个不能叫做一个算法。而只能叫做JVM垃圾收集的一个实现策略。该策略基于以下的弱分代假设:

  • 大多数对象的生存时间都很短,很快就会变得不可达

  • 只有少数情况才会出现老年代对象持有新生代对象的引用,即新生代对象很少引用老年代对象

    JVM也根据这个特性,将堆内存分为两部分:新生代和老年代。针对不同的部分采用不同的垃圾收集算法:

    GC算法 优点 缺点 存活对象移动 内存碎片 适用场景
    Mark-Sweep 不需要额外空间 两次扫描,耗时严重 N Y 老年代
    Copying 没有标记和清除 需要额外空间 Y N 新生代
    Mark-Compact 没有内存碎片 需要移动对象的成本 Y N 老年代


GC收集器类型

​ 前面所讲的堆的布局,堆被分为新生代、老年代。其实就是利用了大部分对象存活时间都较短这个特性。合理的利用不同的垃圾收集算法来处理收集对象。对应各种不同的垃圾收集算法,也就产生了各种GC收集器。具体列表如下:

新生代收集器 老年代收集器
Serial SerialOld
ParNew ParallelOld
Parallel Scavenge CMS

下面就来对各种收集器进行一一说明:

年轻代收集器

  • Serial收集器:

    Serial收集器是Hotspot运行在Client模式下的默认新生代收集器, 采用Copying算法进行垃圾回收。它的特点是 只用一个CPU/一条收集线程去完成GC工作, 且在进行垃圾收集时会发生STW(Stop The World).虽然是单线程收集, 但它省去了多线程上下文切换的开销,简单而高效, 在JVM管理内存不大的情况下(收集几十M~一两百M的新生代), 停顿时间完全可以控制在几十毫秒~一百多毫秒内。当然缺点也很明显:没法利用目前主流机器的多核多CPU优势。

    参数控制:**-XX:+UseSerialGC**** **串行收集器

  • ParNew收集器:

    ParNew收集器其实是前面Serial的多线程版本, 除使用多条线程进行GC外, 包括Serial可用的所有控制参数、收集算法、STW、对象分配规则、回收策略等都与Serial完全一样(也是JVM启用CMS收集器-XX: +UseConcMarkSweepGC的默认新生代收集器)。

    由于存在线程切换的开销, ParNew在单CPU的环境中比不上Serial, 且在通过超线程技术实现的两个CPU的环境中也不能100%保证能超越Serial. 但随着可用的CPU数量的增加, 收集效率肯定也会大大增加(默认ParNew收集线程数与CPU的数量相同, 因此在CPU数量过大的环境中, 可用-XX:ParallelGCThreads参数控制GC线程数)。

    参数控制:-XX:+UseParNewGC ParNew收集器

    ​ **-XX:ParallelGCThreads ** 限制线程数量

  • Parallel Scavenge收集器:

    与ParNew类似, Parallel Scavenge也是使用复制算法, 也是并行多线程收集器. 但与其他收集器关注尽可能缩短垃圾收集时间不同, Parallel Scavenge更关注系统吞吐量

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

    停顿时间越短就越适用于用户交互的程序-良好的响应速度能提升用户的体验;而高吞吐量则适用于后台运算而不需要太多交互的任务-可以最高效率地利用CPU时间,尽快地完成程序的运算任务。

    相关的参数如下:

    Parallel Scavenge参数 描述
    -XX:+UseParallelGC 使用Parallel收集器+ 老年代串行
    MaxGCPauseMillis (毫秒数) 收集器将尽力保证内存回收花费的时间不超过设定值, 但如果太小将会导致GC的频率增加.
    GCTimeRatio (整数:0 < GCTimeRatio < 100) 是垃圾收集时间占总时间的比率
    -XX:+UseAdaptiveSizePolicy 启用GC自适应的调节策略: 不再需要手工指定-Xmn-XX:SurvivorRatio-XX:PretenureSizeThreshold等细节参数, VM会根据当前系统的运行情况收集性能监控信息, 动态调整这些参数以提供最合适的停顿时间或最大的吞吐量

老年代收集器

  • Serial Old收集器:

    是Serial收集器的老年代版本。同样是单线程收集器,使用Mark-Compact算法。

    应用场景:

    • JDK1.5之前与Parallel Scavenge配合使用
    • 作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure的时候启用
  • Parallel Old收集器:

    Parallel Old是Parallel Scavenge收老年代版本, 使用多线程和Mark-Compact算法, 吞吐量优先, 主要与Parallel Scavenge配合在 注重吞吐量CPU资源敏感 系统内使用。

    参数控制:**-XX:+UseParallelOldGC ** 使用Parallel收集器+ 老年代并行

  • CMS收集器:

    Concurrent Mark Sweep。从全称上就可以看出,CMS收集器是基于Mark-Sweep算法实现的。CMS收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中部署在互联网站或者B/S系统的服务端上,这类应用尤其重视服务的相应速度,希望系统停顿时间最短,以给用户带来较好的体验。

    CMS收集器是Sun HotSpot虚拟机中第一款真正意义上的并发收集器,它第一次实现了让垃圾收集器线程和用户线程同时工作。CMS收集器的工作机制也比其他收集器更加复杂,具体来说整个过程可以分为以下4个阶段:

    • Initial Mark(初始标记阶段): 只是标记以下根对象(GC Roots)能直接关联的对象,速度很快,但是仍然需要STW
    • Concurrent Mark(并发标记阶段): 进行GC Roots跟踪的过程,和用户线程一起工作,不需要暂停工作线程
    • Remark(重新标记阶段):为了修正并发标记期间,因用户线程继续运行而导致标记产生变动的那一部分对象的标记记录,仍然需要STW
    • Concurrent Sweep(并发清除阶段): 清除GC Roots不可达对象,和用户线程一起工作,不需要暂停线程

    由于耗时最长的并发标记和并发清除阶段,垃圾收集线程和用户线程是一起工作的,所以总体来说,减少了STW的时间,缩短了应用的相应时间。但是这样的设计也存在以下不足:

    • 对CPU资源非常敏感: CMS默认启动的回收线程数=(CPU数目+3)4。当CPU数>4时, GC线程最多占用不超过25%的CPU资源, 但是当CPU数<=4时, GC线程可能就会过多的占用用户CPU资源, 从而导致应用程序变慢, 总吞吐量降低。

    • 无法处理浮动垃圾,可能出现Promotion FailureConcurrent Mode Failure而导致另一次Full GC的产生:

      浮动垃圾是指在CMS并发清理阶段用户线程运行而产生的新垃圾. 由于在GC阶段用户线程还需运行, 因此还需要预留足够的内存空间给用户线程使用, 导致CMS不能像其他收集器那样等到老年代几乎填满了再进行收集. 因此CMS提供了`-XX:CMSInitiatingOccupancyFraction`参数来设置GC的触发百分比(以及`-XX:+UseCMSInitiatingOccupancyOnly`来启用该触发百分比), 当老年代的使用空间超过该比例后CMS就会被触发(JDK 1.6之后默认92%). 但当CMS运行期间预留的内存无法满足程序需要, 就会出现上述*Promotion Failure*等失败, 这时JVM将启动后备预案: 临时启用Serial Old收集器来重新执行Full GC(CMS通常配合大内存使用, 一旦大内存转入串行的Serial GC, 那停顿的时间就是大家都不愿看到的了)。
    • 无法避免内存碎片:CMS的收集算法不涉及整理/压缩的步骤,可能会产生大量内存碎片. 内存碎片过多可能会导致无法分配大对象而提前触发Full GC. 因此CMS提供了-XX:+UseCMSCompactAtFullCollection开关参数, 用于在Full GC后再执行一个碎片整理过程. 但内存整理是无法并发的, 内存碎片问题虽然没有了, 但停顿时间也因此变长了, 因此CMS还提供了另外一个参数-XX:CMSFullGCsBeforeCompaction用于设置在执行N次不进行内存整理的Full GC后, 跟着来一次带整理的(默认为0: 每次进入Full GC时都进行碎片整理)。

    相关参数设置如下:

    参数名称 描述
    -XX:+UseConcMarkSweepGC 使用CMS收集器
    -XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次碎片整理;整理过程会STW
    -XX:+CMSFullGCsBeforeCompaction 设置进行几次Full GC后,进行一次碎片整理
    -XX:ParallelCMSThreads 设定CMS的线程数量(一般情况约等于可用CPU数量)

分区收集---G1收集器:

​ JDK7引入了G1(Garbage-First)是一款面向服务端应用的收集器, 主要目标用于配备多颗CPU的服务器治理大内存.

	>- G1 is planned as the long term replacement for the Concurrent Mark-Sweep Collector (CMS).
>- -XX:+UseG1GC 启用G1收集器.

​ G1是目前技术发展的最前沿成果之一,HotSpot开发团队赋予它的使命是未来可以替换掉JDK1.5中发布的CMS收集器。与CMS收集器相比G1收集器有以下特点:

  1. 空间整合:G1收集器采用标记整理算法,不会产生内存空间碎片。分配大对象时不会因为无法找到连续空间而提前触发下一次GC。
  2. 可预测停顿:这是G1的另一大优势,降低停顿时间是G1和CMS的共同关注点,但G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定在一个长度为N毫秒的时间片段内,消耗在垃圾收集上的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了

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

​ 每块区域既有可能属于O区、也有可能是Y区, 因此不需要一次就对整个老年代/新生代回收. 而是当线程并发寻找可回收的对象时, 有些区块包含可回收的对象要比其他区块多很多. 虽然在清理这些区块时G1仍然需要暂停应用线程, 但可以用相对较少的时间优先回收垃圾较多的Region(这也是G1命名的来源). 这种方式保证了G1可以在有限的时间内获取尽可能高的收集效率.

​ G1的新生代收集跟ParNew类似: 存活的对象被转移到一个/多个Survivor Regions. 如果存活时间达到阀值, 这部分对象就会被提升到老年代.

​ G1的新生代收集特点如下:

  • 一整块堆内存被分为多个Regions.
  • 存活对象被拷贝到新的Survivor区或老年代.
  • 年轻代内存由一组不连续的heap区组成, 这种方法使得可以动态调整各代区域尺寸.
  • Young GCs会有STW事件, 进行时所有应用程序线程都会被暂停.
  • 多线程并发GC.

​ G1老年代垃圾收集的步骤:

  1. Initial Mark(初始标记): 在G1中, 该操作附着一次Young GC, 以标记Survivor中有可能引用到老年代对象的Regions. 该阶段会STW

  2. Root Region Scanning(根区域扫描):程序运行过程中会回收survivor区(存活到老年代),这一过程必须在Young GC之前完成

  3. Concurrent Marking(并发标记):在整个堆中进行并发标记(和应用程序并发执行),此过程可能被Young GC中断。在并发标记阶段,若发现区域对象中的所有对象都是垃圾,那个这个区域会被立即回收。同时,并发标记过程中,会计算每个区域的对象活性(区域中存活对象的比例)。

  4. Remark(重新标记):完成堆内存中存活对象的标记. 使用snapshot-at-the-beginning(SATB, 起始快照)算法, 比CMS所用算法要快得多(空Region直接被移除并回收, 并计算所有区域的活跃度).

  5. Cleanup(清理):

    • Performs accounting on live objects and completely free regions. (Stop the world)
    • Scrubs the Remembered Sets. (Stop the world)
    • Reset the empty regions and return them to the free list. (Concurrent)
  6. Copying(复制): These are the stop the world pauses to evacuate or copy live objects to new unused regions. This can be done with young generation regions which are logged as [GC pause (young)]. Or both young and old generation regions which are logged as [GC Pause (mixed)].选择”活跃度”最低的区域(这些区域可以最快的完成回收). 拷贝/转移存活的对象到新的尚未使用的regions.

    |

​ G1收集器的相关特性:

    • Concurrent Marking Phase
      • Liveness information is calculated concurrently while the application is running.
      • This liveness information identifies which regions will be best to reclaim during an evacuation pause.
      • There is no sweeping phase like in CMS.
    • Remark Phase
      • Uses the Snapshot-at-the-Beginning (SATB) algorithm which is much faster then what was used with CMS.
      • Completely empty regions are reclaimed.
    • Copying/Cleanup Phase
      • Young generation and old generation are reclaimed at the same time.
      • Old generation regions are selected based on their liveness.

常用收集器组合

新生代GC策略 老年代GC策略 说明
Serial Serial Old Serial和Serial Old都是单线程进行GC,特点就是GC时暂停所有应用线程。
Serial CMS+Serial Old CMS(Concurrent Mark Sweep)是并发GC,实现GC线程和应用线程并发工作,不需要暂停所有应用线程。另外,当CMS进行GC失败时,会自动使用Serial Old策略进行GC。
ParNew CMS 使用-XX:+UseParNewGC选项来开启。ParNew是Serial的并行版本,可以指定GC线程数,默认GC线程数为CPU的数量。可以使用-XX:ParallelGCThreads选项指定GC的线程数。如果指定了选项-XX:+UseConcMarkSweepGC选项,则新生代默认使用ParNew GC策略。
ParNew Serial Old 使用-XX:+UseParNewGC选项来开启。新生代使用ParNew GC策略,年老代默认使用Serial Old GC策略。
Parallel Scavenge Serial Old Parallel Scavenge策略主要是关注一个可控的吞吐量:应用程序运行时间 / (应用程序运行时间 + GC时间),可见这会使得CPU的利用率尽可能的高,适用于后台持久运行的应用程序,而不适用于交互较多的应用程序。
Parallel Scavenge Parallel Old Parallel Old是Serial Old的并行版本
G1GC G1GC -XX:+UnlockExperimentalVMOptions -XX:+UseG1GC #开启- XX:MaxGCPauseMillis =50 #暂停时间目标 -XX:GCPauseIntervalMillis =200 #暂停间隔目标 -XX:+G1YoungGenSize=512m #年轻代大小 -XX:SurvivorRatio=6 #幸存区比例

JVM相关工具说明

在${JAVA_HOME}/bin/目录下Sun/Oracle给我们提供了一些处理应用程序性能问题、定位故障的工具, 包含

工具名称 描述 功能
jps 打印Hotspot VM进程 VMID、JVM参数、main()函数参数、主类名/Jar路径
jstat 查看Hotspot VM 运行时信息 类加载、内存、GC[可分代查看]、JIT编译
jinfo 查看和修改虚拟机各项配置 -flag name=value
jmap heapdump: 生成VM堆转储快照、查询finalize执行队列、Java堆和永久代详细信息 jmap -dump:live,format=b,file=heap.bin [VMID]
jstack 查看VM当前时刻的线程快照: 当前VM内每一条线程正在执行的方法堆栈集合 Thread.getAllStackTraces()提供了类似的功能
javap 查看经javac之后产生的JVM字节码代码 自动解析.class文件, 避免了去理解class文件格式以及手动解析class文件内容
jcmd 一个多功能工具, 可以用来导出堆, 查看Java进程、导出线程信息、 执行GC、查看性能相关数据等 几乎集合了jps、jstat、jinfo、jmap、jstack所有功能
jconsole 基于JMX的可视化监视、管理工具 可以查看内存、线程、类、CPU信息, 以及对JMX MBean进行管理
jvisualvm JDK中最强大运行监视和故障处理工具 可以监控内存泄露、跟踪垃圾回收、执行时内存分析、CPU分析、线程分析…

JVM常用参数配置

参数 描述
-Xms 最小堆大小
-Xmx 最大堆大小
-Xmn 新生代大小
-XX:PermSize 永久代大小
-XX:MaxPermSize 永久代最大大小
-XX:+PrintGC 输出GC日志
-verbose:gc -
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC时间戳(以基准时间的形式)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-Xloggc:/path/gc.log 日志文件的输出路径
-XX:+PrintGCApplicationStoppedTime 打印由GC产生的停顿时间

参考链接

http://www.importnew.com/14630.html

https://mp.weixin.qq.com/s/HS1VT9ww7XOWqKciUOhwUw

http://epy.iteye.com/blog/1914455

https://www.cnblogs.com/duke2016/p/6250766.html

https://www.cnblogs.com/ityouknow/p/5614961.html

https://www.cnblogs.com/sunfie/p/5125283.html

http://zqhxuyuan.github.io/2016/07/26/JVM/

http://www.importnew.com/23035.html

http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html#t5

http://www.oracle.com/technetwork/java/javase/memorymanagement-whitepaper-150215.pdf