目录标题
前言
什么情况下需要考虑调优
Heap内存(老年代)持续上涨达到设置的最大内存值;
Full GC 次数频繁;
GC 停顿时间过长(超过1秒);
应用出现OutOfMemory 等内存异常;
应用中有使用本地缓存且占用大量内存空间;
系统吞吐量与响应性能不高或下降。
JVM调优是一个手段,但并不一定所有问题都可以通过JVM进行调优解决,因此,在进行JVM调优时,我们要遵循一些原则:
大多数的Java应用不需要进行JVM优化;
大多数导致GC问题的原因是代码层面的问题导致的(代码层面);
上线之前,应先考虑将机器的JVM参数设置到最优;
减少创建对象的数量(代码层面);
减少使用全局变量和大对象(代码层面);
优先架构调优和代码调优,JVM优化是不得已的手段(代码、架构层面);
分析GC情况优化代码比优化JVM参数更好(代码层面);
通过以上原则,我们发现,其实最有效的优化手段是架构和代码层面的优化,而JVM优化则是最后不得已的手段,也可以说是对服务器配置的最后一次“压榨”。
调优原则总结
JVM的自动内存管理本来就是为了将开发人员从内存管理的泥潭里拉出来。JVM调优不是常规手段,性能问题一般第一选择是优化程序,最后的选择才是进行JVM调优。
1. 调优
1.1 调优指标
jvm调优涉及到两个很重要的概念:吞吐量和响应时间。jvm调优主要是针对他们进行调整优化,达到一个理想的目标,根据业务确定目标是吞吐量优先还是响应时间优先。
低延迟(相应时间短):整个接口的响应时间(用户代码执行时间+GC执行时间),stw时间越短,响应时间越短。
高吞吐量:用户代码执行时间/(用户代码执行时间+GC执行时间)。
1.2 调优依赖(堆栈错误信息,快照文件)
调优可以依赖、参考的数据有系统运行日志、堆栈错误信息、gc日志、线程快照、堆转储快照等。
堆栈错误信息:当系统出现异常后,可以根据堆栈信息初步定位问题所在,比如根据“java.lang.OutOfMemoryError: Java heap space”可以判断是堆内存溢出;根据“java.lang.StackOverflowError”可以判断是栈溢出;根据“java.lang.OutOfMemoryError: PermGen space”可以判断是方法区溢出等。
堆转储快照:程序启动时可以使用 “-XX:+HeapDumpOnOutOfMemory” 和 “-XX:HeapDumpPath=/data/jvm/dumpfile.hprof”,当程序发生内存溢出时,把当时的内存快照以文件形式进行转储(也可以直接用jmap命令转储程序运行时任意时刻的内存快照),事后对当时的内存使用情况进行分析。
1.3 调优步骤
调优的前提是熟悉业务场景,先判断出当前业务场景是吞吐量优先还是响应时间优先。调优需要建立在监控之上,由压力测试来判断是否达到业务要求和性能要求。
调优的步骤大致可以分为:
- 熟悉业务场景,了解当前业务系统的要求,是吞吐量优先还是响应时间优先;
- 选择合适的垃圾回收器组合。如果是吞吐量优先,则选择ps+po(Parallel Scavenge + ParallelOld)组合;如果是响应时间优先,在1.8以后选择G1,在1.8之前选择ParNew+CMS组合;
- 规划内存需求,只能进行大致的规划。
- 根据实际情况设置升级年龄,最大年龄为15;
- CPU选择,在预算之内性能越高越好;
- 设定日志参数:-Xloggc:/path/name-gc-%t.log -XX:+UseGCLogFileRotation -XX:NumberOfGCLogs=5 -XX:GCLogFileSize=20M -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCCauses
-XX:+UseGCLogFileRotation:GC文件循环使用
-XX:NumberOfGCLogs=5:使用5个GC文件
-XX:GCLogFileSize=20M:每个GC文件的大小
上面这三个参数放在一起代表的含义是:5个GC文件循环使用,每个GC文件20M,总共使用100M存储日志文件,当5个GC文件都使用完毕以后,覆盖第一个GC日志文件,生成新的GC文件。
选择合适的垃圾回收器
一方面是根据业务选择(低延迟还是高吞吐量),一方面也要考虑CPU的实际情况。
CPU单核,那么毫无疑问Serial 垃圾收集器是你唯一的选择。
CPU多核,关注吞吐量 ,那么选择PS+PO组合。
CPU多核,关注用户停顿时间,JDK版本1.6或者1.7,那么选择CMS。
CPU多核,关注用户停顿时间,JDK1.8及以上,JVM可用内存6G以上,那么选择G1。
//设置Serial垃圾收集器(新生代)
开启:-XX:+UseSerialGC
//设置PS+PO,新生代使用功能Parallel Scavenge 老年代将会使用Parallel Old收集器
开启 -XX:+UseParallelOldGC
//CMS垃圾收集器(老年代)
开启 -XX:+UseConcMarkSweepGC
//设置G1垃圾收集器
开启 -XX:+UseG1GC
合理规划内存大小
现象:垃圾收集频率非常频繁。
原因:如果内存太小,就会导致频繁的需要进行垃圾收集才能释放出足够的空间来创建新的对象,所以增加堆内存大小的效果是非常显而易见的。
注意:如果垃圾收集次数非常频繁,但是每次能回收的对象非常少,那么这个时候并非内存太小,而可能是内存泄露导致对象无法回收,从而造成频繁GC。
//设置堆初始值
指令1:-Xms2g
指令2:-XX:InitialHeapSize=2048m
//设置堆区最大值
指令1:`-Xmx2g`
指令2: -XX:MaxHeapSize=2048m
//新生代内存配置
指令1:-Xmn512m
指令2:-XX:MaxNewSize=512m
调整内存区域大小比率
现象:某一个区域的GC频繁,其他都正常。
原因:如果对应区域空间不足,导致需要频繁GC来释放空间,在JVM堆内存无法增加的情况下,可以调整对应区域的大小比率。
注意:也许并非空间不足,而是因为内存泄造成内存无法回收。从而导致GC频繁。
//survivor区和Eden区大小比率
指令:-XX:SurvivorRatio=6 //S区和Eden区占新生代比率为1:6,两个S区2:6
//新生代和老年代的占比
-XX:NewRatio=4 //表示新生代:老年代 = 1:4 即老年代占整个堆的4/5;默认值=2
避免内存泄漏
什么是Java中的内存泄露
在Java中,内存泄漏就是存在一些被分配的对象,这些对象有下面两个特点,首先,这些对象是可达的,即在有向图中,存在通路可以与其相连;其次,这些对象是无用的,即程序以后不会再使用这些对象。如果对象满足这两个条件,这些对象就可以判定为Java中的内存泄漏,这些对象不会被GC所回收,然而它却占用内存。
如何避免内存泄漏
-
及时释放对象。在不再使用对象时,要及时将其引用设置为 null,以便垃圾回收机制能够正确地释放该对象。
-
避免过多的对象创建。过多的对象创建会导致内存占用过多,从而可能导致内存泄漏。因此,可以采用对象池的方式来避免过多的对象创建。对象池是一种重复使用对象的方法,它可以有效地减少对象的创建和销毁,从而降低内存占用。
-
避免对象循环引用。如果对象循环引用,容易导致无法释放内存;
-
谨慎使用静态变量。静态变量生命周期与应用程序一致,容易内存泄漏;
-
可以使用软引用,弱引用。在一些需要缓存的数据对象中,可以使用弱引用,当内存不足时,垃圾回收器可以回收这些对象,避免内存泄漏;
-
及时关闭资源。在使用数据库连接,文件流等资源时,要及时关闭资源,避免长时间占用资源导致内存泄漏;
-
使用连接池。在使用数据库连接的时候,尽量使用连接池,连接池可以管理连接的创建和销毁,避免连接泄漏;
1.4 jvm调优常用参数
通用GC参数
-Xmn:年轻代大小 -Xms:堆初始大小 -Xmx:堆最大大小 -Xss:栈大小
G1常用参数
-XX:+UseG1 使用G1垃圾回收器
-XX:MaxGCPauseMills GCt停顿时间,该参数也是尽量达到,G1会调整yong区的块数来达到这个值
-XX:+G1HeapRegionSize 分区大小,范围为1M~32M,必须是2的n次幂,size越大,GC回收间隔越大,但是GC所用时间越长
G1NewSizePercent 新生代所占最小比例,默认5%
G1MaxNewSizePercent 新生代所占最大比例,默认60%
GCTimeRatio GC时间比例,此值为建议值,G1会调整堆大小来尽量达到这个值
ConcGCThreads GC线程数量
InitiatingHeapOccupancyPercent 启动G1的堆空间占用比例
2. 内存飙高
内存飙高一般都是堆中对象无法回收造成,因为java中的对象大部分存储在堆内存中。其实也就是常见的oom问题(Out Of Memory)。
OOM解决思路
3. CPU飙升
4. 具体调优
4.1 新生代调优
新生代特点
大部分对象存活时间较短
MinorGC时间远小于FullGC
调优参考
- 合理设置新生代内存大小
占用整个堆内存1/4-1/2建议这个范围即可。如果新生代内存过大,老年代内存就会小,频繁FullGC;
可以参考:新生代大小 =【并发量 *(一次请求创建的对象占用内存大小)】 - 合理设置幸存区大小以及晋升阈值
2.1 幸存区大小 = 能保留【当前活跃对象(存活时间较短的对象)+一定可以晋升到老年代的对象】
如果幸存区空间不够,JVM会动态调整晋升规则,一部分对象(当前活跃对象)本来不应该进入老年代,但是由于幸存区较小,会提前这部分对象晋升到老年代,这样只有发生FullGC才能被回收。尽量保证当前活跃对象在在新生代直接GC就处理掉,而不是进入老年代等FullGC。
2.2 晋升阈值配置得当,让长时间存活的对象尽快晋升到老年代。
4.2 老年代调优(CMS为例)
老年代内存越大越好。
CMS垃圾回收的时候,用户线程也在运行,这时候会产生浮动垃圾,如果浮动垃圾导致GC,CMS并发失败,CMS退化为SerialOld串行回收垃圾,直接STW。
4.3 内存泄漏–检测工具valgrind
如果垃圾收集次数非常频繁,但是每次能回收的对象非常少,那么这个时候并非内存太小,而可能是内存泄露导致对象无法回收,从而造成频繁GC。
在Linux上比较常用的内存泄漏检测工具是valgrind。
valgrind的检测信息将内存泄漏分为如下几类:
definitely lost:确定产生内存泄漏
indirectly lost:间接产生内存泄漏
possibly lost:可能存在内存泄漏
still reachable:即使在程序结束时候,仍然有指针在指向该块内存,常见于全局变量
==9652== HEAP SUMMARY:
==9652== in use at exit: 10 bytes in 1 blocks
==9652== total heap usage: 1 allocs, 0 frees, 10 bytes allocated
==9652==
==9652== 10 bytes in 1 blocks are definitely lost in loss record 1 of 1
==9652== at 0x4C29F73: malloc (vg_replace_malloc.c:309)
==9652== by 0x40052E: func (leak.c:4)
==9652== by 0x40053D: main (leak.c:8)
提示在main函数(leak.c的第8行)fun函数(leak.c的第四行)产生了内存泄漏,通过分析代码,原因定位,问题解决。
4.4 后台导出数据引发的OOM
问题描述:公司的后台系统,偶发性的引发OOM异常,堆内存溢出。
1、因为是偶发性的,所以第一次简单的认为就是堆内存不足导致,所以单方面的加大了堆内存从4G调整到8G。
2、但是问题依然没有解决,只能从堆内存信息下手,通过开启了-XX:+HeapDumpOnOutOfMemoryError参数 获得堆内存的dump文件。
3、VisualVM 对 堆dump文件进行分析,通过VisualVM查看到占用内存最大的对象是String对象,本来想跟踪着String对象找到其引用的地方,但dump文件太大,跟踪进去的时候总是卡死,而String对象占用比较多也比较正常,最开始也没有认定就是这里的问题,于是就从线程信息里面找突破点。
4、通过线程进行分析,先找到了几个正在运行的业务线程,然后逐一跟进业务线程看了下代码,发现有个引起我注意的方法,导出订单信息。
5、因为订单信息导出这个方法可能会有几万的数据量,首先要从数据库里面查询出来订单信息,然后把订单信息生成excel,这个过程会产生大量的String对象。
6、为了验证自己的猜想,于是准备登录后台去测试下,结果在测试的过程中发现到处订单的按钮前端居然没有做点击后按钮置灰交互事件,结果按钮可以一直点,因为导出订单数据本来就非常慢,使用的人员可能发现点击后很久后页面都没反应,结果就一直点,结果就大量的请求进入到后台,堆内存产生了大量的订单对象和EXCEL对象,而且方法执行非常慢,导致这一段时间内这些对象都无法被回收,所以最终导致内存溢出。
7、知道了问题就容易解决了,最终没有调整任何JVM参数,只是在前端的导出订单按钮上加上了置灰状态,等后端响应之后按钮才可以进行点击,然后减少了查询订单信息的非必要字段来减少生成对象的体积,然后问题就解决了。
https://blog.csdn.net/weixin_38612401/article/details/123848025
前言:https://baijiahao.baidu.com/s?id=1747377064336158073&wfr=spider&for=pc
具体调优:https://www.bilibili.com/video/BV1yE411Z7AP?p=94&vd_source=b901ef0e9ed712b24882863596eab0ca
后台导出数据引发的OOM:https://mp.weixin.qq.com/s/nSwNZpObWLGteG-v7n5PSw
内存泄漏:https://blog.csdn.net/namelij/article/details/122839293