JVM系列之第一篇:调优实战总结

前言

   本篇文章主要在狸猫技术窝中有关JVM中调优的一些实战基础上进行总结,能够算是本身的一篇学习总结。主要以目前主流的两种垃圾回收组合方式,ParNew +CMS及G1垃圾回收器为基础,梳理下调优思路、GC日志如何阅读及引起OOM的区域和缘由。html

ParNew +CMS组合

   ParNew通常用在新生代的垃圾回收器,CMS用在老年代的垃圾回收器,他们都是多线程并发机制,性能更好,如今通常是线上生产系统的标准组合。java

Minor GC

   Minor GC又称年轻代垃圾回收,年轻代垃圾回收主要采用复制算法,因为年轻代对象大都“朝生夕死”,为下降内存使用率瓶颈,设置了Eden区和2个Survior区,1个Eden区占80%内存空间,每一块Survivor区各占10%内存空间。当前Minor GC主要采用ParNew垃圾回收器。程序员

何时尝试触发Minor GC?

   新生代剩余内存空间放不下新对象,此时须要触发GC。算法

触发Minor GC状况有:数组

  • 新生代现有存活对象小于老年代剩余内存,即老年代空间足以支撑可能晋升的对象
  • 状况1不成立时,设置空间担保而且能够担保成功(当前JDK版本下都有默认开启了空间担保),即老年代空间大于历次Minor GC后进入老年代的平均大小。

Minor GC以前作了什么?

    判断老年代的可用内存是否已经小于了新生代的所有对象大小了,若是是,判断-XX:HandlePromotionFailure参数是否设置,若是有这个参数,那么就会继续尝试进行下一步判断:看老年代的内存大小,是否大于以前每一次Minor GC后进入老年代的对象的平均大小。若是判断失败,或者空间分配担保没有设置,就会直接触发一次FullGC,对老年代进行垃圾回收,尽可能腾出来一些空间,而后再执行Minor GC。bash

Minor GC结果

1.Minor GC事后,剩余的存活对象,小于Survivor区域大小,存活对象进入Survivor区。多线程

2.Minor GC事后,存活对象大于Survivor区域大小,小于老年代可用空间大小,直接进入老年代并发

3.Minor GC事后,存活对象大于Survivor区域大小,也大于老年代可用空间大小,此时,就会发生Handle Promotionoracle

Old GC

   Old GC又称老年代垃圾回收,针对老年代进行垃圾的回收器主要有Serial Old及CMS。若是Minor GC后存活对象大于老年代里的剩余空间,这个时候触发一次Old GC, 将老年代里的没人引用的对象给回收掉,而后才可能让Minor GC事后剩余的存活对象进入老年代里面。app

对象如何进入老年代?

   当对象躲过15次Minor GC后、符合动态对象判断规则、大对象及Minor GC后的对象太多没法放入Survivor区域等场景,都会触发对象进入老年代,下面将逐一分析每种场景。

1.躲过15次GC以后进入老年代
  • 对象每次在新生代躲过一次GC被转移到一块Survivor区域只可以,此时他的年龄就会增加一岁
  • 默认的设置下,当对象的年龄达到15岁的时候,也就是躲过15次GC的时候,他就会转移到老年代里区。具体多少岁进入老年代,能够经过JVM参数-XX:MaxTenuringThreshold来设置,默认是15岁。
2.动态对象年龄判断
  • 若是一次新生代gc事后,发现当前放对象的Survior区域里,几个年龄的对象的总大小大于了这块Survior区域的内存大小的50%,好比说age1 + age2 + age3的对象大小总和,超过了Survivor区域的50%,那么就把age3年龄以上的对象都放入老年代。
  • 动态年龄判断规则,也会让一些新生代的对象进入老年代。

   不管15次GC以后进入老年代,仍是动态年龄判断规则,都是但愿可能长期存活的对象,尽早进入老年代。

3.大对象直接进入老年代
  • 经过参数-XX:PretenureSizeThreshold能够设置对象直接进入老年代的阀值,能够把他的值设置为字节数,好比1048576字节,就是1MB。
  • 若是建立一个大于这个大小的对象,好比一个超大的数组或者别的大对象,此时就直接把这个大对象放到老年代里,压根不会通过新生代。
4.Minor GC后的对象太多没法放入Survivor区域

   这里须要考虑一个问题,就是老年代空间不够放这些对象。若是老年代的内存大小是大于新生代全部对象的,此时就能够对新生代触发一次Minor GC,由于即便全部对象都存活,Survivor区放不下了,也能够转移到老年代去。若是Minor GC前,发现老年代的可用内存已经小于新生代的所有大小了,这个时候若是Minor GC后新生代的对象所有存活下来,都转移到老年代去,老年代空间不够,理论上,是有这种可能的。因此假如Minor GC以前,发现老年代的可用内存已经小于了新生代的所有对象大小了,就会看一个-XX:HandlePromotionFailure的参数是否设置了。若是有这个参数,那么就会继续尝试进行下一步判断:看老年代的内存大小,是否大于以前每一次Minor GC后进入老年代的对象的平均大小。若是判断失败,或者空间分配担保没有设置,就会直接触发一次FullGC,对老年代进行垃圾回收,尽可能腾出来一些空间,而后再执行Minor GC。

   若是老年代回收后,仍然没有足够的空间存放Minor GC事后的剩余存活对象,那么此时就会致使OOM内存溢出

老年代的垃圾回收算法是什么样的?

标记整理算法

   标记老年代当前存活对象,这些对象多是零散分布在内存中,而后将这些存活对象在内存里移动,将存活对象尽可能挪动到一边,将存活对象集中放置,避免回收后出现过多内存碎片。而后一次行把垃圾对象都回收掉。

标记清除算法

   先经过追踪GC Roots的方法,看看各个对象是否被GC Roots给引用了,若是是的话,那就是存活对象,不然就是垃圾对象。先将垃圾对象标记出来,而后一次性把垃圾对象都回收掉,这种方法其实最大的问题就是会形成不少内存碎片。

老年代为何不采用复制算法?

   老年代存活对象太多了,若是采用复制算法,每次挪动可能90%的存活对象,这就不合适了。因此采用先把存活对象挪到一块儿紧凑一些,而后回收垃圾对象的方式。

老年代回收场景

1.Minor GC以前,老年代内存空间小于历次Minor GC后升入老年代对象的平均大小,判断Minor GC有风险,可能就会提早触发老年代GC回收老年代垃圾对象。

2.Minor GC后的对象太多了,都要升入老年代,发现空间不足,触发一次老年代的Old GC。

3.设置了-XX:CMSInitiatingOccuancyFaction参数,好比设置为92%,好比说老年代空间使用超过92%了,此时就会自行触发Old GC.

CMS回收过程

   CMS在执行一次垃圾回收的过程一共分为4个阶段。

1.初始标记

   标记出来全部GC Roots直接引用的对象,会让系统的工做线程所有中止,进入“Stop the World”状态。

2.并发标记

   追踪老年代全部存活对象,老年代存活对象不少,这个过程就会很慢。

3.从新标记

   这个过程会标记整堆,包括年轻代和老年代。

4.并发清理

   找到零零散散分散再各个地方的垃圾对象,速度较慢。最后可能还要执行一次内存碎片整理,把大量的存活对象挪在一块儿,空出来连续空间,这个过程仍然要STW,那就更慢了。

concurrent mode failure是什么?

   CMS垃圾收集器特有的错误,CMS的垃圾清理和引用线程是并行进行的,若是在并行清理的过程当中老年代的空间不足以容纳应用产生的垃圾,则会抛出“concurrent mode failure”。

concurrent mode failure影响

  老年代的垃圾收集器从CMS退化为Serial Old,全部应用线程被暂停,停顿时间变长。

可能缘由及方案
  • 缘由1:CMS触发太晚

    方案:将-XX:CMSInitiatingOccupancyFraction=N调小;

  • 缘由2:空间碎片太多

    方案:开启空间碎片整理,并将空间碎片整理周期设置在合理范围;

-XX:+UseCMSCompactAtFullCollection (空间碎片整理) -XX:CMSFullGCsBeforeCompaction=n,执行多少次Full GC以后再执行一次内存碎片整理工做,默认是0,意思就是每次Full GC以后都会进行一次内存整理。

  • 缘由3:垃圾产生速度超过清理速度 晋升阈值太小; Survivor空间太小,致使溢出; Eden区太小,致使晋升速率提升; 存在大对象;

为何说老年代的Full GC要比新生代的Minor GC慢?

  • 新生代执行速度快,由于直接从GC Roots出发就追踪哪些对象是活的便可,新生代存活对象是不多的,这个速度是很快的,不须要追踪多少对象,最后直接把存活对象放入Survivor中,就一次性直接回收Eden和以前使用的Survivor了。

  • 在老年代回收并发标记阶段,他须要追踪全部存活对象,老年代存活对象不少,这个过程就很慢。

  • 从新标记这个过程要标记整堆,并发清理阶段并非一次性回收一大片内存,而是找到零零散散在各个地方的垃圾对象,速度也很慢。

  • 最后还须要执行一次内存碎片整理,把大量的存活对象给挪在一块儿,空来联系内存空间,这个过程还得STW。

  • 并发清理时,若是剩余内存空间不足以存放要进入老年代的对象,会引起”Concurrent Mode Failure“问题,这时会采用”Serial Old“垃圾回收器,STW以后会重新进行一次Old GC,这就更耗时了。

G1垃圾回收器

   JDK8后出现了G1垃圾回收器,经过-XX:+UseG1GC来指定G1垃圾回收器,是当下比较先进的垃圾回收器。G1能够作到让你来设定垃圾回收对系统的影响,他本身经过把内存拆分为大量小Region,以及追踪每一个Region中能够回收的对象大小和预估时间,最后在垃圾回收的时候,尽可能把垃圾回收对系统形成的影响控制在你指定的时间范围内,同时在有限的时间内尽可能回收尽量躲的垃圾对象。

G1垃圾回收器特色

  • 把java堆内存分为多个大小相等的Region
  • 逻辑上,也会有新生代和老年代的概念
  • 能够设置一个垃圾回收的预期停顿时间

region设置问题

  • -XX:+UseG1GC来指定G1垃圾回收器,此时会自动用堆大小除以2048,jvm最多能够有2048个Region,而后Region的大小必须是2的倍数,好比说1MB、2MB、4MB之类的。能够经过-XX:G1HeapRegionSize指定。
  • 默认新生代对堆内存的占比是5%,也能够经过-XX:G1NewSizePercent来设置新生代初始占比的,其实维持这个默认值便可。系统运行中,JVM其实会不停地给新生代增长更多的Region,可是新生代占比最多不超过60%,能够经过-XX:G1MaxNewSizePercent来设置。

G1新生代是如何回收的?

   新生代也是有eden和survivor划分的,也是经过-XX:SurvivorRatio能够划分eden和survivor各自大小。触发垃圾回收的机制也是相似的,随着不停地在新生代eden对应的region中放对象,jvm会不停地给新生代加入更多的region,直到新生代占堆大小的最大比例60%,好比说新生代1200个region了,里面的eden可能占据了1000个region,每一个survivor是100个region,并且eden区还占满了对象,这时会触发新生代gc,g1采用以前说过的复制算法进行垃圾回收,进入一个STW状态,并发eden对应的region中的存活对象放入S1的region中,接着回收掉eden对应的region中的垃圾对象。    g1是能够设定目标gc停顿时间的,也就是g1执行gc的时候最多可让系统停顿多长时间,能够经过-XX:MaxGCPauseMills参数来设定,默认值是200ms。

G1对象何时重新生代进入老年代呢?

  • 对象在新生代躲过来不少次的垃圾回收,达到来必定的年龄了,-XX:MaxTenuringThreshold参数能够设置这个年龄,他就会进入老年代
  • 动态年龄断定规则,若是一旦发现某次新生代GC事后,存活对象超过了Survivor的50%,此时判断,好比年龄为1岁,2岁,3岁,4岁的对象的大小总和超过了Survivor的50%,此时4岁以上的对象所有进入老年代,这就是动态年龄断定规则

大对象Rregion

   对于G1内存模型来讲,G1提供了专门的Region来存放大对象,而不是让大对象进入老年代的Region中。在G1中,大对象的断定规则就是一个大对象超过了一个region的50%,好比一个region是2MB,只要一个大对象超过了1MB,就会被放入大对象专门的region中,并且一个大对象若是太大,可能会横跨多个region来存放。在新生代、老年代在回收的时候,会顺带着大对象一块儿回收。

发新生代+老年代混合垃圾回收

   G1有一个参数,-XX:InitiatingHeapOccupancyPercent,默认值是45%,若是老年代占据了堆内存45%的Region的时候,此时就会尝试触发一个新生代+老年代一块儿回收的混合回收阶段。

G1垃圾回收过程

  • 初始标记 仅仅标记一下GC Roots直接能引用的对象,这个过程速度是很快的,须要进入STW状态。
  • 并发标记 这个阶段会容许系统程序的运行,同时进行GC Roots追踪,从GC Roots开始追踪全部的存活对象,并发阶段仍是很耗时的,由于要追踪所有的存活对象。可是这个阶段是能够和系统程序并发运行,因此对系统程序的影响不太大。并且JVM会对并发标记阶段对对象作出的一些修改记录起来,好比哪一个对象被新建了,哪一个对象失去了引用。
  • 最终标记 这个阶段会进入STW,系统程序是禁止运行的,可是会根据并发标记阶段记录的那些对象修改,最终标记一下有哪些存活对象,有哪些是垃圾对西那个。
  • 混合回收 这个阶段会计算老年代中每一个Region中的存活对象数量,存活对象的占比,还有执行垃圾回收的预期性能和效率。接着会中止系统程序,而后尽心尽力尽快进行垃圾回收,此时会选择部分Region进行回收,由于必须让垃圾回收的停顿时间控制在咱们指定的范围内。混合回收,会重新生代、老年代、大对象里各自挑选一些Region,保证指定的时间(好比200ms)回收尽量多的垃圾。

G1垃圾回收器的一些参数

  • -XX:G1MixedGCCountTarget,就是在一次混合回收的过程当中,最后一个阶段执行几回混合回收,默认值是8次。意味着最后一个阶段,先中止系统运行,混合回收一些Region,再恢复系统运行,接着再次禁止系统运行,混合回收一些Region,反复8次。反复回收屡次的意义在于,尽量让系统不要停顿时间过长,能够在屡次回收的间隙,也运行一下。
  • -XX:G1HeapWastePercent,默认值是5%,在混合回收的时候,对Region回收都是基于复制算法进行的,都是把要回收的Region里的存活对象放入其余Region,而后这个Region中的垃圾对象所有清理掉。这样的话在回收过程不断空出来新的Region,一旦空闲出来的Region数量达到了堆内存的5%,此时就会当即中止混合回收,意味着本次混合回收就结束了,也就是说进行4次混合回收后,发现空闲Region达到了5%,就不会进行后续的混合回收。从这里也能看出G1总体是基于复制算法进行Region垃圾回收的,不会出现内存碎片的问题,不须要像CMS那样标记-清理以后,再进行内存碎片的整理。
  • -XX:G1MixedGCLiveThresholdPercent,默认值85%,当一个Region的存活对象多余85%,这个时候就不会回收。由于copy到别的Region的成本也是很高的。

回收失败时的Full GC

若是在进行Mixed回收的时候,不管是年轻代仍是老年代都基于复制算法进行回收,都要把各个Region的存活对象copy到别的Region里去,万一出现copy的过程当中发现没有空闲Region能够承载本身的存活对象了,就会触发一次失败。一旦失败,立马就会切换为中止系统程序,而后采用单线程进行标记、清理和压缩整理,空闲出来一批Region,这个过程是极慢极慢的。

如何对JVM参数进行优化?

G1垃圾回收器

G1新生代优化

  • 给整个JVM堆区域足够的内存
  • 合理设置-XX:MaxGCPauseMills参数,参数设置小,每次gc停顿时间可能特别短,gc频率提升。参数设置过大,可能G1会运去不停地在新生代分配新的对象,而后积累了不少对象,再一次性回收几百个Region。

mixed gc优化

   老年代在堆内存里占比超过45%触发mixed gc 优化的思路仍是尽可能避免对象过快进入老年代,尽可能避免频繁触发mixed gc。优化的核心点是:避免老年代达到InitiatingHeapOccupancyPercent设置的值,即避免对象过快进入老年代。

  • 1.让垃圾对象尽可能在新生代就被回收掉,尽可能让短命对象不进老年代。也就是合理设置—XX:SurvivorRatio值。
  • 2.提升触发mixed gc时InitiatingHeapOccupancyPercent的值,这样mixed gc几率下降,但这样作会加大gc回收时计算负担。
  • 3.合理调节-XX:MaxGCPauseMills参数的值,保证他的新生代gc别太频繁的同时,还得考虑每次gc事后的存活对象有多少,避免存活对象太多,快速进入老年代,频发触发mixed gc。

parnew+cms垃圾回收器

   合理分配堆内存,经过调整s区和e区大小来控制进入老年代对象速度,从而减小频繁old gc。

两种垃圾回收器对比

  1. parnew+cms和g1回收的最大区别是是否会进行整堆回收。
  2. g1能够设置预估停顿时间,适用于低延迟应用
  3. g1从总体上看采用复制算法,适合会产生大量碎片的应用。
  4. parnew+cms回收器比较适合内存小,对象在新生代存活周期短的应用;g1适合内存较大的计算应用,由于整堆回收会比较耗时。

gc日志解读

   学会解读gc日志能够很好地分析堆使用状况,是进行调优及解决频繁full gc必备技能。下面咱们以parnew+cms垃圾回收器为例,分析下gc日志。

新生代gc日志

public class JvmTest {

    public static void main(String[] args) {
        byte[] array1 = new byte[1024*1024];
        array1 = new byte[1024*1024];
        array1 = new byte[1024*1024];
        array1 = null;

        byte[] array2 = new byte[2*1024*1024];
    }
}
复制代码

0.268: [GC (Allocation Failure) 0.269: [ParNew: 4030K->512K(4608K), 0.0015734 secs] 4030K->574K(9728K), 0.0017518 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

Heap

par new generation total 4608K, used 2601K [0x00000000ff600000, 0x00000000ffb00000, 0x00000000ffb00000)
eden space 4096K, 51% used [0x00000000ff600000, 0x00000000ff80a558, 0x00000000ffa00000)
from space 512K, 100% used [0x00000000ffa80000, 0x00000000ffb00000, 0x00000000ffb00000)
to space 512K, 0% used [0x00000000ffa00000, 0x00000000ffa00000, 0x00000000ffa80000)

concurrent mark-sweep generation total 5120K, used 62K [0x00000000ffb00000, 0x0000000100000000, 0x0000000100000000)

Metaspace used 2782K, capacity 4486K, committed 4864K, reserved 1056768K class space used 300K, capacity 386K, committed 512K, reserved 1048576K

  • 0.268表示系统运行268毫秒后触发了本次gc
  • GC (Allocation Failure) 表示对象分配失败触发GC
  • [ParNew: 4030K->512K(4608K), 0.0015734 secs] ParNew标示年轻代GC,4608k标示年轻代可用空间4.5MB, 即eden 区 + 1个survivor区大小。4030K->512K表示GC以前使用了4030K,GC以后只有512K的对象。
  • 4030K->574K(9728K);4030K表示gc前整堆的使用了4030K,gc后使用了574K,整堆大小是9728K。
  • 0.0015734 secs 表示此次gc耗时1.5ms。 注意:在GC以前,Eden区里放里3个1MB的数组,一共3MB,也就是3072KB的对象,但为里存储这个数组,JVM 0.268: [GC (Allocation Failure) 0.269: [ParNew: 4030K->512K(4608K), 0.0015734 secs] 4030K->574K(9728K), 0.0017518 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]

JVM退出时打印当前堆内存的使用状况,分析以下:

  • par new generation total 4608K, used 2601K;说明ParNew垃圾回收器负责的年轻代共有4608KB可用内存,目前使用 了2601KB。
  • from space,100% used;代表以前gc后存活下来的512KB的未知对象将from space占满
  • to space,0% used; from space与to space两个区域不能同时被使用,其中一个存放前一次gc存活对象后,另外一个就是闲置的。
  • concurrent mark-sweep generation total 5120K, used 62K;代表使用Concurrent Mark-Sweep垃圾回收器,即CMS垃圾回收器,老年代内存空间一共是5MB,此时使用了62KB的空间。

解析Metaspace的使用状况

   Metaspace是从JVM进程的虚拟地址空间中分离出来的,用以保存类元数据。JVM在启动时根据-XX:MetaspaceSize保留初始大小,该大小具备特定于平台的默认值。

   Metaspace由一个或多个虚拟空间组成。虚拟空间是由操做系统得到的连续地址空间。他们是按需分配的。在分配时,虚拟空间预留(reserves)了操做系统的内存,但尚未提交。Metaspace reserved是全部虚拟空间的总大小。虚拟空间中的分配单元是Metachunk,当从虚拟空间分配新块时,相应的内存将committed, Metaspace committed是全部块的总大小。 从 docs.oracle.com/javase/8/do… 中能够对used,committed,reserved,capacity有了概述解释;

In the line beginning with Metaspace, the used value is the amount of space used for loaded classes. The capacity value is the space available for metadata in currently allocated chunks. The committed value is the amount of space available for chunks. The reserved value is the amount of space reserved (but not necessarily committed) for metadata. The line beginning with class space line contains the corresponding values for the metadata for compressed class pointers.

  • used表示加载的类的空间量,capacity表示当前分配块(非空闲块)的空间,其小于commited的空间量;committed表示当从虚拟空间分配新块时,相应的内存将会被提交,即已经申请提交的可用分配块,其大小要大于used的;reserved指元空间的总大小,空间被分红块,每一个块只能包含与某一个类加载器关联的类元数据。关于这几个参数的定义解释能够参考一篇文章: www.jianshu.com/p/cd34d6f3b…
  • class space是指实际上被用于放class的那块内存的和,关于这块,从此会进行详细分析。

Full GC日志分析

   Full GC有如下表象,如机器CPU负载太高,系统没法处理请求或者处理过慢。引发Full GC的缘由有不少,主要有JVM参数设置不合理和代码层面问题两大类。JVM参数设置不合理,如新生代堆内存大小设置不合理、Eden与Survivor比例设置不合理,抑或是metaspace设置太小等。代码层面问题,主要是程序员本身的问题,好比说对外提供查询接口没有作限制,一次查询太多对象;应用中存在频繁大量导出,且查询没有限制条件;代码中显示调用gc等。

public class FullGCTest {
    public static void main(String[] args) {
        byte[] array1 = new byte[4*1024*1024];
        array1 = null;
        byte[] array2 = new byte[2*1024*1024];
        byte[] array3 = new byte[2*1024*1024];
        byte[] array4 = new byte[2*1024*1024];
        byte[] array5 = new byte[128*1024];
        byte[] array6 = new byte[2*1024*1024];
    }
}
复制代码

   结合上述配置,咱们能够发现,数组array1这个大对象会直接进入老年代;以后连续分配了4个数组,其中3个是2MB的数组,1个是128KB的数组,会所有进入eden区。当再分配array6时,会发现eden区空间不够,须要触发一次minor gc,可是因为array2,array3,array4,array5都被变量引用了,会直接进入老年代,由于老年代里已经存在4MB的数据了,难以存放这么大的数据,所以会触发一次Full GC。Full GC会对老年代进行Old GC,同时通常会跟一次Young GC关联,还会触发一次Metaspace的GC。下面咱们分析下GC日志。

0.308: [GC (Allocation Failure) 0.308: [ParNew (promotion failed): 7260K->7970K(9216K), 0.0048975 secs]0.314: [CMS: 8194K- >6836K(10240K), 0.0049920 secs] 11356K->6836K(19456K), [Metaspace: 2776K->2776K(1056768K)], 0.0106074 secs] [Times: user=0.00 sys=0.00, real=0.01 secs]

Heap

par new generation total 9216K, used 2130K [0x00000000fec00000, 0x00000000ff600000, 0x00000000ff600000)

eden space 8192K, 26% used [0x00000000fec00000, 0x00000000fee14930, 0x00000000ff400000)

from space 1024K, 0% used [0x00000000ff500000, 0x00000000ff500000, 0x00000000ff600000)

to space 1024K, 0% used [0x00000000ff400000, 0x00000000ff400000, 0x00000000ff500000)

concurrent mark-sweep generation total 10240K, used 6836K [0x00000000ff600000, 0x0000000100000000, 0x0000000100000000)

Metaspace used 2782K, capacity 4486K, committed 4864K, reserved 1056768K class space used 300K, capacity 386K, committed 512K, reserved 1048576K

  • ParNew (promotion failed): 7260K->7970K(9216K), 0.0048975 secs;Eden区回收前有7000多KB的对象,回收以后发现一个都回收不掉,主要因为上述几个数组被变量引用了;出现promotion 失败的缘由主要是Minor GC时,Survivor Space放不下,对象只能放入老年代,而此时老年代也放不下形成的。
  • CMS: 8194K- >6836K(10240K), 0.0049920 secs;代表old gc前老年代对象大小是8194KB,old gc后存活对象大小是6836K,老年代总空间大小是10240K。看到这里可能会有一个疑惑,为何old gc前会有8194K的对象呢?这主要是young gc后,往老年代放入了2个2MB的对象。后续继续存放1个2MB和1个128KB的数组到老年代中,放不下,触发Full GC。
  • 11356K->6836K(19456K);代表old gc前整堆使用了11356K,old gc后整堆使用了6836K。

下面分析full gc后堆内存的使用状况

  • par new generation total 9216K, used 2130K;代表Full GC后,剩余的2MB存放在enden区了。
  • from space 1024K, 0% used;young gc时没有存活对象对象;
  • to space 1024K, 0% used;to space自己没有参与到此次gc中,不存在使用情景;
  • concurrent mark-sweep generation total 10240K, used 6836K;代表使用CMS垃圾回收器,新生代中的6836K所有对象进入了老年代;

   尽可能让每次Young GC后的存活对象⼩于Survivor区域的50%,都留存在年轻代⾥。尽可能别让对象进 ⼊⽼年代。尽可能减小Full GC的频率,避免频繁Full GC对JVM性能的影响。

上线时如何肯定jvm参数?

   系统通过单测、集测及测试环境后,进入预发环境进行压测,观察内存使用、Young GC的触发频率,Young GC的耗时,每次YoungGC后有多少对象是存活下来的,每次Young GC事后有多少对象进⼊了⽼年代,⽼年代对象增加的速率,Full GC的触发频率。

经过ps -ef | grep java获取java进程pid,利用jstat工具查看gc状况;

[tian~]$ jstat -gc 2236
 S0C    S1C    S0U    S1U      EC    EU        OC        OU        MC      MU      CCSC    CCSU      YGC     YGCT    FGC     FGCT     GCT   
20480.0 20480.0 269.9 0.0  163840.0 97683.3  319488.0  271892.4  673268.0 661182.8 78048.0 75954.8   508    9.526    18      1.737   11.263
复制代码
S0C:这是From Survivor区的⼤⼩
S1C:这是To Survivor区的⼤⼩
S0U:这是From Survivor区当前使⽤的内存⼤⼩
S1U:这是To Survivor区当前使⽤的内存⼤⼩
EC:这是Eden区的⼤⼩
EU:这是Eden区当前使⽤的内存⼤⼩
OC:这是⽼年代的⼤⼩
OU:这是⽼年代当前使⽤的内存⼤⼩
MC:这是⽅法区(永久代、元数据区)的⼤⼩
MU:这是⽅法区(永久代、元数据区)的当前使⽤的内存⼤⼩
YGC:这是系统运⾏迄今为⽌的Young GC次数
YGCT:这是Young GC的耗时
FGC:这是系统运⾏迄今为⽌的Full GC次数
FGCT:这是Full GC的耗时
GCT:这是全部GC的总耗时
复制代码

   能够利用jstat -gc PID 1000 10命令,每隔1s更新出来最新的一行jstat统计信息,一共执行10次统计,观察每隔一段时间jvm中eden区对象占用变化。若是系统访问量较低,能够适当延长观察时间长度,这样就能够大体推测出每次gc停顿时间长度。如今也有比较好的可视化监测工具如JVisualVM和Cat等。

经常使用GC参数

-Xmx8g -Xms8g -Xmn2g -Xss256k  Xms、Xmx表示堆的大小,Xmn表示年轻代大小,Xss表示线程栈擦小,默认1M
-XX:SurvivorRatio=2 新生代中Eden与Survivor比值,调优的关键,也就是调节新生代堆大小及SurvivorRatio的值,尽可能让新生代垃圾对象存放在Survivor中;
-XX:MetaspaceSize=256m  
-XX:MaxMetaspaceSize=256m 元空间大小
-XX:+UseParNewGC 用并行收集器 ParNew 对新生代进行垃圾回收
-XX:+UseConcMarkSweepGC 并发标记清除收集器 CMS 对老年代进行垃圾回收。
-XX:ParallelGCThreads=2 Young GC工做时的并行线程数
-XX:ParallelCMSThreads=3 CMS GC 工做时的并行线程数
-XX:+CMSParallelRemarkEnabled 并行运行最终标记阶段,加快最终标记的速度
-XX:+CMSParallelInitialMarkEnabled 初始阶段开启多线程并发执行,减小STW时间
-XX:+CMSScavengeBeforeRemark 在CMS从新标记阶段以前,执行一次Young GC,由于从新标记是整堆标记的,执行一次Young GC,回收调年轻代里没人引用的对象,减小扫描对象。
-XX:MaxTenuringThreshold=15 对象重新生代晋升到老年代的年龄阈值(每次 Young GC 留下来的对象年龄加一),默认值15
-XX:+UseCMSCompactAtFullCollection 开启碎片整理
-XX:CMSFullGCsBeforeCompaction=2 与-XX:+UseCMSCompactAtFullCollection配合使用,表示进行2次Full GC后进行整理
-XX:+UseCMSInitiatingOccupancyOnly 只根据老年代使用比例来决定是否进行CMS
-XX:CMSInitiatingOccupancyFraction=80 设置触发CMS老年代回收的内存使用率占比,达到80%时触发old gc
-XX:+CMSClassUnloadingEnabled 默认开启,表示开启 CMS 对元空间的垃圾回收,避免因为元空间耗尽带来 Full GC
-XX:-DisableExplicitGC 禁止代码中显示调用GC
-XX:+HeapDumpOnOutOfMemoryError OOM时dump内存快照
-verbose:gc 表示输出虚拟机中GC的详细状况
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+PrintGCDateStamps
-Xloggc:/app/log/xxx.log gc文件未知
复制代码

OOM分析

   发生OOM的区域主要有三块,一个Metaspace区域,一个是虚拟机栈内存,一个是堆内存空间。

Metaspace内存溢出

   Full GC时,必然会尝试回收Metaspace区域中的类,固然回收条件是比较苛刻的,如这个类的类加载器先要被回收,类的全部对象实例都要被回收等,一旦Metaspace区域满类,未必能回收掉里面不少的类,JVM没有回收太多空间,随着程序运行,还要继续往Metaspace区域中塞入更多的类,直接就会引起内存溢出问题。 引发Metaspace内存溢出的缘由

  • Metaspace设置多小;
  • 大量使用cglib之类的技术动态生成一些类,致使生成的类过多,将Metaspace塞满,引发内存溢出;

栈内存溢出

   每一个线程的虚拟机栈的大小是固定的,线程调用一个方法,都会将本次方法调用的栈桢压入虚拟机栈里,这个栈枕里是有方法的局部变量的。致使栈内存溢出的主要缘由是出现类递归调用。

堆内存溢出

   堆内存溢出主要是eden区不断有存活对象进入老年代,触发full gc后发现老年代回收对象较少,老年代仍然有大量存活对象,年轻代仍然有一批对象等着放进老年代,可是放不下,这时候抛出内存溢出异常。 通常来讲,引发内存溢出主要有两种场景:

  • 系统承载⾼并发请求,由于请求量过⼤,致使⼤量对象都是存活的,因此要继续放⼊新的对象实在是不⾏了,此时就会引起OOM系统崩溃。
  • 系统有内存泄漏的问题,就是莫名其妙弄了不少的对象,结果对象都是存活的,没有及时取消对他们的引⽤,致使触发GC仍是⽆法回收,此时只能引起内存溢出,由于内存实在放不下更多对象了。 所以总结起来,⼀般引起OOM,要否则是系统负载过⾼,要否则就是有内存泄漏的问题。
相关文章
相关标签/搜索