因此这阵子除了优化Memcache客户端和SIP框架逻辑以外其他依赖部分以外,对于内存泄露的压力测试也开始实实在在的做起来。经过这次问题的定位和解决以后,大致觉得对于一个大用户量应用要放心的话,那么需要做这么几步。
1. 在GC输出的环境下,大压力下做多天的测试。(可以在 JAVA_OPTS增加-verbose:gc -XX:+PrintGC Details(这里要把GC和Details中的空格去掉,由于不知道为什么CSDN认为这个是非法字符,所以不得不拆开#_#!) -XX:+HeapDumpOnOutOfMemoryError)
2. 检查GC输出日志来判断是否有内存泄露。(这部分后面有详细的实例说明)
3. 如果出现内存泄露问题,则使用jprofiler等工具来排查内存泄露点(之所以不一开始使用,因为jprofiler等工具对于压力测试有影响,使得大压力无法上去,也使问题不那么容易暴露)
4. 解决问题,并在重复2步骤。
这里对SIP在jdk1.5和jdk1.6下做压力测试的GC 日志来做一个实际的分析对比,通过对比来大致描述一下如何根据输出情况能够了解应用是否存在内存泄露问题。(这里的内存泄露问题就是在以前blog写过的jdk的concurrent包内LinkedBlockingQueue的poll方法存在比较严重的内存泄露,调用频率越高,内存泄露的越厉害)
两次压力测试都差不多都是两天,测试方案如下:
开始50个并发,每个并发每次请求完毕后休息0.1秒,10分钟后增长50个并发,按此规律增长到500并发。
旧版本SIP是在JDK1.5环境下完成的压力测试,
新版本SIP的JDK版本是1.6,
压力机和以前一样,是10.2.226.40,DELL1950,8CPU,8G内存。
压力机模拟发出对一个需要签名的API不断的调用请求。
看看两个Log的具体内容(内容很多截取部分做分析)
先说一下日志输出的结构:(1.6和1.5略微有一些不同,只是1.6对于时间统计更加细致)
[GC [<collector>: <starting occupancy1> -> <ending occupancy1>, <pause time1> secs] <starting occupancy3> -> <ending occupancy3>, <pause time3> secs]
<collector>GC收集器的名称
<starting occupancy1> 新生代在GC前占用的内存
<ending occupancy1> 新生代在GC后占用的内存
<pause time1> 新生代局部收集时jvm暂停处理的时间
<starting occupancy3> JVM Heap 在GC前占用的内存
<ending occupancy3> JVM Heap 在GC后占用的内存
<pause time3> GC过程中jvm暂停处理的总时间
Jdk1.5 log:
启动时GC输出:
[GC [DefNew: 209792K->4417K(235968K), 0.0201630 secs] 246722K->41347K(498112K), 0.0204050 secs]
[GC [DefNew: 214209K->4381K(235968K), 0.0139200 secs] 251139K->41312K(498112K), 0.0141190 secs]
一句输出:
新生代回收前209792K,回收后4417K,回收数量205375K,当前新生代可用235968K,Heap总量回收前246722K回收后41347K,回收总量205375K。这就表示100%的收回,没有任何新生代的对象被提升到中生代或者永久区(名字说的不一定准确,只是表达意思)。
第二句输出:
按照分析也就只是有1K内容被提升到中生代。
运行一段时间后:
[GC [DefNew: 210686K->979K(235968K), 0.0257140 secs] 278070K->68379K(498244K), 0.0261820 secs]
[GC [DefNew: 210771K->1129K(235968K), 0.0275160 secs] 278171K->68544K(498244K), 0.0280050 secs]
第一句输出:
新生代回收前210686K,回收后979K,回收数量209707K,Heap总量回收前278070K回收后68379K,回收总量209691K。这就表示有16k没有被回收。
第二句输出:
新生代回收前210771K,回收后1129K,回收数量209642K,Heap总量回收前278171K回收后68544K,回收总量209627K。这就表示有15k没有被回收。
比较一下启动时与现在的新生代占用内存情况和Heap使用情况发现Heap的使用增长很明显,新生代没有增长,而Heap使用总量增长了27M,这就表明可能存在内存泄露,虽然每一次泄露的字节数很少,但是频率很高,大部分泄露的对象都被升级到了中生代或者持久代。
又一段时间后:
[GC [DefNew: 211554K->1913K(235968K), 0.0461130 secs] 350102K->140481K(648160K), 0.0469790 secs]
[GC [DefNew: 211707K->2327K(235968K), 0.0546170 secs] 350275K->140921K(648160K), 0.0555070 secs]
第一句输出:
新生代回收前211554K,回收后1913K,回收数量209641K,Heap总量回收前350102K回收后140481K,回收总量209621K。这就表示有20k没有被回收。
分析到这里就可以看出每一次泄露的内存只有10几K,但是在大压力长时间的测试下,内存泄露还是很明显的,此时Heap已经增长到了140M,较启动时已经增长了100M。同时GC占用的时间越来越长。
后续的现象:
后续观察日志会发现,Full GC的频率越来越高,收集所花费时间也是越来越长。(Full GC定期会执行,同时局部回收不能满足分配需求的情况下也会执行)。
[Full GC [Tenured: 786431K->786431K(786432K), 3.4882390 secs] 1022399K->1022399K(1022400K), [Perm : 36711K->36711K(98304K)], 3.4887920 secs]
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid7720.hprof ...
出现这个语句表示内存真的被消耗完了。
Jdk1.6 log:
启动时GC的输出:
[GC [PSYoungGen: 221697K->31960K(229376K)] 225788K->36051K(491520K), 0.0521830 secs] [Times: user=0.26 sys=0.05, real=0.05 secs]
[GC [PSYoungGen: 228568K->32752K(229376K)] 232659K->37036K(491520K), 0.0408620 secs] [Times: user=0.21 sys=0.02, real=0.04 secs]
第一句输出:
新生代回收前221697K,回收后31960K,回收数量189737K,Heap总量回收前225788K回收后36051K,回收总量189737K。100%被回收。
运行一段时间后输出:
[GC [PSYoungGen: 258944K->2536K(259328K)] 853863K->598135K(997888K), 0.0471620 secs] [Times: user=0.15 sys=0.00, real=0.05 secs]
[GC [PSYoungGen: 259048K->2624K(259328K)] 854647K->598907K(997888K), 0.0462980 secs] [Times: user=0.16 sys=0.02, real=0.04 secs]
第一句输出:
新生代回收前258944K,回收后2536K,回收数量256408K,Heap总量回收前853863K回收后598135K,回收总量255728K。680K没有被回收,但这并不意味着就会产生内存泄露。同时可以看出GC回收时间并没有增加。
在运行一段时间后输出:
[GC [PSYoungGen: 258904K->2488K(259264K)] 969663K->713923K(1045696K), 0.0485140 secs] [Times: user=0.16 sys=0.01, real=0.04 secs]
[GC [PSYoungGen: 258872K->2448K(259328K)] 970307K->714563K(1045760K), 0.0473770 secs] [Times: user=0.16 sys=0.01, real=0.05 secs]
第一句输出:
新生代回收前258904K,回收后2488K,回收数量256416K,Heap总量回收前969663K回收后713923K,回收总量255740K。676K没有被回收,同时总的Heap也有所增加。
此时看起来好像和1.5的状况一样。但是查看了一下Full GC的执行还是400-500次GC执行一次,因此继续观察。
运行一天多以后输出:
[GC [PSYoungGen: 257016K->3304K(257984K)] 1019358K->766310K(1044416K), 0.0567120 secs] [Times: user=0.18 sys=0.01, real=0.06 secs]
[GC [PSYoungGen: 257128K->2920K(258112K)] 1020134K->766622K(1044544K), 0.0549570 secs] [Times: user=0.19 sys=0.00, real=0.05 secs]
可以发现Heap增长趋缓。
运行两天以后输出:
[GC [PSYoungGen: 256936K->3584K(257792K)] 859561K->606969K(1044224K), 0.0565910 secs] [Times: user=0.18 sys=0.01, real=0.06 secs]
[GC [PSYoungGen: 256960K->3368K(257728K)] 860345K->607445K(1044160K), 0.0553780 secs] [Times: user=0.18 sys=0.01, real=0.06 secs]
发现Heap反而减少了,此时可以对内存泄露问题作初步排除了。(其实在jdk1.6环境下用jprofiler来观察,对于concurrent那个内存泄露点的跟踪发现,内存的确还是会不断增长的,不过在一段时间后还是有回收,因此也就可以部分解释前面出现的情况)
jdk采用1.5的压力测试结果压了一个周末回来就无法响应了,看了看它的GC输出:
全都是[Full GC [Tenured: 786431K->786431K(786432K), 3.4802480 secs] 1022399K->1022399K(1022400K), [Perm : 36711K->36711K(98304K)], 3.4808510 secs]
替换jdk1.5为jdk1.6昨天中午开始做压力测试到今天中午我取了一下日志,看见GC输出如下:
[GC [PSYoungGen: 256608K->3608K(257792K)] 859002K->606698K(1044224K), 0.0562040 secs] [Times: user=0.17 sys=0.01, real=0.05 secs]
这就很明显的看出了差别了,前者已经把家底全部都用完了,出于out of memory的状态了。后者还是普通的GC回收,回收效果很好,同时看了看jboss启动的时候回收后占用的YoungGen就1300K左右,因此跑了一天的压力测试应该说还算是正常。
Full GC也就是在年轻代无法满足内存分配要求的时候才去做,这时候其实对于中老两代来说应该已经可能被占用完毕了,Full GC发生频率也越来越高,服务器响应速度也越来越慢。
因此对于持久的压力测试来说,打印出GC可以很好的分析出应用本身的内存使用状况,避免长期运行中少量内存泄露最终导致的应用不可用。
顺便说一下memcache 客户端修改后的测试结果:
测试方案:
开始50个并发,每个并发每次请求完毕后休息0.1秒,10分钟后增长50个并发,按此规律增长到500并发。
新旧版本SIP是在JDK1.5环境下完成的压力测试,
压力机和以前一样,是10.2.226.40,DELL1950,8CPU,8G内存。
压力机模拟发出对一个需要签名的API不断的调用请求。
测试结论:
老版本的SIP性能和以前一样,并发500的时候,TPS接近480,CPU 68%
新版本的SIP性能比老版本好很多,并发500的时候,TPS接近770,且系统压力很小,CPU利用率平均只有16%
总结:
对于GC输出的观察需要分两个维度来看。一个是纵向比较,也就是一次回收对于内存变化的观察。一个是横向比较,对于长时间内存分配占用情况的比较,这部分比较需要较长时间的观察,不能仅仅凭短时间的几个抽样比较,因为对于抽样来说,Full GC前后的区别,运行时长的区别,资源瞬时占用的区别都会影响判断。同时要结合Full GC发生的时间周期,每一次GC收集所耗费的时间作为辅助判断标准。
顺便说一下,Heap的 YoungGen,OldGen,PermGen的设置也是需要注意的,并不是越大越好,越大执行收集的时间越久,但是可能执行Full GC的频率会比较低,因此需要权衡。这些仔细的去了解一下GC的基础设计思想会更有帮助,不过一般用默认的也不错。还有就是可以配置一些特殊的GC,并行,同步等等,充分利用多CPU的资源。
对于GC的优化可以通过现在很多图形工具来做,也可以类似于我这样采用最原始的分析方式,好处就是任何时间任何地点只要知道原理就可以分析无需借助外部工具。原始的总是最好的^_^。
相关推荐
"jvmgc日志分析工具"专为解析和可视化JVM生成的GC日志而设计,帮助开发者识别内存瓶颈,调整内存设置,以及诊断可能的性能问题。 GC日志是JVM在运行过程中记录的关于垃圾收集活动的详细信息,包括垃圾收集的起始...
因此,能够理解并分析GC日志对于诊断性能问题和调整内存管理策略至关重要。 **GChisto的特性与功能** 1. **用户友好界面**: GChisto提供了一个直观的图形用户界面,使得GC日志的解析和分析变得简单。用户只需将GC...
2. **内存泄漏检测**:如果GC日志显示内存使用持续增加,而无法被正常回收,可能存在内存泄漏问题,需要进一步调查。 3. **调整GC参数**:了解GC行为有助于调整JVM的垃圾收集器设置,以实现更高效的内存管理和应用...
gchisto是一款强大的GC日志分析工具,它能够帮助我们深入解析和可视化GC日志,从而更好地诊断和优化Java应用的内存使用情况。 gchisto的主要功能在于分析Java应用程序产生的GC日志,这些日志通常包含了大量的详细...
通过分析GC日志,我们可以了解JVM的内存使用情况,找出可能的内存泄漏或性能瓶颈,从而优化应用程序的内存管理。IBM提供了各种工具,如Visual GC或JConsole,帮助开发者理解和调整GC策略,以提高系统性能。 其次,...
GCViewer就是这样一款强大的工具,它专门用于可视化分析Java程序的GC日志,包括堆内存、年轻代、老年代和永久代的变化,以及Full GC的情况。 GCViewer是由Stuart Marks开发的一款开源工具,它可以解析并展示Hprof和...
以上知识点涉及了Android内存泄漏分析的多个方面,包括理论知识、工具使用、操作实践和问题诊断技巧。理解和掌握这些知识点对Android开发人员来说是非常重要的,它可以帮助他们在日常开发中有效地避免和解决内存泄漏...
3. **运行与分析**:双击RUN.bat执行文件,GCviewer将读取指定的GC日志文件,并实时展示分析结果。你可以通过界面观察GC行为,调整视图以查看不同维度的数据。 4. **导出报告**:除了实时查看,GCviewer还支持导出...
通过解读GC日志,GCViewer能够帮助开发者识别潜在的内存泄漏、性能瓶颈以及调整优化内存配置。 一、GCViewer简介 GCViewer是基于Java Swing的GUI应用,由Chris Newland开发,它能够解析并可视化JVM生成的GC日志,...
2. **垃圾收集日志**:VisualGC记录并展示每一次垃圾收集的信息,包括收集类型、耗时以及收集前后的内存状态,这对于分析垃圾收集器的工作效率和优化内存配置极其有帮助。 3. **对象统计**:插件还能提供对象的统计...
此外,通过LogCat观察到频繁的GC日志,这是垃圾回收器为了释放内存而频繁工作的体现。这通常意味着有对象被错误地持有,无法被垃圾回收。 现场勘查中提到的线索是GCRoots,它是内存中的根对象,可以引用到有用的ABC...
- **日志分析**:通过JVM的GC日志,观察对象分配和回收的情况,寻找异常模式。 - **代码审查**:检查是否存在未关闭的流、静态集合中的无用对象、长时间持有的强引用等可能引起内存泄露的代码。 - **内存快照对比...
GChisto和GCViewer是两个常用的Java GC日志分析工具,它们能够提供深入的洞察力,帮助我们识别潜在的内存泄漏问题,调整垃圾收集器设置,以及优化整体系统性能。 GChisto是一个基于Java的图形化工具,用于分析JVM的...
总结来说,GChisto是一款强大的GC日志分析工具,它通过解析和可视化JVM的GC日志,帮助开发者和运维人员找出内存管理的问题,特别是Old Generation区域的问题,从而提升Java应用的性能和稳定性。正确使用GChisto,并...
3. **GC日志分析**:`jmap -heap <pid>`可以输出JVM的堆配置信息以及垃圾收集的状态。 4. **堆内存统计**:`jmap -finalizerinfo <pid>`可以查看等待Finalizer线程处理的对象,这有时是内存泄漏的一个迹象。 当...
IBM的garbage collection (GC) 分析工具,通常用于优化WebSphere Application Server (WAS)的性能。...在分析gc日志的过程中,结合WAS的监控工具和其他性能分析工具,能进一步增强问题定位和解决能力。