性能调优内存分析有哪些

80 阅读8分钟

一、GC频繁****

圾回收(Garbage Collection, GC) 是Java等一些编程语言中自动管理内存的一种机制。当程序运行过程中创建的对象不再被使用时,垃圾回收器会自动释放这些对象占用的内存空间。然而,如果GC过于频繁,它可能会对应用程序的性能产生负面影响,因为它会暂停应用程序的执行以清理内存。

原因分析

内存泄漏: 对象本应该被回收但却因为仍然有引用而无法被GC回收。

对象创建过多: 短时间内创建了大量短期存活的对象。

堆内存设置不当: JVM堆内存太小,导致GC频繁发生来清理空间。

选择错误的GC算法: 不同的GC算法适用于不同类型的应用场景,选择了不适合当前应用特性的GC算法也可能导致GC频繁。

大对象分配: 大对象直接进入老年代,可能导致老年代空间快速占满。

长时间运行的任务: 某些任务可能持有大量的活跃数据,阻止了年轻代的正常晋升到老年代的过程。

性能调优建议

检查并修复内存泄漏: 使用工具如VisualVM、JProfiler或Eclipse MAT来识别内存泄漏点,并修改代码消除泄漏。

减少临时对象的创建: 重用对象,使用对象池,或者尽量减少不必要的对象创建。

调整JVM参数:

增加堆大小: 适当增加-Xms(初始堆大小)和-Xmx(最大堆大小)参数值。

调整新生代与老年代比例: 根据实际情况调整-XX:NewRatio参数。

选择合适的GC收集器: 比如对于低延迟要求的应用可以考虑使用G1或ZGC收集器。

优化代码逻辑: 避免一次性加载大量数据至内存;对于大数据处理,可以考虑流式处理方式。

定期进行性能监控: 通过持续监控来了解GC行为的变化,及时发现问题并调整配置。

GC模式设置为**-XX:+UseParallelGC-XX:+UseParallel0ldGC**,堆内存设置为**-Xmx512m-Xms512m**。在ParallelGC+ParalleloldGC模式下,当在老年代分配对象时(可能是直接在老年代分配的对象,也可能是从年轻代晋升上来的对象),剩余的空闲内存不足以分配该对象,就会触发FuIlGC。

原本默认对象是在Eden区分配的,只有当设置了

-XX:PretenureSizeThreshold参数,且对象大小超出这个限制时,该对象会在老年代直接分配。这里单个Sunvivor区的默认大小只有年轻代的十分之一,而年轻代的默认大小占整个堆区的三分之一,那么Survivor区大概只有17M左右。

在这个案例中并没有设置PretenureSizeThreshold,所以大对象还是在Eden区分配的。此时考虑这样一种情况:如果频繁地创建一些大对象,那么在Young GC的时候Eden区和s0区加起来的存活对象所占空间很可能超出s1区的大小,那么放不下的对象也会被直接晋升到老年代。

这样,即使这些对象可能很快就会成为垃圾对象,但由于它们已经被晋升到老年代了,也就只能等待Full GC的时候才能释放。所以在一个小堆中频繁创建大对象,这些大对象即使生命周期很短,也很有可能被晋升到老年代,从而引发FulGC。

此时有两个优化方向:

一是在有足够物理内存资源的情况下,扩大堆内存,降低GC频率,减少因上述原因将对象过早晋升至老年代的概率。

二是分析出到底是哪些大对象在被频繁地创建,优化代码减少大对象创建频率,或者对大对象进行轻量化改造。

二、内存Dump分析HttpClient连接池耗尽问题

PoolingNHttpClientConnectionManager中的pool属性就是用来维护HTTP连接池的。注意其中有两个关键属性maxTotal和defaultMaxPerRoute。

maxTotal表示整个HTTP连接池的大小,defaultMaxPerRoute表示本服务节点到外部服务节点间的子连接池大小。这里maxTotal设置为20,defaultMaxPerRoute设置为2,也就是说本服务节点对外部服务(可能有多个)进行请求时,最多能同时建立20个连接,而对单个下游节点则最多能同时建立2个连接。

这就是为什么可运行状态的线程只有2个,其他正在工作的线程都在等待连接的缘故。其优化方案就是将maxTotal和defaultMaxPerRoute调整为更合理的值,让硬件资源能被充分利用。

三、OOM

当Java应用程序遇到OutOfMemoryError (OOM) 时,通常意味着JVM已经耗尽了所有可用的内存,并且无法再为新对象或数组分配空间。这时,获取和分析堆转储(Heap Dump)是非常有用的,它可以帮助开发者理解内存使用情况,并找出导致OOM的根本原因。

以下是处理OOM异常并利用堆转储文件进行性能调优的一般步骤

步骤 1: 获取堆转储文件****

首先,需要配置JVM在发生OOM时自动产生堆转储文件。这可以通过设置JVM启动参数来实现:

-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=/path/to/directory

其中-XX:HeapDumpPath指定了堆转储文件保存的位置。

步骤 2: 使用工具分析堆转储****

有了堆转储文件后,可以使用多种工具来进行分析,常用的工具有:

Eclipse Memory Analyzer (MAT): 是一个强大的免费工具,可以用来查找内存泄漏和分析内存使用情况。

VisualVM: 提供了图形界面,能够连接正在运行的JVM并查看其状态,同时也支持离线分析堆转储文件。

jhat: JDK自带的一个命令行工具,可以用来浏览堆转储文件的内容,不过它的功能相对有限。

分析堆转储的关键点包括:

识别大对象:查找占用内存最大的对象。

查找内存泄漏:定位那些不应该存活但却被引用链保持住的对象。

分析引用链:了解对象是如何被引用的,从而找到可以断开的引用以释放内存。

步骤 3: 根据分析结果采取行动****

根据堆转储分析的结果,可以采取不同的措施来解决问题:

修正代码中的内存泄漏:比如关闭未使用的资源、移除不再需要的监听器等。

优化数据结构:减少不必要的数据复制,使用更高效的数据存储方式。

调整JVM参数:适当增加堆大小,调整新生代与老年代的比例等。

改进算法:优化业务逻辑,减少内存消耗大的操作。

步骤 4: 验证解决方案****

实施任何变更之后,应该在测试环境下重现问题,并验证新的配置或代码改动是否解决了OOM问题。同时也要注意不要引入新的性能瓶颈或其他问题。

分析导致OOM异常的Dump时,可以从如下几个角度切入。

有无内存占比很高的大对象。这种情况下,对象数量可能不多,但是单个对象占用内存比较大。

如果没有明显的大对象,就需要观察有没有一些实例的累计占用内存较大,每个实例占用内存不一定很大,也可能是实例数非常多。除了实例外,也需要关注类的静态属性有无占用内存过大的现象。

如果占用内存大的是个Thread对象,那就需要找到对应的线程,从线程栈中排查是否某个方法持有一些很大的局部变量。

至此,我们定位到了一个ArrayList对象,它又持有269个PerfType_Large0bject对象,这是导致OOM异常的”罪魁祸首”。而持有该ArrayList对象的是个名为Thread-30的线程。上文提到通过new Thread()创建的子线程的命名就是以Thread-开头的,也就是说这是在某个框架组件或业务代码中创建的独立子线程,非池内线程。再结合栈内的Package Name和方法名就能判断出这是一个业务代码创建的子线程,正在执行的业务方法是Perf_OOM_Heap.lambdaSrunS1。注意,其中Perf_OOM_Heap是类名,run是方法名,之所以显示成lambdaSrun$1是因为创建子线程时使用了Lambda表达式。