JVM调优
对JVM的调优,目的是减少GC的频率和Full GC的次数。
JVM 调试工具
jps, jstat, jmap, jinfo, jhat, jstack, jconsole
JMeter
Java Meter(Measure Performance), 压测工具。
创建线程组
创建sampler
其它工具
jps: 查看 jvm 进程. 后面可以加远程的地址.
jstatd: 开启远程调试.
# 标准的错误流 重定向到 标准输出流 重定向到 文件 后台运行
jstatd 2>&1 > log.txt &
ips=("127.0.0.1" "192.168.31.124")
for ip in ${ips[*]}; do
jps $ip
done
指令后面有个 d, 一般是 daemon, 守护进程的意思.
jstat: Java Visual Machine Statistics Monitoring Tool. JVM统计监控工具.
# 进程id 时间 次数
jstat -gcutil pid time num
# S Survivor
# E Eden
# O Old
# M MetaSpace
# CCS Compressed class space utilization as a percentage. 类的元数据压缩. 64bit, 避免浪费, 进行压缩, 32bit 有 4GB 的选址空间.
# YGC Young GC GC次数
# YGCT Young GC Time
# FGC Full GC GC次数
# FGCT Full GC Time
# CGC
# GCT Total GC Time 所有gc花费时间
# S0 S1 E O M CCS YGC YGCT FGC FGCT CGC CGCT GCT
# 0.00 100.00 6.58 8.46 93.49 59.75 2 0.007 0 0.000 0 0.000 0.007
jcmd
jcmd pid GC.heap_info
# 29236:
# garbage-first heap total 131072K, used 14399K [0x00000000f8000000, 0x0000000100000000)
# region size 1024K, 10 young (10240K), 4 survivors (4096K)
# Metaspace used 1856K, committed 1984K, reserved 1056768K
# class space used 152K, committed 256K, reserved 1048576K
# committed 已提交的. 操作系统给 JVM 的是虚拟内存. 和物理内存有一个映射关系. 代表有这么多内存发生了映射关系.
# -Xmx:128g -Xms:128g 报错. 'Not enough space'. 申请的是堆内存, 是物理内存, 而 committed 是堆外内存(本地内存)来的.
# 虚拟内存 机制, 和硬盘进行交换.
jmap: Java Memory Map
, 导出 JVM 实例的共享对象映射, 堆内详细信息, 支持远程调试.
jmap -dump:live,format=b,file=heap.bin
# 查看
jhat heap.bin
jinfo: Java Configuration Info
, 查看修改虚拟机配置(可远程)
# 有些参数是不支持运行时调整的
jinfo -flag MaxHeapSize=10240 pid
# 查看参数
jcmd pid VM.flags -all
jstack: Java Stack Trace
, 打印 Java 的 Stack(支持远程调试)
jstack ${PID}
# 统计线程数
jstack ${PID} | grep "java.lang.Thread.State" | wc -l
jconsole: 一个实时监控图形界面。
JVM 常用参数
- 标准参数
-cp
(classpath) -X
扩展参数. -Xmx256m 设置Heap Size
最大占用.-XX
开发用(比如打印日志).-XX:+PrintGCDetails
: 打印 GC 详情.-XX:+PrintGCTimeStamps
: 增加时间描述.-Xss
: 规定了每个线程虚拟机栈(堆栈)的大小, 会影响并发线程数的大小.-Xms
: 堆的初始值.-Xmx
: 堆的最大值. 一般都和 -Xms 一样, 因为当内存不够时, 进行扩展时, 会发生内存抖动, 影响程序运行时的稳定性. -Xmx20M: 构造OutOfMemory.-Xmn
: 新生代的大小.-XX:SurvivorRatio
: Eden 和 Survivor 的比值, 默认 8:1-XX:NewRatio
: 老年代和年轻代内存的大小比例, 例如: 2 就是老年代空间是年轻代空间的2倍, Old:Eden = 2:1, 就是说 young 占栈的 1/3, Old 占 2/3. 总大小是通过-Xmx
和-Xms
来决定的.-XX:NewSize
: 新生代的最小的大小. 建议和MaxNewSize设计成一样。-XX:MaxNewSize
: 新生代最大的大小.-XX:MaxTenuringThreshold
: 对象从年轻代晋升到老年代经过的 GC 次数(age)的最大阈值(默认15)-Xlog:gc*
: 打印所有和GC相关的信息-Xlog:gc+age=trace
: 打印老年代信息-Xlog:safepoint
: 打印上一次暂停点距离STW的时间.-XX:MaxDirectMemorySize
: 调整堆外内存映射大小.-XX:-UseCompressedClassPointers
:-
关闭压缩算法,使用压缩算法+
-XX:-UseCompressedOops
: 关闭对象压缩.
内存抖动: 通常指在短时间内发生了多次内存的分配和释放。
JVM 内存大小取舍
JVM 的优化本质上就是 JVM 的调参.
- 扩大内存可以更少的触发GC
- 内存太大触发GC的时候停顿时间会变长
吞吐量 = 花费在非 GC 停顿上的工作时间 / 总时间, 至少需要优化到 95%.
-Xms
启动JVM时堆内存的大小,但是并不意味着程序启动就占用这么多。因为堆是要在程序被使用的时候才会被占用的,当运行过程中使用到堆,堆逐渐增大,直到达到-Xms
设置的初始值,那么JVM的堆大小就不会再次调整到-Xms
以下了。默认是物理内存的1/64。
-Xmx
堆内存的最大限制
因此,两者设置成一样, 防止扩缩容,避免堆的大小在运行过程中频繁JVM堆频繁调整。
-XX:NewSize
年轻代初始大小
-XX:MaxNewSize
最大年轻代大小
两者设置成一样, 防止扩缩容.
-XX:SurvivorRatio
Eden Survivor 占比, 默认为8
Eden 需要比 Survivor 尽可能大, 防止多次触发 young gc
导致年龄增长过快, 然后进入老年代的情况.
-XX:MetaspaceSize
元空间初始空间大小
-XX:MaxMetaspaceSize=512m
元空间最大空间, 默认没有限制.
JVM 内存调优思路
- 监控GC的状态,使用
jstat
实时查看,启动时打开gc相关的日志参数,分析堆内存快照(jmap
指令,dump堆内详细信息) - 生成堆的dump文件。通过JMX的MBean生成当前的Heap信息(hprof文件),没有启动JMX,可以通过
jmap
生成。 - 分析dump文件。使用Mat,Visual VM,JDK自带的Hprof工具可以查看。
- 分析结果,判断是否需要优化。如果GC频率不高,GC耗时不高,就没必要优化。
- Minor GC执行时间不到50ms;
- Minor GC执行不频繁,10s一次;
- Full GC执行时间不到1s;
- Full GC执行频率不高,不低于10分钟一次。
- 调整GC类型和内存分配。
- 不断地分析和调整。不断试验和试错。
调优参数参考
-Xms
,-Xmx
设置为一样。防止扩缩容。- 年轻代和老年代比例。默认1:2。通过
-XX:NewRadio
调整。- 如何选择依赖应用程序的生命周期分布情况。应用存在大量的临时对象,应该选择更大的年轻代;如果较多持久对象,老年代适当增大。但是,其实很多应用是没有这个特性的。
- 本着
Full GC
尽量少的原则,让老年代尽量缓存常用对象。 - 通过观察应用一段时间,看峰值是老年代会占用多少内存,根据实际情况增大年轻代。
- 在配置较好的机器上,可以为老年代选择并行收集算法,
-XX:+UserParallelOldGC
。 - 线程栈设置:每个线程默认是开启1M的栈的,用于存放栈,调用参数,局部变量等,但对于大多数应用而言这个设置太大了,一般256K就够了。
CMS调优
CMS Full GC 条件
Promotion Failure
: 由于内存碎片导致的晋升空间不足(年轻代->老年代).Concurrent Mode Failed
: 还未完成 CMS 又触发了下一次 Major GC.
CMS调优参数
-XX:ParallelGCThreads=N
设置年轻代的并行收集线程数, 避免 docker 踩坑. 如果不指定, 会根据当前电脑的CPU核数进行配置. 所以要配置, 要不可能会出现CPU资源分配的问题.
-XX:ParallelCMSThreads=N
设置CMS
的并行收集线程数, 避免 docker 踩坑. 会根据上面的参数进行调整.
-XX:+UseCMSCompactAtFullCollection
FullGC
情况下的 inital remark
or final remark
整理内存碎片
-XX:+CMSFullGCBeforeCompaction=4
两次 FullGC
情况下的 Initial remark
or final remark
4 次后才整理内存碎片
-XX:+UseCMSInitiatingOccupacyOnly
让阈值驱动 CMS
触发时机
-XX:CMSInitiatingOccupancyFraction=70
老年代占满70%才触发CMS. 一定要设置上面的参数, 否则 JVM 只会遵循一次, 之后还是只会靠自己判断.
-XX:+CMSParallelRemarkEnabled
并行remark
-XX:+CMSScavengeBeforeRemark
remark
前先做一次 minor gc
G1调优
-XX:+UseG1GC
使用G1
-XX:MaxGCPauseMillis=n
GC最大停顿时间, 软性参数
-XX:G1HeapRegionSize=n
每个 region
的大小
GC 实战场景解决方案
- 网络阻塞导致
OutOfMemory
- 堆外内存溢出
- 进程崩溃
- 从ElasticSearch调优看GC优化策略
- IDEA的配置加速
- 引用问题定位
网络阻塞
jmap -dump:live,format=b,file=heap.bin
dump堆信息文件, 然后jhat
指令查看。
写操作太多, 导致网络压力. 分批进行. 1w次分100次, 就1k次网络请求. 日志文件系统, 读写都是日志, 还原到内存才是数据。
堆内存溢出
出现这种情况考虑堆外内存. 数据到网卡, 需要拷贝到内存中(在内核空间). CPU 中的 DMA
负责. 而 JVM 的线程在 用户空间, 如果想要使用, 就要拷贝或者使用映射的方式进行引用. NIO
就是使用映射的. 这时候走的不是堆内存了. 如果开太多连接, 就很有可能 OutOfMemory
.
-XX:MaxDirectMemorySize
调整堆外映射内存的大小.- 使用队列. 队列就是一个天然的缓冲区.
如果是栈溢出, -Xss2m
设置每个线程的堆栈的大小. 堆栈 指的 其实就是 栈.
进程崩溃
在linux中, Socket
的连接有最大连接数的限制. 与慢的服务建立异步队列, 把任务积压过去. 好处: 你的服务不崩. 队列也是解耦神器.
ES优化策略
设置最大值和最小值一样, 可以避免一步步扩容的情况, 避免缓慢增加导致GC频繁, 像这种内存数据库, 一步到位就好了.
关闭交换区(swapoff -a),防止 es 把对象交换到 磁盘 上,出现缺页中断的情况,保证所有的索引都在内存中。全局修改的话修改/etc/fstab
文件。
# 日志相关
-Xlog:gc*,gc+age=trace,safepoint:file=/var/log/myapp/gc.log:utctime,pid,tags:filecount=32,fileszie=64m
IDEA优化
-Xms2048m
设置如果太少, 启动时可能频繁GC.-Xmx2048m
设置堆的大小.-Xmn512m
调整新生代的大小. 占堆的1/4左右-XX:ReservedCodeCacheSize=1024m
idea的配置项.-ea
启动断言机制.-XX:+HeapDumpOnOutOfMemoryError
如果发生OOM, 就会dump出heap信息.-Xverify:none
不进行byteCode
校验.
引用问题查找
以Android应用为例,打开profiler
界面。
Total Count:这个对象在内存里面包含有多少个。
Shallow Size:占用的多少的体积。
导出内存快照
# 转换成mat可以使用的格式 这个工具在 Android SDK中有
hprof-conv -z memory-20200813T172141.hprof memory-mat.hprof
使用 Eclipse Memory Analyzer 打开。
可以看到 MainActivity 被 Handler 引用着,无法回收。
修改,在 onDestroy 中移除 message。
已经不存在了。
解决方案:为什么用 WeakReference,而不是用 SoftReference ?
使用 static 修饰的话,会在链接阶段就进行加载,存到 方法区。内部类持有外部类的引用。不是内部类,这时不会持有Activity的引用。缺点:如果频繁使用 static ,导致 jvm 加载类的时候消耗过多。
使用 WeakReference,GC 回收的时候,就会被自动清除掉,缺点:Activity关闭之后,Handler可能(Activity被GC回收了)无法继续调用Activity的方法。
使用 SoftReference,内存不足的时候才会被清除。缺点:和 WeakReference 一样, 不知道什么时候会被回收。
Handler 是否会导致内存泄漏,还要看具体的逻辑代码到底是什么情况,看业务需求。