前文笔者通过四篇文章,包括内存结构,类加载器,执行引擎,垃圾回收器,具体讲解了JVM的基本知识。而这些知识的真正用途,笔者相信大多数人都不是为了去实现自己的JVM。更多的是为了对JVM进行调优。本文将具体的讲解JVM调优的那些事。
JVM调优是指通过优化Java虚拟机(JVM)来提高Java应用程序的性能和效率的过程。JVM是Java应用程序的运行时环境,包括内存结构,类加载器,执行引擎,垃圾回收器等组件,它们会对应用程序的性能产生影响。
进行JVM调优的目的就是优化这些组件的配置、调整它们的参数、降低开销和延迟,从而提高应用程序的性能和可靠性。在实践中,JVM调优通常涵盖深入理解Java虚拟机,监测应用程序行为,识别性能瓶颈,调整垃圾收集器和堆内存大小等方面。
JVM参数格式一般由"-"加参数名称,后面再跟参数的值组成。
在使用JVM参数的时候,可以分为以下几类:
标准参数(-开头):是所有JVM实现都必须支持的参数,一般是用来控制JVM的基本行为,如 -help、-version 等。
非标准参数(-X开头):是所有JVM实现选做的参数,一般是用于改变JVM的一些默认实现行为,如 -Xms、-Xmx 等。
高级参数(-XX:开头):是所有JVM实现特有的参数,一般是用于控制JVM的高级行为,比如改变JVM垃圾收集器的策略,或者改变JIT编译策略等。
具体例子:
java -Xmx3550m -Xms3550m -Xmn2g -Xss128k -XX:NewSize=256m
指令 |
作用 |
例子 |
-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= |
-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 |
指令 |
作用 |
例子 |
-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=[路径] |
指定堆内存溢出时打印目录 |
指令 |
作用 |
例子 |
-Xint |
完全解释器模式执行 |
|
-Xcomp |
完全采用即时编译器执行,如果即时编译器出现问题,解释器会介入执行 |
|
-Xmixed |
混合模式 |
|
-client |
指定虚拟机在Client模式下运行(使用Client即时编译器) |
|
-server |
指定虚拟机在Server模式下运行(使用Server即时编译器) |
指令 |
作用 |
例子 |
-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 |
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编译的方法信息
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]
jstack命令用于打印指定Java进程、核心文件或远程调试服务器的Java线程堆栈的跟踪信息。换句话说,就是jstack能生成JVM当前时刻的线程快照,以此来定位线程出现长时间停顿的原因。指令格式:jstack [-option]
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。
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
笔者认为JVM调优的基本思路就是,启动时尽量根据使用的硬件性能配置好参数,能不进行JVM调优就不进行JVM调优,绝绝绝绝大部分的问题都应该通过优化代码解决。JVM调优应该是最最最最后的选择,JVM调优需要前期严谨的分析和监控。所以JVM调优一般是伴随着一定的问题,那么要做的就是定位问题和解决问题。
通常情况下我们都是通过jvm调优工具对,吞吐量、延迟和内存占用三个指标进行监测,初步估计问题所在,然后再深入对某些特定指标进行监测分析。这三个指标即使定位问题原因的参考,也是问题的表现。通过对这三个指标的分析一般可以大致分析出问题。
这三个属性中,其中一个任何一个属性性能的提高,几乎都是以另外一个或者两个属性性能的损失作代价,不可兼得,具体某一个属性或者两个属性的性能对应用来说比较重要,要基于应用的业务需求来确定。经常遇到的问题大致有内存溢出、内存泄漏、占用CPU过高、内存飙高、频繁 minor gc、频繁full gc等问题,大多数问题都不会单独出现。
首先,可以简单简单的认为就是堆内存不足导致,单方面的加大了堆内存-Xms
如果问题依然没有解决,只能从堆内存信息下手,通过开启了-XX:+HeapDumpOnOutOfMemoryError参数 获得堆内存的dump文件。用堆分析工具对dump文件进行分析,通过堆分析工具查看到占用内存最大的对象,跟踪对象找到其引用的地方。
如果对象占用比较多也比较正常,于是就从线程信息里面找突破点。通过线程进行分析,先找到了几个正在运行的业务线程,然后逐一跟进业务线程看了下代码,找到产生大量对象的地方,比如数据库查询返回的列表。如果能够大致找到问题尽量根据业务逻辑采用修复代码的方式。
内存泄漏是内在病源,外在症状可能为:应用程序长时间连续运行时,可能性能严重下降。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 文析结果中查找存在大量的对象,再查对其的引用。基本上就可以定位到代码层的逻辑了。
先观察垃圾回收的情况。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文件进行离线解析,找到占用内存高的对象,再找到创建该对象的业务代码位置,从代码和业务场景中定位具体具体问题。
通常情况下,可能是新生代空间较小,Eden区很快被填满,导致频繁的minor gc;因此,可以调大年轻代空间(-Xmx),来降低minor 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参数设置,弄清楚可疑对象是否满足了进入到老年代的条件才能下结论。
先找出那个进程占用CPU高。top列出系统各个进程的资源占用情况。然后,根据找到对应进程里那个线程占用CPU高。top -Hp 进程ID 列出对应进程里线程占用资源情况。找到对应进程后,再打印出对应线程的堆栈信息。jstack PID 打印出进程的所有线程信息,从打印出来的线程信息中找到上一步转换为16进制的线程ID对应的线程信息。最后根据线程的堆栈信息定位到具体业务方法,从代码逻辑中找到问题所在。查看是否有线程长时间的waitting或blocked,如果线程长期出入waitting状态下,关注waitting on xxxxx,说明线程在等待这把锁,然后根据锁的地址找到持有锁的线程。