抱歉,您的浏览器无法访问本站

本页面需要浏览器支持(启用)JavaScript


了解详情 >

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 的调参.

  1. 扩大内存可以更少的触发GC
  2. 内存太大触发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 内存调优思路

  1. 监控GC的状态,使用jstat实时查看,启动时打开gc相关的日志参数,分析堆内存快照(jmap指令,dump堆内详细信息)
  2. 生成堆的dump文件。通过JMX的MBean生成当前的Heap信息(hprof文件),没有启动JMX,可以通过jmap生成。
  3. 分析dump文件。使用Mat,Visual VM,JDK自带的Hprof工具可以查看。
  4. 分析结果,判断是否需要优化。如果GC频率不高,GC耗时不高,就没必要优化。
    1. Minor GC执行时间不到50ms;
    2. Minor GC执行不频繁,10s一次;
    3. Full GC执行时间不到1s;
    4. Full GC执行频率不高,不低于10分钟一次。
  5. 调整GC类型和内存分配。
  6. 不断地分析和调整。不断试验和试错。

调优参数参考

  1. -Xms-Xmx设置为一样。防止扩缩容。
  2. 年轻代和老年代比例。默认1:2。通过-XX:NewRadio调整。
    1. 如何选择依赖应用程序的生命周期分布情况。应用存在大量的临时对象,应该选择更大的年轻代;如果较多持久对象,老年代适当增大。但是,其实很多应用是没有这个特性的。
    2. 本着Full GC尽量少的原则,让老年代尽量缓存常用对象。
    3. 通过观察应用一段时间,看峰值是老年代会占用多少内存,根据实际情况增大年轻代。
  3. 在配置较好的机器上,可以为老年代选择并行收集算法-XX:+UserParallelOldGC
  4. 线程栈设置:每个线程默认是开启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 实战场景解决方案

  1. 网络阻塞导致OutOfMemory
  2. 堆外内存溢出
  3. 进程崩溃
  4. 从ElasticSearch调优看GC优化策略
  5. IDEA的配置加速
  6. 引用问题定位

网络阻塞

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 打开。

可以看到 MainActivityHandler 引用着,无法回收。

修改,在 onDestroy 中移除 message。

已经不存在了。

解决方案:为什么用 WeakReference,而不是用 SoftReference ?

使用 static 修饰的话,会在链接阶段就进行加载,存到 方法区。内部类持有外部类的引用。不是内部类,这时不会持有Activity的引用。缺点:如果频繁使用 static ,导致 jvm 加载类的时候消耗过多。

使用 WeakReference,GC 回收的时候,就会被自动清除掉,缺点:Activity关闭之后,Handler可能(Activity被GC回收了)无法继续调用Activity的方法。

使用 SoftReference,内存不足的时候才会被清除。缺点:和 WeakReference 一样, 不知道什么时候会被回收。

Handler 是否会导致内存泄漏,还要看具体的逻辑代码到底是什么情况,看业务需求。

评论