3

JVM性能调优实战笔记1

 2 years ago
source link: https://codeshellme.github.io/2022/03/jvm1/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

公号:码农充电站pro

主页:https://codeshellme.github.io

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 文件,使用 MemoryAnalyzerJprofilerJconsoleJvisualVmjhat 来分析
  • 解决问题-性能调优
    • 适当增加内存,根据业务背景选择垃圾回收器
    • 优化代码,控制内存使用
    • 增加机器,分散节点压力
    • 合理设置线程池线程数量

内存泄漏的几种情况:

  • 静态集合类:如 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:指定进程 id
  • interval:指定输出统计数据的间隔时间,单位毫秒
  • 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 版本

3,图形化分析工具

JVM 常用图形化分析工具:

  • jdk 自带工具
    • jconsole:基于 JMX
    • jvisualvm
  • 第三方工具
    • 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
    • 老年代(Old)
    • 元空间(Meta)

堆区中 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

堆空间分配:

区域 大小比例 新生代Eden1536006 S0(From)256001 S1(To)256001 老年代Old40960016

参数 说明

-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 会有这种行为
  • 整堆收集(Full GC):收集整个 java 堆和方法区
    • 堆包含新生代,老年代,元空间/永久代
    • 哪些情况会触发Full GC:
      • 老年代空间不足
      • 方法区空间不足
      • 调用 System.gc()
      • Minor GC 进入老年代的数据的平均大小大于老年代的可用空间
      • 大对象直接进入老年代,而老年代的空间不足

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK