JVM性能调优实战笔记1
目录
公号:码农充电站pro
1,性能优化的步骤
性能优化步骤:
- 发现问题-性能监控
- GC 频繁、CPU 负载过高、OOM、内存泄漏、内存负载过高、死锁、程序响应缓慢
jvisualvm、jconsole
等工具
- 排查问题-性能分析
- 打印 GC 日志,通过 GCviewer 或者 http://gceasy.io 来分析
- 灵活使用
jstack、jmap、jinfo
等工具- 这些命令的字节码文件都在
C:\Program Files\Java\jdk1.8.0_211\lib\tools.jar
中
- 这些命令的字节码文件都在
- 使用
阿里 arthas、jconsole、jvisualvm
查看 jvm 状态 - 生成堆 dump 文件,使用 MemoryAnalyzer 或 Jprofiler 或 Jconsole 或 JvisualVm 或 jhat 来分析
- 解决问题-性能调优
- 适当增加内存,根据业务背景选择垃圾回收器
- 优化代码,控制内存使用
- 增加机器,分散节点压力
- 合理设置线程池线程数量
- 等。。
内存泄漏的几种情况:
- 静态集合类:如 HashMap、LinkedList 等
- 如果这些容器是静态的,则它们的生命周期与 JVM 程序一致,容器中的对象在程序结束前不能被释放
- 单例模式:单例的生命周期与 JVM 的生命周期一样长
- 各种连接:如数据库连接,网络连接、IO 连接等
- 如果连接没有正常关闭,则造成内存和系统资源浪费
- 变量不合理的作用域
- 下图中的 msg 变量只被 receiveMsg 方法用到,可将其移到方法内部
- 另外字符串及时置
null
,也可使字符串及时被回收
2,常用命令工具
1,jps:查看 Java 进程
jps:查看正在运行的 java 进程
jps
:显示进程 id 与类名jps -l
:显示进程 id 与全类名jps -m
:显示进程 id 与类名与程序参数jps -v
:显示 JVM 参数jps -q
:只显示进程 id- 如果启动 Java 进程时使用了
-XX:-UserPerfData
参数,那么 jps 将查看不到该进程
2,jstat:查看 JVM 统计信息
jstat:查看 JVM 统计信息,常用于检查垃圾回收与内存泄漏问题
命令格式:jstat -<option> [-t] [-h<lines>] <vmid> [<interval> [<count>]]
(使用时要注意参数的顺序)
-option
-t
:输出进程从开始执行到现在的时间(秒)-h<lines>
:间隔多少行输出一次标头信息(紧挨着,没空格)vmid
:指定进程 idinterval
:指定输出统计数据的间隔时间,单位毫秒count
:指定查询的总次数
查看 option
支持的选项:jstat -options
-class
:显示 ClassLoader 的相关信息:类的装载、卸载数量、总空间、类装载所消耗的时间等-gc
:显示与 GC 相关的堆信息:Eden 区、两个 Survivor 区、老年代、永久代等的容量、已用空间、GC时间合计等信息- S0C/S1C:幸存者区大小(单位字节)
- S0U/S1U:幸存者区已使用的大小
- EC/EU:Eden区大小 / Eden区已使用的大小
- OC/OU:老年代大小 / 老年代已使用的大小
- 如果老年代的使用比例越来越高,而无法降低,则有可能出现内存泄漏
- MC/MU:方法区大小 / 方法区已使用的大小
- CCSC/CCSU:压缩类空间大小 / 压缩类已使用的大小
- YGC/YGCT:进程从启动到目前的 young gc 的次数/消耗时间(秒)
- FGC/FGCT:进程从启动到目前的 full gc 的次数/消耗时间(秒)
- GCT:进程从启动到目前的 gc 的消耗时间(秒)
- 如果 GCT 时间占程序运行时间的 20%,则说明目前堆的压力较大
- 如果 GCT 时间占程序运行时间的 90%,则随时可能 OOM
-gccapacity
:显示内容与-gc
基本相同,但输出主要关注 Java 堆各个区域使用到的最大、最小空间-gcutil
:显示内容与-gc
基本相同,但输出主要关注已使用空间占总空间的百分比-gccause
:与-gcutil
功能一样,但会额外输出导致最后一次或当前正在发生的 GC 产生的原因-gcnew/-gcold
:显示新生代/老年代 GC 状况-gcnewcapcity/-gcoldcapacity
:与-gcnew/-gcold
基本相同,输出主要关注使用到的最大、最小空间-gcmetacapacity
:-compiler
:显示 JIT 编译器编译过的方法、耗时等信息-printcompilation
:输出已经被 JIT 编译的方法
3,jinfo:查看修改 JVM 配置参数
jinfo:实时查看和修改 JVM 配置参数
并非所有的参数都支持动态修改,只有标记为 manageable 的才能被实时修改
java -XX:+PrintFlagsFinal -version | grep manageable
java -XX:+PrintFlagsFinal
查看所有 JVM参数的最终值java -XX:+PrintFlagsInitial
查看所有 JVM参数的初始值
jinfo 参数:
-sysprops
:查看系统属性信息-flags
:查看所有赋过值的参数-flag name
:查看某个具体的参数的使用用情况-flag +/-name
:设置虚拟机参数-flag name=value
:设置虚拟机参数
4,jmap:导出内存映像文件
jmap:导出内存映像文件和内存使用情况
基本语法:jmap [option] <pid>
option 选项:
-dump:<dump-options>
:生成堆转储文件dump:live
:只保存堆中的活跃对象dump:fomat=b
dump:file=<file_path>
-heap
:输出整个堆空间的详细信息,包括 GC 的使用,对配置信息,以及内存的使用信息等-histo[:live]
:输出堆中对象的同级信息,包括类、实例数量和合计容量-histo:live
只统计堆中的存活对象
-permstat
:以 ClassLoader 为统计口径输出永久代的内存状态信息-finalizerinfo
:显示在 F-Queue 中等待 Finalizer 线程执行 finalize 方法的对象-F
:当虚拟机进程对 -dump 选项没有任何响应时,强制生成 dump 文件
导出内存映像文件的两种方式:
- 使用 jmap:
jmap -dump:format=b,file=<file_name.hprof> <pid>
jmap -dump:live,format=b,file=<file_name.hprof> <pid>
生成的文件更小,一般情况使用此命令较多
- 在进程发生 OOM 时,自动生成堆 dump 文件
-XX:+HeapDumpOnOutOfMemoryError -XX:HeapDumpPath=<file_name.hprof>
5,jhat:分析堆转储文件
jhat:堆转储文件分析工具。
jhat file_name.hprof
jhat 内置了一个 HTTP 服务器,生成分析结果后,用户可以在浏览器中查看分析结果。jhat 会启动一个 http 服务器,端口是 7000,即 http://localhost:7000
,用浏览器访问即可。
该命令在 jdk9 及其之后被移除,官方建议使用 jvisualvm 代替 jhat
6,jstack:查看线程快照
jstack:查看 JVM 中线程快照。常用于分析线程长时间卡顿问题,如线程死锁,死循环,请求外部资源导致长时间等待等问题。
需要关注的几种线程状态:
- 死锁:Deadlock(重点关注)
- 等待资源:Waiting on condition(重点关注)
- 等待获取监视器:Waiting on monitor entry(重点关注)
- 阻塞:Blocked(重点关注)
- 执行中:Runnable
- 暂停:Suspended
- 对象等待中:TIMED_WATING / WATING
- 停止:Parked
基本语法:jstack [option] <pid>
jstack pid
:直接查看线程栈信息jstack -F pid
:强制输出线程栈信息jstack -l pid
:除堆栈外,显示锁的附加信息jstack -m pid
:如果调用到本地方法,显示 C/C++ 堆栈信息
7,jcmd:多功能命令行
jcmd 可以用来实现上面除了 jstat 之外所有的功能,比如:导出堆,内存使用,查看进程,导出线程信息,执行 GC,JVM 运行时间等。
命令示例:
# 查看某个 Java 进程的所有支持的命令替换
jcmd <pid> help
---------------
25801:
The following commands are available:
JFR.stop
JFR.start
JFR.dump
JFR.check
VM.native_memory
VM.check_commercial_features
VM.unlock_commercial_features
ManagementAgent.stop
ManagementAgent.start_local
ManagementAgent.start
VM.classloader_stats
GC.rotate_log
Thread.print # 打印线程栈
GC.class_stats
GC.class_histogram # class 直方图
GC.heap_dump # 导出堆dump文件
GC.finalizer_info
GC.heap_info
GC.run_finalization
GC.run
VM.uptime # 显示进程执行的总时间
VM.dynlibs
VM.flags # 显示虚拟机参数配置信息
VM.system_properties # 显示系统属性信息
VM.command_line
VM.version # 打印 jdk 版本
help
3,图形化分析工具
JVM 常用图形化分析工具:
- jdk 自带工具
- jconsole:基于 JMX
- jvisualvm
- jmc
- 第三方工具
- MemoryAnalyzer (MAT):主要用于分析堆 dump 文件
- Jprofiler(商业付费)
- Arthas(阿里出品-命令行工具)
- Btrace
4,JVM 运行时参数
1,JVM 参数选项的类型
JVM 参数选项的类型:
- 标准参数选项:以
-
开头,运行java -help
可见- 参数比较稳定,基本不变动
- -X 参数选项(非标准参数):以
-X
开头,运行java -X
可见- 参数变动小
-Xmixed
:JIT 变异模式,混合模式,默认模式-Xint
:JIT 变异模式,只是用解释器,所有字节码被解释执行,速度较慢-Xmx
:最大堆内存- 等价于
-XX:MaxHeapSize
- 单位
g/G,m/M,k/K
- 等价于
-Xms
:初始堆内存,最好与-Xmx
一样,避免扩容带来的损耗- 等价于
-XX:InitialHeapSize
- 等价于
-Xss
:线程栈大小- 等价于
-XX:ThreadStackSize
- 等价于
- -XX 参数选项(非标准参数):以
-XX
开头,运行-XX:PrintFlagsFinal
显示所有参数- 实验性,参数变动大
- 布尔类型:
-XX:+<option>
:启用某参数-XX:-<option>
:禁用某参数
- 非布尔类型:
-XX:<name>=<value>
2,打印及设置 JVM 参数
参数 | 说明 |
---|---|
-XX:+PrintCommandLineFlags | 打印用户手动设置或JVM自动设置的参数 |
-XX:+PrintFlagsInitial | 打印所有 XX 选项的默认值 |
-XX:+PrintFlagsFinal | 打印 XX 选项的生效值 |
-XX:+PrintVMOptions | 打印 JVM 参数 |
3,设置堆、栈、方法区等大小
堆是GC 执行垃圾回收的重点区域。在方法结束后,堆中的对象不会马上被回收,而是在垃圾收集的时候才会被回收。
运行时数据区结构图:
JVM 堆空间分配:
- Java 7 及之前:
- 新生代(Young):Eden、S0(From)、S1(To)
- 老年代(Old)
- 永久区(Perm)
- Java 8 及之后
- 新生代(Young):IBM研究表明,新生代中的80% 的对象,在经过 GC 之后,都会被回收
- Eden:对象第一次创建时(
new
)的区域,如果Eden 放不下,则会直接在 Old 区创建 - S0(From)
- S1(To)
- S0 与 S1 谁空谁是 To
- Eden:对象第一次创建时(
- 老年代(Old)
- 元空间(Meta)
- 新生代(Young):IBM研究表明,新生代中的80% 的对象,在经过 GC 之后,都会被回收
堆区中 S1 和 S2 两块区域,同一时刻只会有一块区域使用,另一块是空闲状态。
分代的目的就是为了优化 GC 的性能
对象从新生代到老年代的过程:
- new 的对象先放入 Eden 区
- 当 Eden 区满,再创建对象时,垃圾回收器将对 Eden 区进行垃圾回收(Minor GC);Eden 区中不再被引用的对象将被销毁,Eden 区中剩余的对象移动到 S0 区。新 new 的对象依然放入 Eden 区
- 再次发生GC 时,S0 中幸存的对象将放入 S1中,
- 再次发生GC 时,S1 中幸存的对象将放入 S0中,如此 S0 与 S1 交替往复
- 当达到一定次数后,对象将被放入 Old 区
- 该次数可通过参数 -XX:MaxTenuringThreshold 设置,默认 15
- 当 Old 区空间不足时,会发生 Major GC,清理 Old 区对象
- Major GC 会比 Minor GC 慢 10 倍以上
- 若发生 Major GC 后,Old 空间依然不足,将发生 OOM 异常
只有当 Eden 区满了才会触发 Minor GC,Minor GC 会将 Eden 区和 From 区一起回收 如果 To 区满(不会触发 GC),会直接将对象放入 Old 区
发生 GC 时,将 Stop The World
使用 jstat 命令可查看堆中各个区域的空间:
jstat -gc pid
堆空间分配:
区域 | 大小 | 比例 | |
新生代 | Eden | 153600 | 6 |
S0(From) | 25600 | 1 | |
S1(To) | 25600 | 1 | |
老年代 | Old | 409600 | 16 |
参数 | 说明 |
---|---|
-Xss/-XX:ThreadStackSize | 设置线程栈大小 |
-Xms/-XX:InitialHeapSize | 设置JVM初始堆大小,默认是机器内存的 1/64 |
-Xmx/-XX:MaxHeapSize | 设置JVM最大堆大小,默认是机器内存的 1/4,当实际堆使用量大于此值时,将发生 OOM(一般不设置) |
-Xmn/-XX:NewSize,-XX:MaxNewSize | 设置年轻代初始值/最大值,官方推荐配置为堆大小的3/8 (一般不设置) |
-XX:SurvivorRatio | 设置年轻代中 Eden区与一个Survior区的比值,默认为 8,8:1:1 ,实际上真正看内存的话是 6:1:1 (一般不设置) |
-XX:+UseAdaptiveSizePolicy | 自动选择各区大小比例,默认开启 (一般不设置) |
-XX:NewRatio | 设置老年代与年轻代(1个Eden区和2个Survivor区)的比值,默认为 2,2:1 (一般不设置) |
-XX:PretenureSizeThreadshould | 让大于此值的对象直接分配在老年代,单位字节,只对 Serial、ParNew 收集器有效 |
-XX:MaxTenuringThreshold | 新生代每次MinorGC后,还存活的对象年龄+1,当对象的年龄大于此值时就进入老年代,默认为 15 |
-XX:+PrintTenuringDistribution | 让JVM在每次MinorGC后打印出当前使用的Survivor中对象的年龄分布 |
-XX:TargeSurvivorRatio | 设置MinorGC结束后,Survivor 区域中占用空间的期望比例 |
-XX:PermSize | 设置方法区永久代初始值 |
-XX:MaxPermSize | 设置方法区永久代最大值 |
-XX:MetaspaceSize | 设置方法区元空间初始值 |
-XX:MaxMetaspaceSize | 设置方法区元空间最大值 |
-XX:+UseCompressedOops | 使用压缩对象指针 |
-XX:+UseCompressedClassPointers | 是引用压缩类指针 |
-XX:CompressedClassSpaceSize | 设置Klass Metaspace 的大小,默认 1 G |
-XX:MaxDirectMemorySize | 指定直接内存容量,默认与堆最大值一样 |
4,内存溢出相关参数
参数 | 说明 |
---|---|
-XX:+HeapDumpOnOutMemoryError | 在内存出现OOM时,生成堆转储文件 |
-XX:+HeapDumpBeforeFullGC | 在出现FullGC时,生成堆转储文件,有几次FullGC,就生成几个文件 |
-XX:HeapDumpPath | 设置堆转储文件路径 |
-XX:OnOutOfMemoryError | 设置一个可执行程序或脚本路径,当发生OOM是,执行它 |
5,垃圾收集器相关参数
Java 垃圾收集器:
- SerialGC:HotSpot 中 Client模式下的默认新生代垃圾收集器
- 串行收集器,可获得最高的单线程收集效率
- SerialOldGC:HotSpot 中 Client模式下的默认老年代垃圾收集器
- ParNewGC:SerialGC 的并行版本(不重要)
- ParallelGC:并行收集器,主打吞吐量(jdk 8 默认开启)
- ParallelOldGC:老年代 GC
- CMS 收集器:并发收集器,主打低延迟(未来将被丢弃)
- 使用标记清除算法
- G1 收集器:主打低延迟
如何选择垃圾收集器:
- 优先调整堆的大小,让JVM自适应完成
- 如果内存小于 100M,使用串行收集器
- 如果是单核,单机程序,且没有停顿时间的要求,使用串行收集器
- 如果是多核CPU,需要高吞吐量,允许停顿时间超过 1 秒,选择并行或者 JVM 自己选择
- 如果是多核CPU,追求低停顿时间,需快速响应,使用并发收集器,官方推荐 G1,性能高
- 现在互联网的项目,基本都是 G1
ParallelGC 垃圾收集器相关参数:
CMS 垃圾收集器相关参数:
补充参数:
特别说明:
G1 垃圾收集器:
6,GC 日志相关参数
参数 | 说明 |
---|---|
-verbose:gc/-XX:+PrintGC | 输出GC日志信息 |
-XX:+PrintGCDetails | 在发生垃圾回收时打印内存回收详细日志,并在进程退出时输出当前内存各区域的分配情况 |
-XX:+PrintGCTimeStamps | 程序启动到GC发生的时间秒数,需配合 PrintGCDetails 使用 |
-XX:+PrintGCDateStamps | 输出GC发生时的时间戳,需配合 PrintGCDetails 使用 |
-XX:+PrintHeapAtGC | 每次GC前和GC后,都打印堆信息 |
-Xloggc:<file> |
将GC日志写入文件中 |
-XX:TraceClassLoading | 监控类的加载 |
-XX:PrintGCApplicationStoppedTime | 打印GC时线程的停顿时间 |
-XX:+PrintGCApplicationConcurrentTime | 垃圾收集之前,打印应用未中断的执行时间 |
-XX:+PrintReferenceGC | 记录回收了多少种不同引用类型的引用 |
-XX:+PrintTenuringDistribution | 每次MinorGC后打印出当前使用的Survivor中对象的年龄分布 |
-XX:+UseGCLogFileRotation | 启用GC日志文件的自动转储 |
-XX:NumberOfGCLogFiles | GC日志文件的循环数目 |
-XX:GCLogFileSize | 控制GC日志文件的大小 |
7,其它参数
参数 | 含义 |
---|---|
-XX:+DisableExplicitGC | 禁用hotspot 执行 System.gc(),默认禁用 |
-XX:ReservedCodeCacheSize, -XX:InitialCodeCacheSize | 指定代码缓存大小 |
-XX:+UseCodeCacheFlushing | 让JVM放弃一些被编译的代码,避免代码缓存被占满时JVM切换到 interpreted-only 的情况 |
-XX:+DoEscapeAnalysis | 开启逃逸分析 |
-XX:+UseBiasedLocking | 开启偏向锁 |
-XX:+UseLargePages | 开启使用大页面 |
-XX:+PrintTLAB | 打印TLAB的使用情况 |
-XX:TLABSize | 设置TLAB大小 |
5,GC 日志分析
对于 HotSpot VM,它的GC 按照回收区域又分为两大类:
- 部分收集(Partial GC):非整堆收集,其中又分为:
- 新生代收集(Minor GC/Young GC):只是新生代(Eden\S0,S1)的垃圾收集
- 当 Eden 区满的时候就会进行新生代收集
- 新生代收集比老年代收集更加简单,快速,频繁
- 老年代收集(Minor GC/Old GC):只是老年代的垃圾收集
- 目前,只有 CMS GC 会有单独收集老年代的行为
- 进行老年代收集之前会先进行一次年轻代收集,原因如下:
- 一个比较大的对象无法放入新生代,那它自然会往老年代去放,如果老年代也放不下,那会先进行一次新生代收集,
- 之后尝试往新生代放,如果还是放不下,才会进行老年代的垃圾收集,然后往老年代去放
- 混合收集(Mixed GC):收集整个新生代以及部分老年代
- 目前,只有 G1 GC 会有这种行为
- 新生代收集(Minor GC/Young GC):只是新生代(Eden\S0,S1)的垃圾收集
- 整堆收集(Full GC):收集整个 java 堆和方法区
- 堆包含新生代,老年代,元空间/永久代
- 哪些情况会触发Full GC:
- 老年代空间不足
- 方法区空间不足
- 调用 System.gc()
- Minor GC 进入老年代的数据的平均大小大于老年代的可用空间
- 大对象直接进入老年代,而老年代的空间不足
文章作者 @码农加油站
上次更改 2022-03-15