美文网首页
Java性能调优

Java性能调优

作者: 取名废同学 | 来源:发表于2023-03-25 23:36 被阅读0次

JVM启动参数优化:

实则是在对原生内存的优化,含使用压缩的OOP(jvm启动参数上增加-XX:+UseCompressedOops)以及调整大内存分页(同时修改linux配置以及jvm启动参数-XX:LargePageSizeInBytes)等,都可以提升性能。

垃圾回收机制:

设置太小容易频繁GC,设置太大,GC时停顿时间太长,同时为了避免可能使用到虚拟内存,内存页交换导致更慢,至少保留1G的物理内存。

一、性能分析:

性能分析包括CPU、内存和IO

(一)CPU分析:

当程序响应变慢的时候,首先使用top、vmstat、ps等命令查看系统的cpu使用率是否有异常,从而判断是否是cpu繁忙造成的性能问题。主要通过us(用户进程所占的%)这个数据来看异常的进程信息。当us接近100%甚至更高时,可以确定是cpu繁忙造成的响应缓慢。

cpu繁忙的原因有以下几个:

(1)线程中有无限空循环、无阻塞、正则匹配或者单纯的计算

(2)发生了频繁的gc

(3)多线程的上下文切换

确定好cpu使用率最高的进程之后就可以使用jstack来打印出异常进程的堆栈信息:jstack [pid]

观察GC的次数,是否发生minor gc 和 full gc

(二)内存分析:

常见问题:

1、频繁GC -> Stop the world,使你的应用响应变慢

2、OOM,直接造成内存溢出错误使得程序退出。OOM又可以分为以下几种:

(1)Heap space:堆内存不足

(2)PermGen space:永久代内存不足

(3)Native thread:本地线程没有足够内存可分配

堆内存问题排查:

排查堆内存问题的常用工具是jmap,是jdk自带的。一些常用用法如下:

查看jvm内存使用状况:jmap -heap

查看jvm内存存活的对象:jmap -histo:live

把heap里所有对象都dump下来,无论对象是死是活:jmap -dump:format=b,file=xxx.hprof

先做一次full GC,再dump,只包含仍然存活的对象信息:jmap -dump:format=b,live,file=xxx.hprof

jstat gcccapacity pid

(三)文件IO

二、性能调优:

同样是分为CPU调优、内存调优和IO调优

(一)CPU调优:

不要存在一直运行的线程(无限while循环),可以使用sleep休眠一段时间。这种情况普遍存在于一些pull方式消费数据的场景下,当一次pull没有拿到数据的时候建议sleep一下,再做下一次pull。

轮询的时候可以使用wait/notify机制

避免循环、正则表达式匹配、计算过多,包括使用String的format、split、replace方法(可以使用apache的commons-lang里的StringUtils对应的方法),使用正则去判断邮箱格式(有时候会造成死循环)、序列/反序列化等。

结合jvm和代码,避免产生频繁的gc,尤其是full GC。

此外,使用多线程的时候,还需要注意以下几点:

使用线程池,减少线程数以及线程的切换

多线程对于锁的竞争可以考虑减小锁的粒度(使用ReetrantLock)、拆分锁(类似ConcurrentHashMap分bucket上锁), 或者使用CAS、ThreadLocal、不可变对象等无锁技术。此外,多线程代码的编写最好使用jdk提供的并发包、Executors框架以及ForkJoin等,此外Discuptor和Actor在合适的场景也可以使用。

(二)内存调优原则:主要就是JVM调优

要则:

1、合理设置各个代的大小,避免新生代设置过小(不够用而导致经常minor gc后对象就进入了老年代,易造成full gc),和避免过大(易产生碎片)

2、选择合适的GC策略。

3、jvm启动参数配置-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:[log_path],以记录gc日志,便于排查问题。

一般吞吐量优先的应用都应该有一个很大的年轻代和一个较小的年老代。这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代存放长期存活对象。

步骤:

1、堆设置:

参数-Xms和-Xmx,通常设置为相同的值,避免运行时要不断扩展JVM内存,建议扩大至3-4倍FullGC后的老年代空间占用。

2、年轻代设置:

新生代分为S0,S1和Eden区

参数-Xmn,1-1.5倍FullGC之后的老年代空间占用。

避免新生代设置过小,当新生代设置过小时,会带来两个问题:一是minor GC次数频繁,二是可能导致 minor GC对象直接进老年代。当老年代内存不足时,会触发Full GC。

避免新生代设置过大,当新生代设置过大时,会带来两个问题:一是老年代变小,可能导致Full GC频繁执行;二是 minor GC 执行回收的时间大幅度增加。

3、老年代设置:

新生代经过多次Minor GC之后依然存活的对象,就会变成老年代。

老年代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率和会话持续时间等一些参数

如果堆设置偏小,可能会造成内存碎片、高回收频率以及应用暂停

如果堆设置偏大,则需要较长的收集时间

4、永久代(Perm):主要存放已被虚拟机加载的类信息,常量,静态变量等数据。该区域对GC的影响不大。

5、方法区设置:

基于jdk1.7版本,永久代:参数-XX:PermSize和-XX:MaxPermSize;

基于jdk1.8版本,元空间:参数 -XX:MetaspaceSize和-XX:MaxMetaspaceSize;

通常设置为相同的值,避免运行时要不断扩展,建议扩大至1.2-1.5倍FullGc后的永久带空间占用。

6、代码上,也需要注意:

避免保存重复的String对象,同时也需要小心String.subString()与String.intern()的使用

尽量不要使用finalizer

释放不必要的引用:ThreadLocal使用完记得释放以防止内存泄漏,各种stream使用完也记得close。

使用对象池避免无节制创建对象,造成频繁gc。但不要随便使用对象池,除非像连接池、线程池这种初始化/创建资源消耗较大的场景,

缓存失效算法,可以考虑使用SoftReference、WeakReference保存缓存对象

谨慎热部署/加载的使用,尤其是动态加载类等

7、log4j文件:

观察log4j配置的轮巡加载日志到磁盘的配置,单个文件的大小不宜太大,会占用一部分的内存,同时Log4j通过打印线程堆栈实现,会生成大量String。

调优步骤:

GC日志:

dump文件:

1、dump文件的生成:

(1)第一种:JVM启动时增加两个参数:(常见方式)

第一种方式是一种事后方式,需要等待当前JVM出现问题后(例如oom了,就会生成dump文件)才能生成dump文件,实时性不高;

# 出现OOME时生成堆dump:

-XX:+HeapDumpOnOutOfMemoryError

# 生成堆文件地址:

-XX:HeapDumpPath=/home/hadoop/dump/

(新版本的openjdk无需增加该配置,OOM时会自动生成dump文件)

(2)第二种:当发现程序异常前,会通过执行指令,直接生成当前JVM的dump文件:

jmap -dump:file=文件名.dump [pid]

# 9257是指JVM的进程号

jmap -dump:format=b,file=testmap.dump 9257

调优目标:

调优的最终目的都是为了应用程序使用最小的硬件消耗来承载更大的吞吐量或者低延迟。 jvm调优主要是针对垃圾收集器的收集性能优化,减少GC的频率和Full GC的次数,令运行在虚拟机上的应用能够使用更少的内存、高吞吐量、低延迟。

下面列举一些JVM调优的量化目标参考实例,注意:不同应用的JVM调优量化目标是不一样的。

堆内存使用率<=70%;

老年代内存使用率<=70%;

avgpause<=1秒;

Full GC次数0或avg pause interval>=24小时 ;

GC日志相关

设置JVM GC格式日志的主要参数包括如下8个:

-XX:+PrintGC 输出简要GC日志

-XX:+PrintGCDetails 输出详细GC日志

-Xloggc:gc.log 输出GC日志到文件

-XX:+PrintGCTimeStamps 输出GC的时间戳(以JVM启动到当期的总时长的时间戳形式)

-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2020-04-26T21:53:59.234+0800)

-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息

-verbose:gc : 在JDK 8中,-verbose:gc是-XX:+PrintGC一个别称,日志格式等价于:-XX:+PrintGC。不过在JDK 9中 -XX:+PrintGC被标记为deprecated。 -verbose:gc是一个标准的选项,-XX:+PrintGC是一个实验的选项,建议使用-verbose:gc 替代-XX:+PrintGC

-XX:+PrintReferenceGC 打印年轻代各个引用的数量以及时长

-XX:HeapDumpPath:

堆内存出现OOM的概率是所有内存耗尽异常中最高的,出错时的堆内信息对解决问题非常有帮助,所以给JVM设置这个参数(-XX:+HeapDumpOnOutOfMemoryError),让JVM遇到OOM异常时能输出堆内信息,并通过(-XX:+HeapDumpPath)参数设置堆内存溢出快照输出的文件地址,这对于特别是对相隔数月才出现的OOM异常来说尤为重要。 这两个参数通常配套使用:

-XX:+HeapDumpOnOutOfMemoryError

-XX:HeapDumpPath=./

二、OOM

1、内存溢出:(Out Of Memory,简称OOM)是指应用系统中存在无法回收的内存或使用的内存过多,最终使得程序运行要用到的内存大于能提供的最大内存。

2、造成原因:

2.1、内存泄漏

由于长期保持某些资源的引用,垃圾回收器无法回收它,从而使该资源不能够及时释放,也称为内存泄露。因而尽量不要将所有引用都使用为强引用,可以在合适的地方使用弱引用和软引用。

2.2、超大对象

保存多个耗用内存过大或当加载单个超大的对象时,该对象的大小超过了当前剩余的可用内存空间。比如查询数据库中的数据,一次查询过多,直接导致内存溢出了。因此查询数据库如果数据过多尽量使用分页查询。

2.3、其他各种原因

比如是否存在死循环,大循环重复产生对象,是否有集合对象使用完之后,依然被引用着,导致无法清除,是否使用了不恰当的数据结构,导致占用空间过大等等

三、解决方案

https://blog.csdn.net/m0_67698950/article/details/125053199

1、例如: 堆溢出:Java heap space 表明了是堆内存的溢出。(最常见)

修改JVM启动参数,直接增加内存

JVM默认可以使用的内存为64M,Tomcat默认可以使用的内存为128MB,对于稍复杂一点的系统就会不够用。在某项目中,就因为启动参数使用的默认值,经常报“Out Of Memory”错误。因此,-Xms,-Xmx(堆内存的最小大小和最大大小值)参数一定不要忘记加。

如何处理:

(1)首选检查代码是否存在循环或者死循环,是否能够不断的创建对象。

(2)查看启动参数-Xmx和-Xms 设置的堆内存是否过小,不足以加载服务中的所有类,可以适当增加。

(3)检查代码中是否存在数据库查询,没有分页一次性返回大量数据。

还可以通过MAT或者VisualVM工具分析,找到占用大量堆空间的对象,然后做出合理优化。

2、直接内存溢出

Exceptioninthread"main"java.lang.OutOfMemoryError: Direct buffer memory

这个问题遇到的一般比较少,直接内存不是运行时数据区的一部分。

配置启动参数-XX:MaxDirectMemorySize设置直接内存的最大值,如果不配置此参数则默认最大可用直接内存是-Xmx的值

3、线程过多导致OOM(常见):

这个错误是程序中创建的线程过多,而线程需要的空间却不够了。

我们运行程序创建的线程都是需要一定内存的,而机器的内存大小是一定的,当线程的数量过多的时候,就会导致OOM。

Exceptioninthread"main"java.lang.OutOfMemoryError: unabletocreate newnativethread

如何处理:

首先检查代码,是否有优化的空间,减少不必要的线程创建,或者使用线程池复用线程。

减少堆空间的内存,这样操作系统可以预留更多的内存用于线程创建。

减少每个线程内存空间的占用,使用-Xss可以指定栈空间的大小(注意:太小会出现栈溢出)。

如果真的需要的线程特别多,但是受到操作系统的限,这种需要修改操作系统的最大线程数。

4、GC效率低下引起的OOM(较少见)

java.lang.OutOfMemoryError:GC over headlimitexceeded

https://blog.csdn.net/orecle_littleboy/article/details/119890296?utm_medium=distribute.pc_relevant.none-task-blog-2~default~baidujs_baidulandingword~default-0-119890296-blog-125354491.235^v27^pc_relevant_3mothn_strategy_and_data_recovery&spm=1001.2101.3001.4242.1&utm_relevant_index=3

Xms 是指设定程序启动时占用内存大小。一般来讲,大点,程序会启动的快一点,但是也可能会导致机器暂时间变慢。

Xmx 是指设定程序运行期间最大可占用的内存大小。如果程序运行需要占用更多的内存,超出了这个设置值,就会抛出OutOfMemory异常。

Xss 是指设定每个线程的堆栈大小。这个就要依据你的程序,看一个线程大约需要占用多少内存,可能会有多少线程同时运行等。

-Xms 可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。

堆大小设置

JVM 中最大堆大小有三方面限制:相关操作系统的数据模型(32-bt还是64-bit)限制;系统的可用虚拟内存限制;系统的可用物理内存限制。32位系统下,一般限制在1.5G~2G;64为操作系统对内存无限制。我在Windows Server 2003 系统,3.5G物理内存,JDK5.0下测试,最大可设置为1478m。

典型设置:

(1)java -Xmx3550m -Xms3550m -Xmn2g -Xss128k

- Xmx3550m :设置JVM最大可用内存为3550M。

-Xms3550m :设置JVM促使内存为3550m。此值可以设置与-Xmx相同,以避免每次垃圾回收完成后JVM重新分配内存。

-Xmn2g :设置年轻代大小为2G。整个堆大小=年轻代大小 + 年老代大小 + 持久代大小 。持久代一般固定大小为64m,所以增大年轻代后,将会减小年老代大小。此值对系统性能影响较大,Sun官方推荐配置为整个堆的3/8。

-Xss128k :设置每个线程的堆栈大小。JDK5.0以后每个线程堆栈大小为1M,以前每个线程堆栈大小为256K。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。但是操作系统对一个进程内的线程数还是有限制的,不能无限生成,经验值在3000~5000左右。

(2)java -Xmx3550m -Xms3550m -Xss128k -XX:NewRatio=4 -XX:SurvivorRatio=4 -XX:MaxPermSize=16m -XX:MaxTenuringThreshold=0

-XX:NewRatio=4 :设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5

-XX:SurvivorRatio=4 :设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6

-XX:MaxPermSize=16m :设置持久代大小为16m。

-XX:MaxTenuringThreshold=0 :设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代 。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间 ,增加在年轻代即被回收的概论。

常见配置汇总

一、堆设置

-Xms :初始堆大小

-Xmx :最大堆大小

-Xmn:年轻代大小(包括Eden区和两个survivor区)

-XX:NewSize=n :设置年轻代大小

-XX:NewRatio=n: 设置年轻代和年老代的比值。如:为3,表示年轻代与年老代比值为1:3,年轻代占整个年轻代年老代和的1/4

-XX:SurvivorRatio=n :年轻代中Eden区与两个Survivor区的比值。注意Survivor区有两个。如:3,表示Eden:Survivor=3:2,一个Survivor区占整个年轻代的1/5

-Xss:设置每个线程的堆栈大小

-XX:MaxPermSize=n :设置持久代大小

堆是GC收集垃圾的主要区域。GC分为两种:Minor GC、Full GC。

https://cloud.tencent.com/developer/article/2055935


二、收集器设置

-XX:+UseSerialGC :设置串行收集器

-XX:+UseParallelGC :设置并行收集器

-XX:+UseParalledlOldGC :设置并行年老代收集器

-XX:+UseConcMarkSweepGC :设置并发收集器

三、垃圾回收统计信息

-XX:+PrintGC

-XX:+PrintGCDetails

-XX:+PrintGCTimeStamps

-Xloggc:filename

四、行收集器设置

-XX:ParallelGCThreads=n :设置并行收集器收集时使用的CPU数。并行收集线程数。

-XX:MaxGCPauseMillis=n :设置并行收集最大暂停时间

-XX:GCTimeRatio=n :设置垃圾回收时间占程序运行时间的百分比。公式为1/(1+n)

五、并发收集器设置

-XX:+CMSIncrementalMode :设置为增量模式。适用于单CPU情况。

-XX:ParallelGCThreads=n :设置并发收集器年轻代收集方式为并行收集时,使用的CPU数。并行收集线程数。

四、调优总结

年轻代大小选择

响应时间优先的应用 :尽可能设大,直到接近系统的最低响应时间限制 (根据实际情况选择)。在此种情况下,年轻代收集发生的频率也是最小的。同时,减少到达年老代的对象。

吞吐量优先的应用 :尽可能的设置大,可能到达Gbit的程度。因为对响应时间没有要求,垃圾收集可以并行进行,一般适合8CPU以上的应用。

年老代大小选择

响应时间优先的应用 :年老代使用并发收集器,所以其大小需要小心设置,一般要考虑并发会话率 和会话持续时间 等一些参数。如果堆设置小了,可以会造成内存碎片、高回收频率以及应用暂停而使用传统的标记清除方式;如果堆大了,则需要较长的收集时间。最优化的方案,一般需要参考以下数据获得:

并发垃圾收集信息

持久代并发收集次数

传统GC信息

花在年轻代和年老代回收上的时间比例

减少年轻代和年老代花费的时间,一般会提高应用的效率

吞吐量优先的应用 :一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。

较小堆引起的碎片问题

因为年老代的并发收集器使用标记、清除算法,所以不会对堆进行压缩。当收集器回收时,他会把相邻的空间进行合并,这样可以分配给较大的对象。但是,当堆空间较小时,运行一段时间以后,就会出现“碎片”,如果并发收集器找不到足够的空间,那么并发收集器将会停止,然后使用传统的标记、清除方式进行回收。如果出现“碎片”,可能需要进行如下配置:

-XX:+UseCMSCompactAtFullCollection :使用并发收集器时,开启对年老代的压缩。

-XX:CMSFullGCsBeforeCompaction=0 :上面配置开启的情况下,这里设置多少次Full GC后,对年老代进行压缩

区域 含义

S0C Survive0 第一个幸存区的容量大小

S1C Survive1 第二个幸存区的容量大小

S0U Survive0 第一个幸存区已使用的内存大小

S1U Survive1 第二个幸存区已使用的内存大小

EC Eden 伊甸园区的容量大小

EU Eden 伊甸园区已使用的内存大小

OC Old 老年代的容量大小

OU Old 老年代已使用的内存大小

MC Meta 方法区的容量大小

MU Meta 方法区已使用的内存大小

YGC Young GC发生的次数

YGCT Young GC的总耗时

FGC Full GC发生的次数

FGCT Full GC的总耗时

————————————————

版权声明:本文为CSDN博主「Petter's Blog」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:https://blog.csdn.net/A_captain_608/article/details/126391048

相关文章

网友评论

      本文标题:Java性能调优

      本文链接:https://www.haomeiwen.com/subject/nlqordtx.html