现象描述:堆内存占用较快,运行到3天时就达到100%,并触发了老年代GChtml
JVM知识回顾:
Java的堆内存由新生代(New or Young)和老年代(Old)组成。新生代进一步划分为一个Eden空间和两个Survivor空间S0、S1(也称为From、To)。Eden空间是对象被建立时的地方,通过新生代GC后存活的对象存放到Survivor空间,通过几轮新生代GC仍然存活会存放到Old区。java
在JVM默认参数下,Survivor空间过小。致使在GC清理后,Eden区不少对象没法存放到Survivor区。所以,这些对象被过早地保存在老年代中,这会致使老年代占用增加很快。经过jmap -heap pid查看堆内存分布状况以下:python
上图是设置了SurviorRatio=6时(Eden区与Survivor区比例为6:1),按照计算发现Survior区比设置的小不少。这个数值仍是动态变化的,有时候十几兆,有时候几兆。缘由是:JDK8使用ParallelGC做为默认GC算法,在这个算法下使用了“AdaptiveSizePolicy”策略,它会动态调整Eden和Survivor的空间。若是开启 AdaptiveSizePolicy,则每次 GC 后会从新计算 Eden、From 和 To 区的大小,计算依据是 GC 过程当中统计的 GC 时间、吞吐量、内存占用量。算法
关闭AdaptiveSizePolicy有两种办法:缓存
1. 经过JMV参数关闭:-XX:-UseAdaptiveSizePolicy服务器
2. 老年代使用CMS回收器,CMS不会开启UseAdaptiveSizePolicy。开启CMS回收器,新生代会默认使用ParNew GC回收器(并发收集器)。 数据结构
-XX:SurvivorRatio=6 -XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=70 -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+CMSScavengeBeforeRemark -XX:NativeMemoryTracking=detail
-XX:+UseConcMarkSweepGC 使用CMS收集器 -XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次碎片整理;整理过程是独占的,会引发停顿时间变长 -XX:+CMSFullGCsBeforeCompaction 设置进行几回Full GC后,进行一次碎片整理 -XX:ParallelCMSThreads 设定CMS的线程数量(通常状况约等于可用CPU数量) -XX:+CMSScavengeBeforeRemark 在remark以前强制进行一次Young GC。
修改参数优化后:多线程
优化后效果:并发
Java程序占用内存分为堆内存和非堆内存,堆内存里分新生代和老年代,非堆内存包含方法区、jdk8的MetaSpace、栈空间、程序计数器等。咱们常常关注的是堆内存,而较少关注非堆内存。酒店静态外网接口应用缩容时,容器内存是2G,并修改了堆内存最大限制为1.5G(-Xms1536m -Xmx1536m)。也就是说java进程的非堆内存+操做系统的其余进程占用的内存若是不超过500M,就不会引发oom问题。经过使用Native Memory Tracking (NMT) 来分析JVM内部内存使用状况,能够发现非堆内存占用达到800~900M。所以系统长时间运行后,内存占用可能会超出2G,所以宕机。oracle
Native Memory Tracking (NMT) 是Hotspot VM用来分析VM内部内存使用状况的一个功能。在JVM参数中加入-XX:NativeMemoryTracking=detail,打开NMT。而后登陆堡垒机,经过jcmd VM.native_memory summary命令,能够查看内存占用状况。
下面是对一个2G内存容器,Xmx为1G的应用的统计。每一项含义说明,见官方说明。
其中第一项Java Heap是堆内存。其余就能够理解外非堆内存的占用,共700M。其中Thread项是线程占用的内存,默认每一个线程1M,一共303个线程占用了300M。
Native Memory Tracking: Total: reserved=3000668KB, committed=1755712KB - Java Heap (reserved=1048576KB, committed=1048576KB) (mmap: reserved=1048576KB, committed=1048576KB) - Class (reserved=1126128KB, committed=85464KB) (classes #13164) (malloc=1776KB #25469) (mmap: reserved=1124352KB, committed=83688KB) - Thread (reserved=311789KB, committed=311789KB) (thread #303) (stack: reserved=310456KB, committed=310456KB) (malloc=979KB #1567) (arena=355KB #605) - Code (reserved=258501KB, committed=54209KB) (malloc=8901KB #11187) (mmap: reserved=249600KB, committed=45308KB) - GC (reserved=101204KB, committed=101204KB) (malloc=97784KB #800) (mmap: reserved=3420KB, committed=3420KB) - Compiler (reserved=456KB, committed=456KB) (malloc=325KB #600) (arena=131KB #3) - Internal (reserved=87776KB, committed=87776KB) (malloc=87744KB #19745) (mmap: reserved=32KB, committed=32KB) - Symbol (reserved=18473KB, committed=18473KB) (malloc=14533KB #158025) (arena=3940KB #1) - Native Memory Tracking (reserved=3674KB, committed=3674KB) (malloc=215KB #3261) (tracking overhead=3459KB) - Arena Chunk (reserved=226KB, committed=226KB) (malloc=226KB) - Unknown (reserved=43864KB, committed=43864KB) (mmap: reserved=43864KB, committed=43864KB)
解决方案建议
一、设置Xmx和Xms时要为堆外内存留出足够的空间,建议大于1G,若是内存资源容许大于2G。
二、优化JVM参数。
a) 对应接口类型的应用,使用CMS回收器,减小停顿时间,保证接口性能。
-XX:SurvivorRatio=6-XX:+UseConcMarkSweepGC -XX:CMSInitiatingOccupancyFraction=70-XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0-XX:+CMSScavengeBeforeRemark -XX:NativeMemoryTracking=detail
b) 对应后台定时任务,MQ消费监听等类型的应用,使用默认的Parallel Scavenge+Parallel Old回收器,优先保证吞吐量。并关闭UseAdaptiveSizePolicy参数。
-XX:SurvivorRatio=6-XX:-UseAdaptiveSizePolicy
JVM GC只回收堆区和方法区内的对象。而栈区的数据,在超出做用域后会被JVM自动释放掉,因此其不在JVM GC的管理范围内。
(1)判断对象是否存活
a)引用计数算法:
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器+1,当引用失效,计数器-1。任什么时候刻计数器为0的对象就是不可能再被使用的。
优势:实现简单,断定效率高效,被actionscript3和python中普遍应用。
缺点:没法解决对象之间的相互引用问题。java没有采纳
b)可达性分析算法:
经过一系列称为“GC Roots”的对象做为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GCRoots没有任何引用链相连的时候,则证实此对象是不可用的。
好比以下,右侧的对象是到GCRoot时不可达的,能够断定为可回收对象。
在java中,能够做为GCRoot的对象包括如下几种:
* 虚拟机栈中引用的对象。
* 方法区中静态属性引用的对象。
* 方法区中常量引用的对象。
* 本地方法中JNI引用的对象。
基于以上,咱们能够知道,当当前对象到GCRoot中不可达时候,即会知足被垃圾回收的可能。
(2)判断是否能够被回收
那么是否是这些对象就非死不可,也不必定,此时只能宣判它们存在于一种“缓刑”的阶段,要真正的宣告一个对象死亡。至少要经历两次标记:
第一次:对象可达性分析以后,发现没有与GCRoots相链接,此时会被第一次标记并筛选。
第二次:对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,此时会被认定为不必执行。
在finalize里能够将该对象从新赋予给某个引用,从而使对象不会被回收。
Eden区空间不够存放新对象的时候,执行Minor GC。升到老年代的对象大于老年代剩余空间的时候执行Full GC,或者小于的时候被HandlePromotionFailure 参数强制Full GC 。
调优主要是减小 Full GC 的触发次数,能够经过 NewRatio(新生代和老年代的大小比例) 控制新生代转老年代的比例,经过MaxTenuringThreshold 设置对象进入老年代的年龄阀值。
基于分代垃圾回收机制,新生代分为:Eden区、两个Survivor区(From区和To区),每次只占用Eden区和一个Survivor区,另一个Survivor区空闲。新建立的对象优先在Eden区分配分配内存空间,当Eden区的空间不足以存放新对象时,会触发一次Minor GC,将Eden区和一个Survivor区中存活的对象复制到另外一个Survivor区,而且对象的年龄+1,而后清空Eden区和Survivor区。在知足以下状况之一,新生代中的对象会移动到老年代:
(1)Eden区满时,进行Minor GC,当Eden和一个Survivor区中依然存活的对象没法放入到另外一个Survivor中,则经过分配担保机制提早转移到老年代中。
(2)若对象体积太大, 新生代没法容纳这个对象,-XX:PretenureSizeThreshold超过这个值的时候,对象直接在old区分配内存,默认值是0,意思是无论多大都是先在eden中分配内存, 此参数只对Serial及ParNew两款收集器有效。
(3)长期存活的对象将进入老年代。
虚拟机对每一个对象定义了一个对象年龄(Age)计数器。当年龄增长到必定的临界值时,就会晋升到老年代中,该临界值由参数:-XX:MaxTenuringThreshold来设置。
若是对象在Eden出生并在第一次发生MinorGC时仍然存活,而且可以被Survivor中所容纳的话,则该对象会被移动到Survivor中,而且设Age=1;之后每经历一次Minor GC,该对象还存活的话Age=Age+1。
(4)动态对象年龄断定。
虚拟机并不老是要求对象的年龄必须达到MaxTenuringThreshold才能晋升到老年代,若是在Survivor区中相同年龄(设年龄为age)的对象的全部大小之和超过Survivor空间的一半,年龄大于或等于该年龄(age)的对象就能够直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。
JVM引入动态年龄计算,主要基于以下两点考虑:
若是固定按照MaxTenuringThreshold设定的阈值做为晋升条件: a)MaxTenuringThreshold设置的过大,本来应该晋升的对象一直停留在Survivor区,直到Survivor区溢出,一旦溢出发生,Eden+Svuvivor中对象将再也不依据年龄所有提高到老年代,这样对象老化的机制就失效了。 b)MaxTenuringThreshold设置的太小,“过早晋升”即对象不能在新生代充分被回收,大量短时间对象被晋升到老年代,老年代空间迅速增加,引发频繁的Major GC。分代回收失去了意义,严重影响GC性能。
相同应用在不一样时间的表现不一样:特殊任务的执行或者流量成分的变化,都会致使对象的生命周期分布发生波动,那么固定的阈值设定,由于没法动态适应变化,会形成和上面相同的问题。
持久代(Permanent generation)也称之为方法区(Method area):用于保存类常量以及字符串常量。注意,这个区域不是用于存储那些从老年代存活下来的对象,这个区域也可能发生GC。发生在这个区域的GC事件也被算为 Major GC 。只不过在这个区域发生GC的条件很是严苛,必须符合如下三种条件才会被回收:
一、全部实例被回收
二、加载该类的ClassLoader 被回收
三、Class 对象没法经过任何途径访问(包括反射)
Major GC:清理永久代,可是因为不少MojorGC 是由MinorGC 触发的,因此有时候很难将MajorGC 和MinorGC区分开。
FullGC:是清理整个堆空间—包括年轻代和永久代。FullGC 通常消耗的时间比较长,远远大于MinorGC,所以,有时候咱们必须下降FullGC 发生的频率。
(1)“标记-清除”(Mark-Sweep)算法:首先标记出全部须要回收的对象,在标记完成后统一回收掉全部被标记的对象。
它的主要缺点有两个:一个是效率问题,标记和清除过程的效率都不高;另一个是空间问题,标记清除以后会产生大量不连续的内存碎片,空间碎片太多可能会致使,当程序在之后的运行过程当中须要分配较大对象时没法找到足够的连续内存而不得不提早触发另外一次垃圾收集动做。
(2)“复制”(Mark-Copying)算法,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,而后再把已使用过的内存空间一次清理掉。
这样使得每次都是对其中的一块进行内存回收,内存分配时也就不用考虑内存碎片等复杂状况,只要移动堆顶指针,按顺序分配内存便可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,持续复制长生存期的对象则致使效率下降。
(3)“标记-整理”(Mark-Compact)算法,标记过程仍然与“标记-清除”算法同样,但后续步骤不是直接对可回收对象进行清理,而是让全部存活的对象都向一端移动,而后直接清理掉端边界之外的内存。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少许存活,所以通常选用复制算法,只须要付出少许存活对象的复制成本就能够完成收集。而老年代中由于对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”或“标记-整理”算法来进行回收。
(1)每次垃圾回收的时间愈来愈长,由以前的10ms延长到50ms左右,FullGC的时间也有以前的0.5s延长到四、5s
(2)FullGC的次数愈来愈多,最频繁时隔不到1分钟就进行一次FullGC
(3)老年代的内存愈来愈大而且每次FullGC后老年代没有内存被释放
以后系统会没法响应新的请求,逐渐到达OutOfMemoryError的临界值。
(1)为何崩溃前垃圾回收的时间愈来愈长?
根据内存模型和垃圾回收算法,垃圾回收分两部分:内存标记、清除(复制),标记部分只要内存大小固定时间是不变的,变的是复制部分,由于每次垃圾回收都有一些回收不掉的内存,因此增长了复制量,致使时间延长。因此,垃圾回收的时间也能够做为判断内存泄漏的依据。
(2)为何Full GC的次数愈来愈多?
所以内存的积累,逐渐耗尽了年老代的内存,致使新对象分配没有更多的空间,从而致使频繁的Full GC垃圾回收。
(3)为何年老代占用的内存愈来愈大?
由于年轻代的内存没法被回收,愈来愈多的对象被Copy到年老代。
(1)将进入老年代的对象数量降到最低
(2)减小Full GC的执行时间
(1)针对JVM堆的设置,通常能够经过-Xms -Xmx限定其最小、最大值,为了防止垃圾收集器在最小、最大之间收缩堆而产生额外的时间,咱们一般把最大、最小设置为相同的值。
(2)年轻代和年老代将根据默认的比例(1:2)分配堆内存,能够经过调整两者之间的比率NewRadio来调整两者之间的大小。也能够针对回收代,好比年轻代,经过 -XX:newSize -XX:MaxNewSize来设置其绝对大小。一样,为了防止年轻代的堆收缩,咱们一般会把-XX:newSize -XX:MaxNewSize设置为一样大小。
(3)合理设置年轻代和老年代大小
更大的年轻代必然致使更小的年老代,大的年轻代会延长普通GC的周期,但会增长每次GC的时间,小的年老代会致使更频繁的Full GC。
更小的年轻代必然致使更大年老代,小的年轻代会致使young GC很频繁,但每次的GC时间会更短;大的年老代会减小Full GC的频率,可是会增长老年代的gc时间。
如何选择应该依赖应用程序对象生命周期的分布状况:
a)若是应用存在大量的临时对象,应该选择更大的年轻代;
b)若是存在相对较多的持久对象,年老代应该适当增大。
但不少应用都没有这样明显的特性,在抉择时应该根据如下两点:
(A)本着Full GC尽可能少的原则,让年老代尽可能缓存经常使用对象,JVM的默认比例1:2也是这个道理
(B)经过观察应用一段时间,看应用在峰值时年老代会占多少内存,在不影响Full GC的前提下,根据实际状况加大年轻代,好比能够把比例控制在1:1。但应该给年老代至少预留1/3的增加空间。
(4)在配置较好的机器上(好比多核、大内存),能够为年老代选择并行收集算法: -XX:+UseParallelOldGC ,默认为Serial收集器。
(5)线程堆栈的设置:每一个线程默认会开启1M的堆栈,用于存放栈帧、调用参数、局部变量等,对大多数应用而言这个默认值太了,通常256K就足用。理论上,在内存不变的状况下,减小每一个线程的堆栈,能够产生更多的线程,但这实际上还受限于操做系统。最大线程数计算公式以下:
(MaxProcessMemory – JVMMemory – ReservedOsMemory) / (ThreadStackSize) = Number of threads
注:
MaxProcessMemory:进程最大寻址空间。(通常为服务器内存)
JVMMEMORY:JVM的内存空间(堆+永久区)即-Xmx大小 (应该是实际分配大小)
ReservedOsMemory:操做系统预留内存
ThreadStackSize:-Xss大小
(6)能够经过下面的参数打Heap Dump信息
-XX:HeapDumpPath
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-Xloggc:/usr/aaa/dump/heap_trace.txt
经过下面参数能够控制OutOfMemoryError时打印堆的信息
-XX:+HeapDumpOnOutOfMemoryError
(1)一个服务系统,常常出现卡顿,分析缘由,发现Full GC时间太长:
执行命令: jstat -gc pid:
S0C S1C S0U S1U EC EU OC OU MC MU CCSC CCSU YGC YGCT FGC FGCT GCT
4032.0 4032.0 2541.6 0.0 32320.0 17412.6 80620.0 51918.6 88576.0 84546.7 12288.0 11336.9 522 5.408 5 6.946 5.952
解释以下:
S0C:第一个幸存区的大小;S1C:第二个幸存区的大小;S0U:第一个幸存区的使用大小;S1U:第二个幸存区的使用大小;EC:Enden区的大小;EU:Enden区的使用大小;
OC:老年代大小;OU:老年代使用大小;MC:方法区大小;MU:方法区使用大小;CCSC:压缩类空间大小;CCSU:压缩类空间使用大小;YGC:年轻代垃圾回收次数;
YGCT:年轻代垃圾回收消耗时间;FGC:老年代垃圾回收次数;FGCT:老年代垃圾回收消耗时间;GCT:垃圾回收消耗总时间
分析上面的数据,发现Young GC执行了522次,耗时5.408秒,每次Young GC耗时1ms,在正常范围,而Full GC执行了5次,耗时6.946秒,每次平均1.389s,数据显示出来的问题是:Full GC耗时较长,分析该系统的参数发现,NewRatio=9,也就是说,新生代和老生代大小之比为1:9,这就是问题的缘由:
1,新生代过小,致使对象提早进入老年代,触发老年代发生Full GC;
2,老年代较大,进行Full GC时耗时较大;
优化的方法是:调整NewRatio的值,调整到4,发现Full GC没有再发生,只有Young GC在执行。这就是把对象控制在新生代就清理掉,没有进入老年代(这种作法对一些应用是颇有用的,但并非对全部应用都要这么作)
(2)一应用在性能测试过程当中,发现CPU占用率很高,Full GC频繁,使用jmap -dump:format=b,file=文件名.hprof pid 来dump内存,生成dump文件,并使用Eclipse下的mat工具进行分析,发现:
从图中能够看出,这个线程存在问题,队列LinkedBlockingQueue所引用的大量对象并未释放,从而致使一直在执行Full GC,从而致使CPU占用率达到100%。
(1)经过ps -ef | grep java或者执行top -c
,显示进程运行信息列表。按下P,进程按照cpu使用率排序获取程序的pid
(2)查看该pid下线程对应的系统占用状况。top -Hp 8813
,显示一个进程的线程运行信息列表。按下P,进程按照cpu使用率排序
(3)发现pid 8851线程占用的CPU最大
(4)将这几个pid转为16进制, printf “0x%x\n” 8851 为0x2293
(5)下载当前的java线程栈jstack pid>pid.log
将线程栈 dump
到日志文件中
(6)在日志中查询(5)中对应的线程状况,发现都是GC线程致使的
(7)dump java堆数据
jmap -dump:format=b,file=heap.log pid 保存堆快照
(8)使用MAT加载堆文件,能够看到javax.crypto.JceSecurity对象占用了95%的内存空间,初步定位到问题。而后排查代码,查看是什么致使的内存溢出。例如使用了静态变量的Map,因此每次运行到某个方法时都会向这个Map put一个对象,而这个map属于类的维度,因此不会被GC回收。这就致使了大量的new的对象不被回收。
(9)优化代码,若是经过这个流程没法解决问题,或者无法优化代码,那么走最后一步:GC调优:
(1)GC日志介绍
YoungGC日志解释以下:
FullGC日志解释以下:
(2)查看GC 日志
1768.617: [GC [PSYoungGen: 1313280K->31072K(1341440K)] 3990240K->2729238K(4137984K), 0.0992420 secs] [Times: user=0.36 sys=0.01, real=0.10 secs] 1770.525: [GC [PSYoungGen: 1316704K->27632K(1345536K)] 4014870K->2750306K(4142080K), 0.0552640 secs] [Times: user=0.20 sys=0.00, real=0.06 secs] [Full GC [PSYoungGen: 47079K->0K(1350144K)] [ParOldGen: 2780532K->191662K(2796544K)] 2827611K->191662K(4146688K) [PSPermGen: 60530K->60530K(524288K)],3.4921610 secs] [Times: user=13.39 sys=0.08, real=3.49 secs]
日志介绍:
PSYoungGen: 1313280K->31072K(1341440K)]:
格式为[PSYoungGen: a->b(c)]。PSYoungGen表示新生代使用的是多线程垃圾收集器Parallel Scavenge。a为GC前新生代已占用空间,b为GC后新生代已占用空间。新生代又细分为一个Eden区和两个Survivor区,Minor GC以后Eden区为空,b就是Survivor中已被占用的空间。括号里的c表示整个新生代的大小。
3990240K->2729238K(4137984K):
格式为x->y(z)。x表示GC前堆的已占用空间,y表示GC后堆已占用空间,z表示堆的总大小。
由新生代和Java堆占用大小能够算出年老代占用空间,此例中就是4137984K-1341440K=2796544k=2731M。
[Times: user=0.36 sys=0.01, real=0.10 secs]:
提供cpu使用及时间消耗,user是用户态消耗的cpu时间,sys是系统态消耗的cpu时间,real是实际的消耗时间。
分析上述日志,能够看出两个问题:
1. 每次Minor GC,晋升至老年代的对象体积较大,平均为20m+(2750306K-2729238K=21068K=20.57M),这致使老年代占用持续升高,Full GC频率较高,直观现象是内存占用一直升高;
2. Full GC的时间很长,上面看到的是3.49 secs,这致使ull FGC开销很大,直观现象是CPU占用一直上升,达到100%;
于是调优思路很明确:
1. 减小每次Young GC晋升到老年代的对象大小;
2. 尽量的减小每次Full GC的时间开销;
(3)进行了以下的尝试
一. 新生代使用默认的Parallel Scavenge GC,可是加入以下参数 :
-Xmn1350m -XX:-UseAdaptiveSizePolicy -XX:SurvivorRatio=6
调优思路:
Young GC每次晋升到Old Gen的内容较多,而这极可能是由于Parallel Scavenge垃圾收集器会动态的调整JVM的Eden 和Survivor区,致使Survior空间太小,致使更多对象进入老年代。
-Xmn1350m设置堆内新生代的大小。经过这个值能够获得老生代的大小:-Xmx减去-Xmn。
-XX:-UseAdaptiveSizePolicy表示关闭动态调全年轻代区大小和相应的Survivor区比例, -XX:SurvivorRatio=6表示Eden去和Suvior区的大小比例为6:2:2
调优效果:
能够看到调优后full gc频率大为减小(由4min一次--->变为30h一次),同时由于少了频繁调整new gen的开销,ygc耗时也略微减小了。
遗留问题:
虽然Full GC频率大为下降,可是每次Full GC的耗时仍是同样,500ms+~2000ms
二. 老年代改用CMS GC,加入jvm参数以下(原来的配置不变):
-XX:+UseConcMarkSweepGC -XX:+UseCMSCompactAtFullCollection -XX:CMSFullGCsBeforeCompaction=0 -XX:+UseCMSInitiatingOccupancyOnly
-XX:CMSInitiatingOccupancyFraction=70
调优思路:
老年代使用CMS GC,启用碎片整理,下降Full GC的耗时,此时新生代会默认使用ParNew GC收集器
调优效果:
Oldgen GC开销仍是较大,虽然比ps gc略好,并且经过gc日志发现,主要耗时都是在remark的rescan阶段
91832.767: [CMS-concurrent-mark-start] 91834.022: [CMS-concurrent-mark: 1.256/1.256 secs] [Times: user=4.06 sys=0.94, real=1.25 secs] 91834.022: [CMS-concurrent-preclean-start] 91834.040: [GC91834.040: [ParNew: 1091621K->50311K(1209600K), 0.0469420 secs] 3059979K->2018697K(4021504K), 0.0473540 secs] [Times: user=0.16 sys=0.01, real=0.05 secs] 91834.123: [CMS-concurrent-preclean: 0.051/0.101 secs] [Times: user=0.31 sys=0.05, real=0.10 secs] 91834.123: [CMS-concurrent-abortable-preclean-start] 91834.900: [CMS-concurrent-abortable-preclean: 0.769/0.777 secs] [Times: user=2.36 sys=0.53, real=0.78 secs] 91834.903: [GC[YG occupancy: 595674 K (1209600 K)]91834.904: [Rescan (parallel) , 0.6762340 secs]91835.580: [weak refs processing, 0.0728400 secs]91835.653: [scrub string table, 0.0009380 secs] [1 CMS-remark: 1968386K(2811904K)] 2564060K(4021504K), 0.7555510 secs] [Times: user=2.73 sys=0.03, real=0.76 secs] 91835.659: [CMS-concurrent-sweep-start]
三. 下降remark的时间开销,加入参数:-XX:+CMSScavengeBeforeRemark
调优思路:
一般状况下进行remark会先对new gen进行一次扫描,并且这个开销占比挺大,因此加上这个参数,在remark以前强制进行一次Young GC。
Serial收集器是一个串行收集器。在JDK1.3以前是Java虚拟机新生代收集器的惟一选择。目前也是ClientVM下ServerVM 4核4GB如下机器默认垃圾回收器。Serial收集器并非只能使用一个CPU进行收集,而是当JVM须要进行垃圾回收的时候,需暂停全部的用户线程,直到回收结束。
使用算法:新生代复制算法、老年代标记-整理算法;垃圾收集的过程当中会Stop The World(服务暂停)
JVM中文名称为Java虚拟机,所以它像一台虚拟的电脑在工做,而其中的每个线程都被认为是JVM的一个处理器,所以图中的CPU0、CPU1实际上为用户的线程,而不是真正的机器CPU。
Serial收集器虽然是最老的,可是它对于限定单个CPU的环境来讲,因为没有线程交互的开销,专心作垃圾收集,因此它在这种状况下是相对于其余收集器中最高效的。
SerialOld是Serial收集器的老年代收集器版本,它一样是一个单线程收集器,这个收集器目前主要用于Client模式下使用。若是在Server模式下,它主要还有两大用途:一个是在JDK1.5及以前的版本中与Parallel Scavenge收集器搭配使用,另一个就是做为CMS收集器的后备预案,若是CMS出现Concurrent Mode Failure,则SerialOld将做为后备收集器。
ParNew其实就是Serial收集器的多线程版本。除了Serial收集器外,只有它能与CMS收集器配合工做。
使用算法:标记-复制算法
参数控制:-XX:+UseParNewGC ParNew收集器
-XX:ParallelGCThreads 限制线程数量
ParNew是许多运行在Server模式下的JVM首选的新生代收集器。可是在单CPU的状况下,它的效率远远低于Serial收集器,由于线程切换须要消耗时间,因此必定要注意使用场景。
Parallel Scavenge又被称为吞吐量优先收集器,和ParNew 收集器相似,是一个新生代并行收集器。目前是默认垃圾回收器。
使用算法:复制算法
Parallel Scavenge收集器的目标是达到一个可控件的吞吐量,所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 /(运行用户代码时间 + 垃圾收集时间)。若是虚拟机总共运行了100分钟,其中垃圾收集花了1分钟,那么吞吐量就是99% 。
ParallelOld是并行收集器,和SerialOld同样,ParallelOld是一个老年代收集器,是老年代吞吐量优先的一个收集器。这个收集器在JDK1.6以后才开始提供的,在此以前,Parallel Scavenge只能选择Serial Old来做为其老年代的收集器,这严重拖累了Parallel Scavenge总体的速度。而ParallelOld的出现后,“吞吐量优先”收集器才名副其实!
使用算法:标记 - 整理算法
在注重吞吐量与CPU数量大于1的状况下,均可以优先考虑Parallel Scavenge + ParalleloOld收集器。
CMS是一个老年代收集器,全称 Concurrent Low Pause Collector,是JDK1.4后期开始引用的新GC收集器,在JDK1.五、1.6中获得了进一步的改进。CMS是对于响应时间的重要性需求大于吞吐量要求的收集器。对于要求服务器响应速度高的状况下,使用CMS很是合适。当CMS进行GC失败时,会自动使用Serial Old策略进行GC。开启CMS回收器,新生代会默认使用ParNew GC回收器。
CMS的一大特色,就是用两次短暂的暂停来代替串行或并行标记整理算法时候的长暂停。
使用算法:标记 - 清理
CMS的执行过程以下:
a)初始标记(STW initial mark)
在这个阶段,须要虚拟机停顿正在执行的应用线程,官方的叫法STW(Stop Tow World)。这个过程从GC Roots扫描直接关联的对象,并做标记。这个过程会很快的完成。
b)并发标记(Concurrent marking)
这个阶段紧随初始标记阶段,在“初始标记”的基础上继续向下追溯标记。注意这里是并发标记,表示用户线程能够和GC线程一块儿并发执行,这个阶段不会暂停用户的线程。
c)并发预清理(Concurrent precleaning)
这个阶段仍然是并发的,JVM查找正在执行“并发标记”阶段时候进入老年代的对象(可能这时会有对象重新生代晋升到老年代,或被分配到老年代)。经过从新扫描,减小在一个阶段“从新标记”的工做,由于下一阶段会STW。
d)从新标记(STW remark)
这个阶段会再次暂停正在执行的应用线程,从新重根对象开始查找并标记并发阶段遗漏的对象(在并发标记阶段结束后对象状态的更新致使),并处理对象关联。这一次耗时会比“初始标记”更长,而且这个阶段能够并行标记。
e)并发清理(Concurrent sweeping)
这个阶段是并发的,应用线程和GC清除线程能够一块儿并发执行。
f)并发重置(Concurrent reset)
这个阶段仍然是并发的,重置CMS收集器的数据结构,等待下一次垃圾回收。
CMS的缺点:
一、内存碎片。因为使用了 标记-清理 算法,致使内存空间中会产生内存碎片。不过CMS收集器作了一些小的优化,就是把未分配的空间汇总成一个列表,当有JVM须要分配内存空间的时候,会搜索这个列表找到符合条件的空间来存储这个对象。可是内存碎片的问题依然存在,若是一个对象须要3块连续的空间来存储,由于内存碎片的缘由,寻找不到这样的空间,就会致使Full GC。
二、须要更多的CPU资源。因为使用了并发处理,不少状况下都是GC线程和应用线程并发执行的,这样就须要占用更多的CPU资源,也是牺牲了必定吞吐量的缘由。
三、须要更大的堆空间。由于CMS标记阶段应用程序的线程仍是执行的,那么就会有堆空间继续分配的问题,为了保障CMS在回收堆空间以前还有空间分配给新加入的对象,必须预留一部分空间。CMS默认在老年代空间使用68%时候启动垃圾回收。能够经过-XX:CMSinitiatingOccupancyFraction=n来设置这个阀值。
参数控制:
-XX:+UseConcMarkSweepGC 使用CMS收集器
-XX:+ UseCMSCompactAtFullCollection Full GC后,进行一次碎片整理;整理过程是独占的,会引发停顿时间变长
-XX:+CMSFullGCsBeforeCompaction 设置进行几回Full GC后,进行一次碎片整理
-XX:ParallelCMSThreads 设定CMS的线程数量(通常状况约等于可用CPU数量)
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的集合。
G1的新生代收集跟ParNew相似,当新生代占用达到必定比例的时候,开始出发收集。和CMS相似,G1收集器收集老年代对象会有短暂停顿。
收集步骤:
一、标记阶段,首先初始标记(Initial-Mark),这个阶段是停顿的(Stop the World Event),而且会触发一次普通Mintor GC。对应GC log:GC pause (young) (inital-mark)
二、Root Region Scanning,程序运行过程当中会回收survivor区(存活到老年代),这一过程必须在young GC以前完成。
三、Concurrent Marking,在整个堆中进行并发标记(和应用程序并发执行),此过程可能被young GC中断。在并发标记阶段,若发现区域对象中的全部对象都是垃圾,那个这个区域会被当即回收(图中打X)。同时,并发标记过程当中,会计算每一个区域的对象活性(区域中存活对象的比例)。
四、Remark, 再标记,会有短暂停顿(STW)。再标记阶段是用来收集 并发标记阶段 产生新的垃圾(并发阶段和应用程序一同运行);G1中采用了比CMS更快的初始快照算法:snapshot-at-the-beginning (SATB)。
五、Copy/Clean up,多线程清除失活对象,会有STW。G1将回收区域的存活对象拷贝到新区域,清除Remember Sets,并发清空回收区域并把它返回到空闲区域链表中。
六、复制/清除过程后。回收区域的活性对象已经被集中回收到深蓝色和深绿色区域。
一、jvm优化—— 图解垃圾回收 https://my.oschina.net/u/1859679/blog/1548866
二、GC算法 垃圾收集器 https://www.cnblogs.com/ityouknow/p/5614961.html
三、经过 jstack 与 jmap 分析一次线上故障 http://www.importnew.com/28916.html
四、性能调优-------(三)1分钟带你入门JVM性能调优 https://blog.csdn.net/wolf_love666/article/details/79787735
五、JVM相关 https://blog.csdn.net/wolf_love666/article/details/85712922
六、JVM命令大全 https://www.cnblogs.com/ityouknow/p/5714703.html
七、JVM调优之---一次GC调优实战 http://www.cnblogs.com/onmyway20xx/p/6626567.html
八、从实际案例聊聊Java应用的GC优化 https://tech.meituan.com/2017/12/29/jvm-optimize.html