java GC分析浅谈

GC分析是为了进一步优化系统性能,性能优化是一个很大的领域,CPU、cache命中、IO各个方面都要综合进行考虑,这里我们只讲其中的一小部分,GC分析。在进行性能优化之前先要根据业务场景制定一个明确的性能需求指标,优化是一个无止境的事情,先制定好性能优化指标以便平衡投入和产出的问题。性能需求指标一般有以下几个:

  1. 应用预期的吞吐量是多少?
  2. 请求和响应之间的延迟预期是多少?
  3. 应用支持多少并发用户或并发任务?
  4. 当并发用户数或并发任务数达到最大时,可接受的吞吐量和延迟是多少?
  5. 最差情况下的延迟是多少?
  6. 要使垃圾收集引入的延迟在可容忍范围之内,垃圾回收的频率应该是多少?

一、GC判断方法(如何判断Java对象需要被回收)

一般我们把Java内存划分为以下几个区域,如图:
内存模型
我们常说的垃圾回收指的是回收掉堆内存,那如何来判断堆内存里的对象需要回收呢,业界通用有以下两种办法:

  1. 引用计数算法:引用计数法记录着每一个对象被其它对象所持有的引用数,被引用一次就加一,引用失效就减一;引用计数器为0则说明该对象不再可用;当一个对象被回收后,被该对象所引用的其它对象的引用计数都应该相应减少,它很难解决对象之间的相互循环循环引用实例
  2. 可达性分析算法:从GC Root对象向下搜索其所走过的路径称为引用链,当一个对象不再被任何的GC root对象引用链相连时说明该对象不再可用,GC root对象包括四种:方法区中常量和静态变量引用的对象,虚拟机栈中变量引用的对象,本地方法栈中引用的对象; 解决循环引用是因为GC Root通常是一组特别管理的指针,这些指针是tracing GC的trace的起点。它们不是对象图里的对象,对象也不可能引用到这些“外部”的指针。

Java GC采用的是第二种方法,可达性分析算法,接下来我们了解下Java对象的四种引用,这方面的知识在进行一般的业务开发时用的较少,但在深入优化GC时还是需要掌握的。

  • 强引用 :创建一个对象并把这个对象直接赋给一个变量,eg :Person person = new Person(“sunny”);不管系统资源有么的紧张,强引用的对象都绝对不会被回收,即使他以后不会再用到。
  • 软引用 :通过SoftReference类实现,eg : SoftReference p = new SoftReference(new Person(“Rain”));内存非常紧张的时候会被回收,其他时候不会被回收,所以在使用之前要判断是否为null从而判断他是否已经被回收了。
  • 弱引用 :通过WeakReference类实现,eg : WeakReference p = new WeakReference(new Person(“Rain”));不管内存是否足够,系统垃圾回收时必定会回收。
  • 虚引用:不能单独使用,主要是用于追踪对象被垃圾回收的状态,为一个对象设置虚引用关联的唯一目的是希望能在这个对象被收集器回收时收到一个系统通知。通过PhantomReference类和引用队列ReferenceQueue类联合使用实现。

二、垃圾回收算法(如何回收Java对象)

1、标记—清除算法
算法的执行过程与名字一样,先标记所有需要回收的对象,在标记完成后统一回收所有被标记的对象。该算法有两个问题:
1)标记和清除过程效率不高。主要由于垃圾收集器需要从GC Roots根对象中遍历所有可达的对象,并给这些对象加上一个标记,表明此对象在清除的时候被跳过,然后在清除阶段,垃圾收集器会从Java堆中从头到尾进行遍历,如果有对象没有被打上标记,那么这个对象就会被清除。显然遍历的效率是很低的;
2)会产生很多不连续的空间碎片,所以可能会导致程序运行过程中需要分配较大的对象的时候,无法找到足够的内存而不得不提前出发一次垃圾回收。
标记清除

2、复制算法
复制算法是为了解决标记-清除算法的效率问题的,其思想如下:将可用内存的容量分为大小相等的两块,每次只使用其中的一块,当这一块内存使用完了,就把存活着的对象复制到另外一块上面,然后再把已使用过的内存空间清理掉。这样当垃圾收集器进行回收的时候就不用考虑空间碎片的问题,缺点在于把内存缩小为原来的一半,代价未免有点大。
这里写图片描述
当然正是由于其缩小内存为原来的一半代价大的问题,现代的JVM并不是按照1:1划分内存空间的,二是将内存分为一块较大的Eden区和两块较小的Survivor区,每次使用其中的Eden和一块Survivor区。当回收的时候,将Eden和Survivor中还存活着的对象一次性复制到另外一块Survivor中,最后把Eden和Survivor的空间清理出来。其实这里还有一个问题:就是如果垃圾回收后,存活的对象需要的空间大于剩余一块Survivor的空间怎么办?答案是需要依赖其他内存进行分配(这里主要指的是老年代)。

3、标记—整理算法
与标记-清除算法过程一样,只不过在标记后不是对未标记的内存区域进行清理,二是让所有的存活对象都向一端移动,然后清理掉边界外的内存
这里写图片描述

三、HostSpot垃圾回收器种类介绍

以垃圾回收机制分类,HostSpot垃圾回收器可被分四类,下面简单介绍下这四种回收机制,详细介绍参见 http://www.zicheng.net/article/55.htm

  • Serial收集:单线程收集,在垃圾回收时会“stop-the-world”,后面简称为STW,由于是单线程回收,停顿时间较长,比较适合Client模式的Java程序和内存使用量较小的程序。
  • Parallel收集:多线程收集,在垃圾回收时同样也会产生STW现象,顾名思义,这种垃圾回收期会使用多个线程回收垃圾,在多核处理器上,可以大幅缩短停顿时间。
  • CMS收集:为了尽可能减少STW对应用程序的应用,HotSpot设计者引入了CMS(concurrent mark sweep)垃圾回收期,这种方式只在初始标记阶段和重新标记阶段会发生STW,其余时间的垃圾回收操作可以和应用程序的工作线程并发执行,当然,这种方式也有其缺点,比如在年老代不满时就要进行回收、会出现许多内存碎片、为了减少每次的GC停顿时间会使总的GC停顿时间变长,影响吞吐量等等。
  • G1收集 G1(garbage first)收集相对其他收集器有了革命性的改变,它将Java内存分成一些大小相等的区域,使得新生代内存和年老代内存可以在物理上不是连续的。
    这里写图片描述
    HotSpot针对上面四种垃圾收集器做了部分扩展,就形成了自己的七种垃圾回收器。
  • Serial垃圾回收器—年轻代:基于Serial收集机制,采用复制算法,新生的对象分配在Eden区和一个Survivor区(from区),经过一次垃圾回收后所有存活的对象被复制到另一个Survivor区(to区),当Survivor区内存不足以容纳这些存活对象时就会溢出到年老代,这对GC的伤害特别大,在实际开发中要尽量避免这种情况。使用-XX:+UseSerialGC参数开启。
  • ParNew垃圾回收器—年轻代:基于Parallel收集机制,采用复制算法,可以通过-XX:ParallelGCThreads=参数控制垃圾回收器所使用的线程数,使用-XX:+UseParNewGC开启。
  • Parallel Scavenge垃圾回收器—年轻代:基于Parallel搜集机制,采用复制算法,这个垃圾回收器与ParNew最大的不同就在于可设置的参数不同,我们可以更精确地控制GC停顿时间以及吞吐量(应用程序线程用时占程序总耗时的比例,比如应用程序运行了99s,GC垃圾回收停顿了1S,那么吞吐量就是99%),设置了GC停顿时间和吞吐量参数后,此垃圾回收器会优先满足最大停顿时间的目标,次之是吞吐量,最后才是新生代区域的最小值。另外此垃圾回收器还有一个自适应策略(-XX:UseAdaptiveSizePolicy),默认开启,这个策略可以动态调整内存区域的大小,包括晋升为年老代的年龄,建议不要轻易关闭此策略,除非你对自己的应用程序已经非常了解。设置最大停顿时间:-XX:MaxGCPauseMillis=,设置吞吐量:-XX:GCTimeRatio=,开启此垃圾回收器:-XX:+UseParallelGC。
  • Serial Old垃圾回收器—年老代:基于Serial收集机制,采用标记—整理算法,标记所有存活的对象,将其向内存一端移动,然后清理掉边界外的内存,是Jdk1.7的默认年老代垃圾回收器。
  • Parallel Old垃圾回收器—年老代:基于Parallel收集机制,采用标记—整理算法,使用-XX:+UseParallelOldGC开启。
  • CMS垃圾回收器—年老代:基于CMS收集机制,采用标记—清除算法,CMS垃圾回收器的处理分以下几个阶段:
    • 初始标记(CMS-initial-mark) ,会导致swt;
    • 并发标记(CMS-concurrent-mark),与用户线程同时运行;
    • 预清理(CMS-concurrent-preclean),与用户线程同时运行;
    • 可被终止的预清理(CMS-concurrent-abortable-preclean) 与用户线程同时运行;
    • 重新标记(CMS-remark) ,会导致swt;
    • 并发清除(CMS-concurrent-sweep),与用户线程同时运行;
    • 并发重置状态等待下次CMS的触发(CMS-concurrent-reset),与用户线程同时运行;

    它只在初识标记和重新标记阶段STW,其他阶段可以和应用程序的程序并发执行,因此它的停顿时间是非常短暂的(低延迟应用),如果重新标记阶段暂停时间太长,可以通过-XX:+CMSParallelRemarkEnabled参数和-XX:+CMSScavengeBeforeRemark参数进行调优,前者会启用并行Remark,后者会在Remark执行之前进行一次Young gc,因为这个阶段,年轻代也是cms的Gcroot,cms会扫描年轻代指向年老代的引用,如果年轻代有大量引用需要被扫描,会使Remark阶段耗时增加。
    但是此垃圾回收器的缺点也是很明显的。
    (1)由于GC线程与应用程序并发执行时会抢占CPU资源,有较多的线程切换开销,因此会造成整体的吞吐量下降。
    (2)采用标记—清除算法,会造成许多内存碎片的产生,有可能会在分配大对象时由于没有连续的内存空间而不得不进行一次Full GC,针对这种情况,提供了-XX:+UseCMSCompactAtFullCollection参数用于在Full GC之后进行一次碎片整理的工作,-XX:CMSFullGCsBeforeCompaction参数用于控制在进行几次全局GC后会进行一次碎片整理的工作。
    (3)并发模式失败,由于CMS垃圾回收器在进行垃圾回收的同时应用程序还在运行,不断有新的垃圾产生,这部分垃圾在本次垃圾回收过程中无法处理,也是由于这个原因,CMS不能像其他垃圾回收器那样等到年老代几乎完全被填满了再去进行收集,而是需要预留一部分空间供并发收集时的程序使用,如果这部分预留的空间无法满足程序需求,就会出现Concurrent Mode Failure,这时候虚拟机会使用Serial Old垃圾回收器STW回收垃圾,我们可以通过-XX:CMSInitiatingOccupancyFraction来控制当年老代的内存占用达到多少时开始回收。使用-XX:+UseConcMarkSweepGC开启。
    7.G1垃圾回收器—年轻代+年老代: 为解决CMS算法产生空间碎片和其他一系列缺陷,HotSpot提供了G1算法,通过-XX:+UseG1GC参数来启用,G1方面的改变较大,详细介绍请参见 https://tech.meituan.com/g1.html,下面节选部分入门知识介绍下。
    传统的GC收集器将连续的内存空间划分为年轻代、年老代和永久代(JDK8去除了永久代,引入了元空间Metaspace),这种划分的特点是各代的存储地址是连续的,如下图所示:
    这里写图片描述
    而G1的各代存储地址是不连续的,每一代都使用了n个不连续的大小相同的Region,每个Region占有一块连续的虚拟内存地址,如下图所示:
    这里写图片描述
    在上图中,我们注意到还有一些Region标明了H,它代表Humongous,这表示这些Region存储的是巨大对象(humongous object,H-obj),即大小大于等于region一半的对象。H-obj有如下几个特征:

  • H-obj直接分配到了old gen,防止了反复拷贝移动。
  • H-obj在global concurrent marking阶段的cleanup 和 full GC阶段回收。
  • 在分配H-obj之前先检查是否超过 initiating heap occupancy percent和the marking threshold, 如果超过的话,就启动global concurrent marking,为的是提早回收,防止 evacuation failures 和 full GC。

    为了减少连续H-Objs分配对GC的影响,需要把大对象变为普通的对象,建议增大Region size,可以通过-XX:G1HeapRegionSize参数配置,取值范围是1M到32M内2的指数。
    G1提供了两种GC模式,Young GC和Mixed GC,两种都是完全Stop The World的。

  • Young GC:选定所有年轻代里的Region。通过控制年轻代的region个数,即年轻代内存大小,来控制young GC的时间开销。
  • Mixed GC:选定所有年轻代里的Region,外加根据global concurrent
    marking统计得出收集收益高的若干老年代Region。在用户指定的开销目标范围内尽可能选择收益高的老年代Region。

    由上面的描述可知,Mixed GC不是full GC,它只能回收部分老年代的Region,如果mixed GC实在无法跟上程序分配内存的速度,导致老年代填满无法继续进行Mixed GC,就会使用serial old GC(full GC)来收集整个GC heap。所以我们可以知道,G1是不提供full GC的。

以上就是Java七种垃圾回收器的简单介绍,但年轻代的垃圾回收器和年老代的垃圾回收器并不是可以任意组合的,他们的组合关系如下图所示。
这里写图片描述
下面介绍下常用的三种组合(年轻代+年老代)
serial & serial Old:适用于Client模式的应用程序,处理器较少,规模不大
Parallel Scavenge & Parallel Old/Serial Old:适合吞吐量优先的服务端程序
ParNew & Cms:适合响应时间优先的服务端程序

四、GC分析

1、准备工作
打开tomcat/bin/catalina.sh,在“Execute The Requested Command”此行注释后加入以下代码配置JMX,方面在图形化界面上监控程序运行情况

 # ----- JMX Config Start -----
        if [ "$1" = "run" ]; then
          JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote -Djava.rmi.server.hostname=<hostip> -Dcom.sun.management.jmxremote.port=1099 
          -Dcom.sun.management.jmxremote.rmi.port=1099 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false 
          -Dcom.sun.management.jmxremote.access.file=<jdk.path>/jre/lib/management/jmxremote.access 
          -Dcom.sun.management.jmxremote.password.file=<jdk.path>/jre/lib/management/jmxremote.password"
        elif [ "$1" = "start" ]; then
        JAVA_OPTS="$JAVA_OPTS -Dcom.sun.management.jmxremote -Djava.rmi.server.hostname=<hostip> -Dcom.sun.management.jmxremote.port=1099 
          -Dcom.sun.management.jmxremote.rmi.port=1099 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false 
          -Dcom.sun.management.jmxremote.access.file=<jdk.path>/jre/lib/management/jmxremote.access 
          -Dcom.sun.management.jmxremote.password.file=<jdk.path>/jre/lib/management/jmxremote.password"
        fi
# ----- JMX Config End -----

加入输出GC日志的参数,具体每个参数的意义请自行查阅

JAVA_OPTS="$JAVA_OPTS -XX:+PrintCommandLineFlags -XX:+PrintTenuringDistribution -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:../logs/gc.log"

2、堆栈大小初始值估算
按照业务的性能需求,对程序进行压测,可以得到一份gc日志,CMS日志和G1日志和其他日志有所不同,此处我们只分析Serial/Parallel垃圾回收器产生的日志,网上找到这两张图,很好地概括了GC日志中每个值的意义。
这里写图片描述
这里写图片描述
多次测试后我们可以得到多个Full GC的日志,算出Full GC后Old区的内存平均占用,此值就是程序的活跃数据,也就是程序稳定运行时存活对象在堆中占用的内存大小,我们可以根据此值来估算堆内存的空间大小。
这里写图片描述
例如,根据GC日志获得年老代的活跃数据大小为300M,那么各分区的大小可以设为:
总堆:1200MB=300MB*4
年轻代:450MB=300MB*1.5
年老代:750MB=1200MB-450MB
这部分的设置仅仅是堆大小的初始值,后续可能会根据应用程序的特性和需求再次调整。
3、GC分析
我们先来了解一下堆内存的分配和回收步骤:

  • 对象在Eden区完成内存分配
  • 当Eden区满了,再创建对象时,会因为申请不到空间,触发minorGC,进行young(eden+1个survivor)区的垃圾回收。
  • minorGC时,Eden不能被回收的对象被放入到空的survivor区(Eden肯定会被清空),另一个survivor里不能被GC回收的对象也会被放入这个survivor,始终保证一个survivor是空的。
  • 当做第3步的时候,如果发现survivor区空间不够了,则这些对象被copy到old区,或者survivor区空间足够,但是有些对象已经足够Old,也被放入Old区(XX:MaxTenuringThreshold,MaxTenuringThreshold这个参数用于控制对象能经历多少次Minor GC才晋升到旧生代,默认值是15,如果设置为0,则直接进入Old区)。
  • 当Old区被放满之后,进行FullGC。

-XX:MaxTenuringThreshold参数主要是控制新生代需要经历多少次GC晋升到老年代中的最大阈值。在JVM中用4个bit存储(放在对象头中),所以其最大值是15。
但并非意味着,对象必须要经历15次YGC才会晋升到老年代中。例如,当survivor区空间不够时,便会提前进入到老年代中,但这个次数一定不大于设置的最大阈值。
那么JVM到底是如何来计算S区对象晋升到Old区的呢?介绍另一个重要的JVM参数:
-XX:TargetSurvivorRatio:一个计算期望s区存活大小(Desired survivor size)的参数。默认值为50,即50%。
当一个S区中所有的age对象的大小如果大于等于Desired survivor size,则重新计算threshold,以age和MaxTenuringThreshold两者的最小值为准。还有一个参数:
-XX:SurvivorRatio:设置Eden区和Survivor区的比例为8:1,稍大的Survivor空间可以提高在年轻代回收生命周期较短对象的可能。

一边压测,一边使用VisualVm/JvisualVm的visual GC插件实时监控在线程序的GC情况,查看程序的GC情况是否健康,晋升到年老代的对象是否过快、过多,监控界面如下图:
这里写图片描述
压测足够时间之后(20min以上),使用在线GC分析工具(Gceasy)分析GC日志,Gceasy会给出一份分析报告,此报告中有三个关键指标需要注意下,通过GC调优的效果可以从这三个值上反映出来。
Avg GC time:每次GC停顿的时间,程序的平均响应时长与这个值关系很大
GC Interval avg time:每两次gc之间的平均间隔时间
Throughput:用户应用程序运行时间占总程序的比例
注:尽量避免在程序中出现大而短的对象,这种对象对垃圾回收影响很大,java的GC回收器回收小对象的速度几乎是大对象的一倍。


《Java性能优化权威指南》
http://www.charlesxiao.top/2015/07/29/%E6%B5%85%E6%9E%90java%E5%86%85%E5%AD%98%E7%AE%A1%E7%90%86%E6%9C%BA%E5%88%B6/
https://blog.csdn.net/jiasike/article/details/51355729

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值