jvm系列——5.JVM调优(建议按目录看)

S1.JVM调优

前文笔者通过四篇文章,包括内存结构,类加载器,执行引擎,垃圾回收器,具体讲解了JVM的基本知识。而这些知识的真正用途,笔者相信大多数人都不是为了去实现自己的JVM。更多的是为了对JVM进行调优。本文将具体的讲解JVM调优的那些事。

S1.1.介绍

JVM调优是指通过优化Java虚拟机(JVM)来提高Java应用程序的性能和效率的过程。JVM是Java应用程序的运行时环境,包括内存结构,类加载器,执行引擎,垃圾回收器等组件,它们会对应用程序的性能产生影响。

S1.2.调优目的

进行JVM调优的目的就是优化这些组件的配置、调整它们的参数、降低开销和延迟,从而提高应用程序的性能和可靠性。在实践中,JVM调优通常涵盖深入理解Java虚拟机,监测应用程序行为,识别性能瓶颈,调整垃圾收集器和堆内存大小等方面。

S2.JVM调优参数合集

S2.1.参数格式简介

JVM参数格式一般由"-"加参数名称,后面再跟参数的值组成。

在使用JVM参数的时候,可以分为以下几类:

标准参数(-开头):是所有JVM实现都必须支持的参数,一般是用来控制JVM的基本行为,如 -help、-version 等。

非标准参数(-X开头):是所有JVM实现选做的参数,一般是用于改变JVM的一些默认实现行为,如 -Xms、-Xmx 等。

高级参数(-XX:开头):是所有JVM实现特有的参数,一般是用于控制JVM的高级行为,比如改变JVM垃圾收集器的策略,或者改变JIT编译策略等。

具体例子:

java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:NewSize=256m

S2.2.内存结构常用参数

指令

作用

例子

-XX:InitialHeapSize=

-Xms

设置 JVM 的初始堆大小。

-Xms512m 表示设置 JVM 的初始堆大小为 512MB。

-XX:MaxHeapSize=

-Xmx

设置 JVM 的最大堆大小。

-Xmx1024m 表示设置 JVM 的最大堆大小为 1024MB。

-XX:MaxNewSize=

-Xmn

设置 JVM 新生代的大小。

例如,-Xmn256m 表示设置 JVM 新生代的大小为 256MB。

-XX:NewSize=

-Xns

设置 JVM 新生代的初始大小。

-XX:NewSize=128m 表示设置 JVM 新生代的初始大小为 128MB。

-XX:OldSize=

老年代大小

-XX:OldSize=示设置 JVM 老年代的初始大小为 128MB。

-XX:NewRatio=

设置 JVM 新生代与老年代的比例。

-XX:NewRatio=2 表示设置新生代与老年代的比例为 1:2。

-XX:SurvivorRatio=

设置 JVM Eden 区域与 Survivor 区域的比例

-XX:SurvivorRatio=8 表示设置 Eden 区域与 Survivor 区域的比例为 8:1

-XX:MaxPermSize=

设置 JVM 永久代的最大大小(jdk1.8后没有永久代)

-XX:MaxPermSize=256m 表示设置 JVM 永久代的最大大小为 256MB

-XX:MetaspaceSize=

设置 JVM 元空间的初始大小(1.8之后)

-XX:MetaspaceSize=256m 表示设置 JVM 元空间的初始大小为 256MB

-XX:MaxMetaspaceSize=

设置 JVM 元空间的最大大小(1.8之后)

-XX:MetaspaceSize=512m 表示设置 JVM 元空间的最大大小为 512MB

-Xss

每个线程的栈内存大小

-Xss8m 示设置 JVM每个线程的栈内存为8m

S2.3.类加载器常用参数

指令

作用

例子

-classpath

用来指示查找类的路径

-classpath /home/user/myapp

-Xbootclasspath/a:<路径>

指定JVM启动时类的路径,包括扩展路径、应用程序路径和引导路径。在bootclasspath后面添加。

-Xbootclasspath/p:<路径>

指定JVM启动时类的路径,包括扩展路径、应用程序路径和引导路径。在bootclasspath前面添加。

-verbose:class

打印类加载器的详细信息,包括类加载的顺序和所在的位置。

-XX:[+/-]TraceClassLoading

是否打印所有加载的类信息。

-XX:+raceClassLoading

打印所有加载的类信息。

-XX:[+/-]HeapDumpOnOutOfMemoryError

是否开启堆内存溢出时打印

-XX:+HeapDumpOnOutOfMemoryError

开启堆内存溢出时打印

-XX:HeapDumpPath=[路径]

指定堆内存溢出时打印目录

S2.4.执行引擎常用参数

指令

作用

例子

-Xint

完全解释器模式执行

-Xcomp

完全采用即时编译器执行,如果即时编译器出现问题,解释器会介入执行

-Xmixed

混合模式

-client

指定虚拟机在Client模式下运行(使用Client即时编译器)

-server

指定虚拟机在Server模式下运行(使用Server即时编译器)

S2.5.垃圾回收器常用参数

指令

作用

例子

-XX:[+/-]UseSerialGC

是否开启串行回收器

-XX:[+/-]UseParallelGC

是否开启并行,关注吞吐量的新生代回收器

-XX:[+/-]UseParNewGC

是否开启并行新生代垃圾回收器

-XX:[+/-]UseParallelOldGC

是否开启并行,关注吞吐量的老年代回收器

-XX:[+/-]UseConcMarkSweepGC

是否开启,并发,老年代回收器(GMS)

-XX:[+/-]UseG1GC

是否开启并发分区型回收器G1

-XX:[+/-]UseAdaptiveSizePolicy

是否开启随着GC,会动态调整新生代的大小,Eden,Survivor比例等

-XX:[+/-]PrintGCDetails

-XX:[+/-]PrintGCTimeStamps

-XX:[+/-]PrintGCDateStamps

-Xloggc:logs/gc.log

回收器日志输出设置

-XX:MaxTenuringThreshold=

对象进入老年代的年龄阈值,默认值为15

-XX:InitiatingHeapOccupancyPercent

当整个堆占用超过某个百分比时,就会触发并发GC周期,基于整个堆的占用率,默认值为45

-XX:G1HeapWastePercent

允许整个堆内存中被浪费的空间的百分比,默认值为5%。如果并发标记可回收的空间小于5%,则不会触发MixedGC

-XX:MaxGCPauseMills

G1最大停顿时间,暂停时间不能太小,太小会导致G1跟不上垃圾产生的速度,最终退化成Full GC。

-XX:ConcGCThreads

并发垃圾收集器使用的线程数量

-XX:G1MixedGCLiveThresholdPercent

混合垃圾回收周期中要包括的老年代域设置占用率阈值,默认65%

-XX:G1MixedGCCountTarget

G1回收分区时最大混合式GC周期数,默认值为8。

-XX:G1OldCSetRegionThresholdPercent

设置混合垃圾回收期间要回收的最大老年代域数,默认值为10

S3.调优工具

S3.1.监控工具

jps(Java Virtual Machine Process Status Tool)是JDK 1.5提供的一个显示当前所有java进程pid的命令,简单实用,非常适合在linux/unix平台上简单察看当前java进程的一些简单情况。常用参数:-p,显示pid;-m,显示传递给main方法的参数;-l,输出应用程序main class的完整package名 或者 应用程序的jar文件完整路径名;-v,输出传递给JVM的参数。

jstat是JDK自带的一个轻量级小工具。它位于java的bin目录下,主要利用JVM内建的指令对Java应用程序的资源和性能进行实时的命令行的监控,包括了对堆内存和垃圾回收状况的监控。命令用法:jstat [-命令选项] [进程的pid] [间隔时间/毫秒] [查询次数]。命令选项包括:-class 用于查看类加载情况的统计;-compiler 用于查看HotSpot中即时编译器编译情况的统计;-gc 用于查看JVM中堆的垃圾收集情况的统计;-gccapacity 用于查看新生代、老生代及持久代的存储容量情况;-gcmetacapacity 显示metaspace的大小;-gcnew 用于查看新生代垃圾收集的情况;-gcnewcapacity 用于查看新生代存储容量的情况;-gcold 用于查看老生代及持久代垃圾收集的情况;-gcoldcapacity 用于查看老生代的容量;-gcutil 显示垃圾收集信息;-gccause 显示垃圾回收的相关信息(通-gcutil),同时显示最后一次仅当前正在发生的垃圾收集的原因;-printcompilation 输出JIT编译的方法信息

S3.2.故障排查工具

jinfo(Configuration Info for Java) 查看虚拟机配置参数信思,也可用于调整虚拟机的配置参数。在很多情况下,Java应用程序不会指定所有的Java虚拟机参数。而此时,开发人员可能不知道某一个具体的Java虚拟机参数的默认值。在这种情况下,可能需要通过查找文档获取某个参数的默认值。这个查找过程可能是非常艰难的。但有了 jinfo工具,开发人员可以很方便地找到Java虚拟机参数的当前值。jinfo不仅可以查看运行时某一个Java虚拟机参数的实际取值, 甚至可以在运行时修改部分参数,并使之立即效。但是,并非所有参数都支持动态修改。参数只有被标记 manageable的flag可以被实时修改。其实,这个修改能力是 极其有限的。常用参数:-sysprops pid,查看该进程的全部配置信息;jinfo -flags pid,查看曾经赋过值的参数值;jinfo -flag <具体参数> pid,查看具体参数的值;jinfo -flag [+/-]<参数> pid,修改布尔值类型的参数;jinfo -flag [参数名=参数值] pid,修改非布尔型的参数。

jmap(Java Memory Map)主要用于打印指定Java进程(或核心文件、远程调试服务器)的共享对象内存映射或堆内存细节。命令格式jmap [option] 。常用参数:-heap,打印Java堆概要信息,包括使用的GC算法、堆配置参数和各代中堆内存使用情况;-histo,打印Java堆中对象直方图,通过该图可以获取每个class的对象数目,占用内存大小和类全名信息,-histo:live,打印jvm heap的直方图,但是只统计存活对象的情况;-clstatst,(1.8以后从-permsta改为-clstatst)打印 类加载器(永久代)统计信息;-finalizerinfo,打印等待回收的对象信息;-dump: ,以hprof二进制格式将Java堆信息输出到文件内,该文件可以用MAT、VisualVM或jhat等工具查看,dump-options选项包括,live 只输出活着的对象,format=b 指定输出格式为二进制,file= 指定文件名及文件存储位置,例如:jmap -dump:live,format=b,file=G:\heap.bin ;-F 与-dump: 或-histo一起使用,当没有响应时,强制执行;注意:不支持live子选项。

jstack命令用于打印指定Java进程、核心文件或远程调试服务器的Java线程堆栈的跟踪信息。换句话说,就是jstack能生成JVM当前时刻的线程快照,以此来定位线程出现长时间停顿的原因。指令格式:jstack [-option] 。常用参数包括,-F,当jstack指令无响应时,强制打印一个堆栈信息;-m打印包含Java和C/C++帧的混合模式堆栈跟踪;-l打印关于锁的其他信息,比如拥有java.util.concurrent ownable同步器的列表。

jcmd 是一个多功能的工具,相比 jstat 功能更为全面的工具,可用于获取目标 Java 进程的性能统计、JFR、内存使用、垃圾收集、线程堆栈、JVM 运行时间,也可以手动执行 GC、导出线程信息、堆信息等信息。常用参数包括,-l,查看当前机器上所有的 jvm 进程信息;GC.heap_info查看JVM内存信息,虽然名称为heap_info,但是除了堆内存信息,也会有堆外内存之一的Metaspace的信息,相比jstat命令结果会更直观一些;PerfCounter.print,查看指定进程的性能统计信息;VM.uptime,查看 JVM 的已启动时长;GC.class_histogram,查看系统中类统计信息;Thread.print,查看线程堆栈信息;GC.heap_dump,查看 JVM 的Heap Dump,导出的 dump 文件,可以使用MAT 或者 Visual VM 等工具进行分析(如果只指定文件名,默认会生成在启动 JVM 的目录里);VM.system_properties,查看 JVM 的属性信息;VM.flags,查看 JVM 的启动参数;VM.command_line,查看 JVM 的启动命令行; GC.run_finalization,对 JVM 执行 java.lang.System.runFinalization(),执行一次 finalization 操作,相当于执java.lang.System.runFinalization(),调用已经失去引用的对象的finalize方法,但是JVM可以选择执行或者不执行;GC.run,对 JVM 执行 java.lang.System.gc(),同 GC.run_finalization 告诉垃圾收集器打算进行垃圾收集,但是JVM可以选择执行或者不执行;VM.version,查看目标jvm进程的版本信息;VM.native_memory,查看目标jvm进程的Native Memory Tracking (NMT)信息,用于追踪JVM的内部内存使用。

jhat是Java堆分析工具(Java heap Analyzes Tool)。 在JDK6u7之后成为标配。使用该命令需要有一定的Java开发经验,官方不对此工具提供技术支持和客户服务。命令格式:jhat [ options ] heap-dump-file。常用参数包括,-stack false|true,关闭对象分配调用栈跟踪(tracking object allocation call stack),如果分配位置信息在堆转储中不可用. 则必须将此标志设置为 false. 默认值为true;-refs false|true,关闭对象引用跟踪(tracking of references to objects),默认值为 true. 默认情况下, 返回的指针是指向其他特定对象的对象,如反向链接或输入引用(referrers or incoming references), 会统计/计算堆中的所有对象;-port port-number,设置 jhat HTTP server 的端口号. 默认值 7000;-exclude exclude-file,指定对象查询时需要排除的数据成员列表文件(a file that lists data members that should be excluded from the reachable objects query), 例如, 如果文件列列出了 java.lang.String.value , 那么当从某个特定对象 Object o 计算可达的对象列表时, 引用路径涉及 java.lang.String.value 的都会被排除;-baseline exclude-file,指定一个基准堆转储(baseline heap dump), 在两个 heap dumps 中有相同 object ID 的对象会被标记为不是新的(marked as not being new). 其他对象被标记为新的(new). 在比较两个不同的堆转储时很有用;-debug int,设置 debug 级别,0表示不输出调试信息,值越大则表示输出更详细的 debug 信息;-version,启动后只显示版本信息就退出。-J< flag >,因为 jhat 命令实际上会启动一个JVM来执行, 通过 -J 可以在启动JVM时传入一些启动参数,例如, -J-Xmx512m 则指定运行 jhat 的Java虚拟机使用的最大堆内存为 512 MB,如果需要使用多个JVM启动参数,则传入多个 -Jxxxxxx。

S3.3.可视化工具

jhsdb(Java HotSpot Debugger)是一个针对HotSpot虚拟机的调试工具,可用于分析Java应用程序的运行状态。它提供了命令行界面和图形界面两种使用方式,可以帮助开发人员定位应用程序中的问题。jhsdb具有源级调试和字节码级调试功能,并可以检查线程、堆栈、变量和对象的状态,以及进行断点调试等常见调试操作。具体请查看https://www.javacodegeeks.com/2017/06/jhsdb-new-tool-jdk-9.html

jconsole,是一个基于JMX的GUI工具,用于连接正在运行的JVM,不过此JVM需要使用可管理的模式启动。具体请查看https://blog.csdn.net/ma_xiao_qi/article/details/124712769

VisualVM,是一个工具,它提供了一个可视界面,用于查看 Java 虚拟机上运行的基于 Java 技术的应用程序(Java 应用程序)的详细信息。具体请查看https://github.com/oracle/visualvm

Java Mission Control可用于本地/远程监控JVM的运行状态的管理工具,具体请查看https://blog.csdn.net/suremeng/article/details/51584785

S4.调优思路

笔者认为JVM调优的基本思路就是,启动时尽量根据使用的硬件性能配置好参数,能不进行JVM调优就不进行JVM调优,绝绝绝绝大部分的问题都应该通过优化代码解决。JVM调优应该是最最最最后的选择,JVM调优需要前期严谨的分析和监控。所以JVM调优一般是伴随着一定的问题,那么要做的就是定位问题和解决问题。

通常情况下我们都是通过jvm调优工具对,吞吐量、延迟和内存占用三个指标进行监测,初步估计问题所在,然后再深入对某些特定指标进行监测分析。这三个指标即使定位问题原因的参考,也是问题的表现。通过对这三个指标的分析一般可以大致分析出问题。

这三个属性中,其中一个任何一个属性性能的提高,几乎都是以另外一个或者两个属性性能的损失作代价,不可兼得,具体某一个属性或者两个属性的性能对应用来说比较重要,要基于应用的业务需求来确定。经常遇到的问题大致有内存溢出、内存泄漏、占用CPU过高、内存飙高、频繁 minor gc、频繁full gc等问题,大多数问题都不会单独出现。

S5.常见场景总结

S5.1.内存溢出

首先,可以简单简单的认为就是堆内存不足导致,单方面的加大了堆内存-Xms

如果问题依然没有解决,只能从堆内存信息下手,通过开启了-XX:+HeapDumpOnOutOfMemoryError参数 获得堆内存的dump文件。用堆分析工具对dump文件进行分析,通过堆分析工具查看到占用内存最大的对象,跟踪对象找到其引用的地方。

如果对象占用比较多也比较正常,于是就从线程信息里面找突破点。通过线程进行分析,先找到了几个正在运行的业务线程,然后逐一跟进业务线程看了下代码,找到产生大量对象的地方,比如数据库查询返回的列表。如果能够大致找到问题尽量根据业务逻辑采用修复代码的方式。

S5.2.内存泄漏

内存泄漏是内在病源,外在症状可能为:应用程序长时间连续运行时,可能性能严重下降。CPU使用率飙升,甚至到100%。频繁Full GC,各种报警,例如接口超时报警等。应用程序抛出OutOfMemoryError错误。应用程序偶尔会耗尽连接对象。严重的内存泄漏往往伴随着频繁的Full GC,所以分析排查内存泄漏问题首先还得从查看 Full GC 入手。

主要有以下操作步骤:使用 jps 查看运行的JAVA进程ID;使用 top -p pid 查看进程使用CPU和MEM的情况;使用 top -Hp pid 查看进程下所有线程占用CPU和MEM的情况;将线程 ID 转换为 16 进制:printf “%x\n” pid ,输出的值就是线程栈信息中的 nid。例如:printf “%x\n” 29471,换行输出 731f。抓取线程栈:jstack 29452 > 29452.txt,可以多抓几次做个对比。在线程栈信息中找到对应线程号的 16 进制值,如下是 731f 线程的信息。线程栈分析可使用 Visualvm 插件 TDA。使用 jstat -gcutil [pid] 5000 10 每隔 5 秒输出 GC 信息,输出 10 次,查看 YGC 和 Full GC 次数。通常会出现 YGC 不增加或增加缓慢,而 Full GC 增加很快。或使用 ** jstat -gccause [pid] 5000 ** ,同样是输出 GC 摘要信息。或使用 jmap -heap [pid] 查看堆的摘要信息,关注老年代内存使用是否达到阀值,若达到阀值就会执行 Full GC。如果发现 Full GC 次数太多,就很大概率存在内存泄漏了使用 **jmap -histo:live [pid] ** 输出每个类的对象数量,内存大小(字节单位)及全限定类名。生成 dump 文件,借助工具分析哪 个对象非常多,基本就能定位到问题在那了使用 jmap 生成 dump 文件:# jmap -dump:live,format=b,file=29471.dump 29471

dump 文件分析:可以使用 jhat 命令分析:jhat -port 8000 29471.dump,浏览器访问 jhat 服务,端口是 8000。通常使用图形化工具分析,如 JDK 自带的 jvisualvm,从菜单 > 文件 > 装入 dump 文件。或使用第三方式具分析的,如 JProfiler 也是个图形化工具,GCViewer 工具。Eclipse 或以使用 MAT 工具查看。或使用在线分析平台 GCEasy。注意:如果 dump 文件较大的话,分析会占比较大的内存。在 dump 文析结果中查找存在大量的对象,再查对其的引用。基本上就可以定位到代码层的逻辑了。

S5.3.内存飙高

先观察垃圾回收的情况。jstat -gc PID 1000 查看GC次数,每隔一秒打印一次。jmap -histo PID | head -20 查看堆内存占用空间最大的前20个对象类型,可初步查看是哪个对象占用了内存。如果每次GC次数频繁,而且每次回收的内存空间也正常,那是说明因为对象创建速度快导致内存一直占用很高;如果每次回收内存非常少,那么很可能是因为内存泄漏导致内存一直无法被回收。导出堆内存文件快照,jmap -dump:live,format=b,file=/home/myheapdump.hprof PID (dump堆内存信息到文件)。使用visualVM对dump文件进行离线解析,找到占用内存高的对象,再找到创建该对象的业务代码位置,从代码和业务场景中定位具体具体问题。

S5.4.频繁 minor gc

通常情况下,可能是新生代空间较小,Eden区很快被填满,导致频繁的minor gc;因此,可以调大年轻代空间(-Xmx),来降低minor gc次数。

S5.5.频繁full gc

清楚从程序角度,有哪些原因导致FGC。

1.大对象:系统一次性加载了过多数据到内存中(比如SQL查询未做分页),导致大对象进入了老年代。

2.内存泄漏:频繁创建了大量对象,但是无法被回收(比如IO对象使用完后未调用close方法释放资源),先引发FGC,最后导致OOM。

3.程序BUG

4.代码中显示调用了System.gc(),包括自己的代码和依赖jar包中的代码。

5.JVM参数设置问题:包括总内存大小、新生代和老年代大小、Eden区、S0区、S1区、元空间大小、垃圾回收算法等等。

清楚排查问题时使用哪些工具

公司的监控系统: 大部分公司都会用,可全方位监控JVM的各项指标。

JDK自带的工具,包括jmap、jstat等常用命令

可视化的堆内存分析工具:VisualVM、MAT等

排查指南

查看监控,以了解出现问题的时间点以及当前FGC的频率(对比正常频率)

了解该时间点之前有没有程序上线、基础组件升级等情况。

了解JVM的参数设置,包括:堆空间各个区域的大小设置,新生代和老年代分别采用了哪些垃圾收集器,然后分析JVM参数设置是否合理。

再对步骤1中列出的可能原因做排除法,其中元空间被打满、内存泄漏、代码显式调用gc方法比较容易排查。

针对大对象或者长生命周期对象导致的FGC,可通过 jmap -histo 命令并结合dump堆内存文件作进一步分析,需要先定位到可疑对象。

通过可疑对象定位到具体代码再次分析,这时候要结合GC原理和JVM参数设置,弄清楚可疑对象是否满足了进入到老年代的条件才能下结论。

S5.6.cpu飙高

先找出那个进程占用CPU高。top列出系统各个进程的资源占用情况。然后,根据找到对应进程里那个线程占用CPU高。top -Hp 进程ID 列出对应进程里线程占用资源情况。找到对应进程后,再打印出对应线程的堆栈信息。jstack PID 打印出进程的所有线程信息,从打印出来的线程信息中找到上一步转换为16进制的线程ID对应的线程信息。最后根据线程的堆栈信息定位到具体业务方法,从代码逻辑中找到问题所在。查看是否有线程长时间的waitting或blocked,如果线程长期出入waitting状态下,关注waitting on xxxxx,说明线程在等待这把锁,然后根据锁的地址找到持有锁的线程。

你可能感兴趣的:(JVM系列讲解,jvm,java,算法)