java

1、内存调优

监控 Java 内存的常用工具(学习使用 VisualVM 查看内存)

内存泄漏的常见场景(大型应用常见内存泄漏案例分析)

内存泄漏的解决方案(几大常见的内存泄漏问题原因分析及解决思路)

:::

内存溢出和内存泄漏

内存泄漏(memory leak)

内存泄漏绝大多数情况都是由堆内存泄漏引起的,所以后续没有特别说明则讨论的都是堆内存泄漏。

少量的内存泄漏可以容忍,但是如果发生持续的内存泄漏,就像滚雪球雪球越滚越大,不管有多大的内存迟早会被消耗完,最终导致的结果就是内存溢出

但是产生内存溢出并不是只有内存泄漏这一种原因。

内存泄漏的常见场景

内存泄漏导致溢出的常见场景是大型的 Java 后端应用中,在处理用户的请求之后,没有及时将用户的数据删除。随着用户请求数量越来越多,内存泄漏的对象占满了堆内存最终导致内存溢出。

这种产生的内存溢出会直接导致用户请求无法处理,影响用户的正常使用。重启可以恢复应用使用,但是在运行一段时间之后依然会出现内存溢出。

第二种常见场景是分布式任务调度系统如 Elastic-job、Quartz 等进行任务调度时,被调度的 Java 应用在调度任务结束中出现了内存泄漏,最终导致多次调度之后内存溢出。

这种产生的内存溢出会导致应用执行下次的调度任务执行。同样重启可以恢复应用使用,但是在调度执行一段时间之后依然会出现内存溢出。

解决内存溢出的方法

解决内存溢出的思路

解决内存溢出的步骤总共分为四个步骤,其中前两个步骤是最核心的:

发现问题

1)发现问题(Top 命令)

进程使用的内存为 RES(常驻内存)- SHR(共享内存)

优点:操作简单、无额外的软件安装

缺点:只能查看最基础的进程信息,无法查看到每个部分的内存占用(堆、方法区、堆外)

2)发现问题(VisualVM)

这款软件在 Oracle JDK 6~8 中发布,但是在 Oracle JDK 9 之后不在 JDK 安装目录下需要单独下载。

下载地址:https://visualvm.github.io/

优点:

  • 功能丰富,实时监控 CPU、内存、线程等详细信息
  • 支持 Idea 插件,开发过程中也可以使用

缺点:对大量集群化部署的 Java 进程需要手动进行管理

3)发现问题(Arthas)

优点:

  • 功能强大,不止于监控基础的信息,还能监控单个方法的执行耗时等细节内容。
  • 支持应用的集群管理

缺点:部分高级功能使用门槛较高

案例:使用阿里 arthas tunnel 管理所有的需要监控的程序

小李的团队已经普及了 arthas 的使用,但是由于使用了微服务架构,生产环境上的应用数量非常多,使用 arthas 还得登录到每一台服务器上再去操作非常不方便。他看到官方文档上可以使用 tunnel 来管理所有需要监控的程序。

步骤:

  1. 在 Spring Boot 程序中添加 arthas 的依赖(支持 Spring Boot2),在配置文件中添加 tunnel 服务端的地址,便于 tunnel 去监控所有的程序。
  2. 将 tunnel 服务端程序部署在某台服务器上并启动。
  3. 启动 Java 程序
  4. 打开 tunnel 的服务端页面,查看所有的进程列表,并选择进程进行 arthas 的操作。

**4)发现问题(Prometheus + Grafana)**

Java 程序员要学会如何读懂 Grafana 展示的 Java 虚拟机相关的参数。

优点:

  • 支持系统级别和应用级别的监控,比如 linux 操作系统、Redis、MySQL、Java 进程。
  • 支持告警并允许自定义告警指标,通过邮件、短信等方式尽早通知相关人员进行处理。

缺点:环境搭建较为复杂,一般由运维人员完成。

**5)发现问题:堆内存状况的对比**

  • 处理业务时会出现上下起伏,业务对象频繁创建内存会升高,触发 MinorGC 之后内存会降下来。
  • 手动执行 FULL GC 之后,内存大小会骤降,而且每次降完之后的大小是接近的。
  • 长时间观察内存曲线应该是在一个范围内。

出现内存泄漏:

  • 处于持续增长的情况,即使 Minor GC 也不能把大部分对象回收
  • 手动 FULL GC 之后的内存量每一次都在增长
  • 长时间观察内存曲线持续增长

**内存溢出的两个原因**

产生内存溢出原因一 :代码中的内存泄漏

案例 1:equals() 和 hashCode() 导致的内存泄漏

问题:在定义新类时没有重写正确的 equals() 和 hashCode() 方法。在使用 HashMap 的场景下,如果使用这个类对象作为 key,HashMap 在判断 key 是否已经存在时会使用这些方法,如果重写方式不正确,会导致相同的数据被保存多份。

正常情况:

1、以 JDK8 为例,首先调用 hash 方法计算 key 的哈希值,hash 方法中会使用到 key 的 hashcode 方法。根据 hash 方法的结果决定存放的数组中位置。

2、如果没有元素,直接放入。如果有元素,先判断 key 是否相等,会用到 equals 方法,如果 key 相等,直接替换 value;key 不相等,走链表或者红黑树查找逻辑,其中也会使用 equals 比对是否相同。

异常情况:

1、hashCode 方法实现不正确,会导致相同 id 的学生对象计算出来的 hash 值不同,可能会被分到不同的槽中。

2、equals 方法实现不正确,会导致 key 在比对时,即便学生对象的 id 是相同的,也被认为是不同的 key。

3、长时间运行之后 HashMap 中会保存大量相同 id 的学生数据。

解决方案:

1、在定义新实体时,始终重写 equals() 和 hashCode() 方法。

2、重写时一定要确定使用了唯一标识去区分不同的对象,比如用户的 id 等。

3、HashMap 使用时尽量使用编号 id 等数据作为 key,不要将整个实体类对象作为 key 存放。

案例 2:内部类引用外部类

问题:

1、非静态的内部类默认会持有外部类,尽管代码上不再使用外部类,所以如果有地方引用了这个非静态内部类,会导致外部类也被引用,垃圾回收时无法回收这个外部类。

2、匿名内部类对象如果在非静态方法中被创建,会持有调用者对象,垃圾回收时无法回收调用者。

解决方案:

1、这个案例中,使用内部类的原因是可以直接获取到外部类中的成员变量值,简化开发。如果不想持有外部类对象,应该使用静态内部类。

2、使用静态方法,可以避免匿名内部类持有调用者对象。

案例 3:ThreadLocal 的使用

问题:

如果仅仅使用手动创建的线程,就算没有调用 ThreadLocal 的 remove 方法清理数据,也不会产生内存泄漏。因为当线程被回收时,ThreadLocal 也同样被回收。但是如果使用线程池就不一定了。

解决方案:

线程方法执行完,一定要调用 ThreadLocal 中的 remove 方法清理对象。

案例 4:String 的 intern 方法

问题:

JDK6 中字符串常量池位于堆内存中的 Perm Gen 永久代中,如果不同字符串的 intern 方法被大量调用,字符串常量池会不停的变大超过永久代内存上限之后就会产生内存溢出问题。

解决方案:

1、注意代码中的逻辑,尽量不要将随机生成的字符串加入字符串常量池。

2、增大永久代空间的大小,根据实际的测试/估算结果进行设置<font style="color:#117cee;">-XX:MaxPermSize=256M</font>

案例 5:通过静态字段保存对象

问题:

如果大量的数据在静态变量中被长期引用,数据就不会被释放,如果这些数据不再使用,就成为了内存泄漏。

解决方案:

1、尽量减少将对象长时间的保存在静态变量中,如果不再使用,必须将对象删除(比如在集合中)或者将静态变量设置为 null。

2、使用单例模式时,尽量使用懒加载,而不是立即加载。

3、Spring 的 Bean 中不要长期存放大对象,如果是缓存用于提升性能,尽量设置过期时间定期失效。

案例 6:资源没有正常关闭

问题:

连接和流这些资源会占用内存,如果使用完之后没有关闭,这部分内存不一定会出现内存泄漏,但是会导致 close 方法不被执行。

解决方案:

1、为了防止出现这类的资源对象泄漏问题,必须在 finally 块中关闭不再使用的资源。

2、从 Java 7 开始,使用 try-with-resources 语法可以用于自动关闭资源。

产生内存溢出原因二 :并发请求问题

并发请求问题指的是用户通过发送请求向 Java 应用获取数据,正常情况下 Java 应用将数据返回之后,这部分数据就可以在内存中被释放掉。

但是由于用户的并发请求量有可能很大,同时处理数据的时间很长,导致大量的数据存在于内存中,最终超过了内存的上限,导致内存溢出。这类问题的处理思路和内存泄漏类似,首先要定位到对象产生的根源。

**使用 Jmeter 模拟并发请求**

Apache Jmeter 是一款开源的测试软件,使用 Java 语言编写,最初是为了测试 Web 程序,目前已经发展成支持数据库、消息队列、邮件协议等不同类型内容的测试工具。

Apache Jmeter 支持插件扩展,生成多样化的测试结果。

案例:使用 Jmeter 进行并发测试,发现内存溢出问题

小李的团队发现有一个微服务在晚上 8 点左右用户使用的高峰期会出现内存溢出的问题,于是他们希望在自己的开发环境能重现类似的问题。

步骤:

  1. 安装 Jmeter 软件,添加线程组。
  2. 在线程组中增加 Http 请求,添加随机参数。
  3. 在线程组中添加监听器 – 聚合报告,用来展示最终结果。
  4. 启动程序,运行线程组并观察程序是否出现内存溢出。

诊断问题

**诊断 – 内存快照**

内存快照

生成内存快照的 Java 虚拟机参数:

<font style="color:#117CEE;">-XX:+HeapDumpOnOutOfMemoryError</font>:发生 OutOfMemoryError 错误时,自动生成 hprof 内存快照文件。

<font style="color:#117CEE;">-XX:HeapDumpPath=<path></font>:指定 hprof 文件的输出路径。

使用 MAT 打开 hprof 文件,并选择内存泄漏检测功能,MAT 会自行根据内存快照中保存的数据分析内存泄漏的根源。

**MAT 内存泄漏检测的原理**

MAT 内存泄漏检测的原理 – 支配树

MAT 提供了称为支配树(Dominator Tree)的对象图。支配树展示的是对象实例间的支配关系。在对象引用图中,所有指向对象 B 的路径都经过对象 A,则认为对象 A 支配对象 B。

MAT 内存泄漏检测的原理 – 深堆和浅堆

支配树中对象本身占用的空间称之为 浅堆(Shallow Heap)。

支配树中对象的子树就是所有被该对象支配的内容,这些内容组成了对象的 深堆(Retained Heap),也称之为保留集(Retained Set )。

深堆的大小表示该对象如果可以被回收,能释放多大的内存空间。

MAT 内存泄漏检测的原理

MAT 就是根据支配树,从叶子节点向根节点遍历,如果发现深堆的大小超过整个堆内存的一定比例阈值,就会将其标记成内存泄漏的“嫌疑对象”。

分析超大堆的内存快照

下载地址:https://eclipse.dev/mat/downloads.php

通过 MAT 中的脚本生成分析报告:

./ParseHeapDump.sh 快照文件路径 org.eclipse.mat.api:suspects
org.eclipse.mat.api:overview org.eclipse.mat.api:top_components
注意:默认 MAT 分析时只使用了 1G 的堆内存,如果快照文件超过 1G,需要修改 MAT 目录下的 MemoryAnalyzer.ini 配置文件调整最大堆内存。

修复问题

修复内存溢出问题的要具体问题具体分析,问题总共可以分成三类:

1、代码中的内存泄漏

代码中的内存泄漏在前面的篇章中已经介绍并提供了解决方案、

2、并发引起内存溢出 – 设计不当

系统的方案设计不当,比如:

  • 从数据库获取超大数据量的数据
  • 线程池设计不当
  • 生产者-消费者模型,消费者消费性能问题

解决方案:优化设计方案

3、并发引起内存溢出 - 参数不当

由于参数设置不当,比如堆内存设置过小,导致并发量增加之后超过堆内存的上限。

解决方案:调整参数,下一章中详细介绍

诊断和解决问题的两种方案对比

生成内存快照并分析

在线定位问题

1)arthas

步骤:

1、使用 jmap -histo:live 进程 ID > 文件名 命令将内存中存活对象以直方图的形式保存到文件中,这个过程会影响用户的时间,但是时间比较短暂。

2、分析内存占用最多的对象,一般这些对象就是造成内存泄漏的原因。

3、使用 arthas 的 stack 命令,追踪对象创建的方法被调用的调用路径,找到对象创建的根源。也可以使用 btrace 工具编写脚本追踪方法执行的过程。

2)btrace

BTrace 是一个在 Java 平台上执行的追踪工具,可以有效地用于线上运行系统的方法追踪,具有侵入性小、对性能的影响微乎其微等特点。

项目中可以使用 btrace 工具,打印出方法被调用的栈信息。

使用方法:

1、下载 btrace 工具

官方地址:https://github.com/btraceio/btrace/releases/latest

2、编写 btrace 脚本,通常是一个 Java 文件。

3、将 btrace 工具和脚本上传到服务器,在服务器上运行 btrace 进程 ID 脚本文件名 。

4、观察执行结果。

内存溢出总结

1、什么是内存溢出,什么是内存泄漏?

内存泄漏 内存泄漏 内存溢出 OutOfMemory

2、内存溢出有哪几种产生的原因?

1)持续的内存泄漏:内存泄漏持续发生,不可被回收同时不再使用的内存越来越多,就像滚雪球雪球越滚越大,最终内存被消耗完无法分配更多的内存取使用,导致内存溢出。

2)并发请求问题:用户通过发送请求向 Java 应用获取数据,正常情况下 Java 应用将数据返回之后,这部分数据就可以在内存中被释放掉。但是由于用户的并发请求量有可能很大,同时处理数据的时间很长,导致大量的数据存在于内存中,最终超过了内存的上限,导致内存溢出。

3、解决内存泄漏问题的方法是什么?

1)发现问题,通过监控工具尽可能尽早地发现内存慢慢变大的现象。

2)诊断原因,通过分析内存快照或者在线分析方法调用过程,诊断问题产生的根源,定位到出现问题的源代码。

3)修复源代码中的问题,如代码 bug、技术方案不合理、业务设计不合理等等。

4)在测试环境验证问题是否已经解决,最后发布上线。

2、GC 调优

GC 调优的核心分成三部分:

1、通用 JVM 参数的设置。

2、特定垃圾回收器的 JVM 参数的设置。

3、解决由频繁的 FULL GC 引起的程序性能问题。

GC 调优没有没有唯一的标准答案,如何调优与硬件、程序本身、使用情况均有关系,重点学习调优的工具和方法。

GC 调优的核心指标

1、吞吐量

2、延迟

3、内存使用量

吞吐量

业务吞吐量指的在一段时间内,程序需要完成的业务数量。

比如企业中对于吞吐量的要求可能会是这样的:支持用户每天生成 10000 笔订单、在晚上 8 点到 10 点,支持用户查询 50000 条商品信息。

保证高吞吐量的常规手段有两条:

1)优化业务执行性能,减少单次业务的执行时间

2)优化垃圾回收吞吐量

垃圾回收吞吐量

垃圾回收吞吐量指的是 CPU 用于执行用户代码的时间与 CPU 总执行时间的比值,即吞吐量 = 执行用户代码时间 /(执行用户代码时间 + GC 时间)。吞吐量数值越高,垃圾回收的效率就越高,允许更多的 CPU 时间去处理用户的业务,相应的业务吞吐量也就越高。

比如:虚拟机总共运行了 100 秒,其中 GC 花掉 1 秒,那么吞吐量就是 99% 。

延迟(Latency)

比如企业中对于延迟的要求可能会是这样的:所有的请求必须在 5 秒内返回给用户结果。

延迟 = GC 延迟 + 业务执行时间,所以如果 GC 时间过长,会影响到用户的使用。

内存使用量

GC 调优的方法

发现问题

Jstat 工具

使用方法为:<font style="color:#117CEE;">jstat -gc 进程ID</font> 每次统计的间隔(毫秒) 统计次数

C 代表 Capacity 容量,U 代表 Used 使用量

S – 幸存者区,E – 伊甸园区,O – 老年代,M – 元空间

YGC、YGT:年轻代 GC 次数和 GC 耗时(单位:秒)

FGC、FGCT:Full GC 次数和 Full GC 耗时

GCT:GC 总耗时

优点:操作简单,无额外的软件安装

缺点:无法精确到 GC 产生的时间,只能用于判断 GC 是否存在问题

visualvm 插件

优点:适合开发使用,能直观的看到堆内存和 GC 的变化趋势

缺点:对程序运行性能有一定影响,生产环境程序员一般没有权限进行操作。

Prometheus + Grafana

优点:

  • 支持系统级别和应用级别的监控,比如 linux 操作系统、Redis、MySQL、Java 进程。
  • 支持告警并允许自定义告警指标,通过邮件、短信等方式尽早通知相关人员进行处理。

缺点:环境搭建较为复杂,一般由运维人员完成。

GC 日志

使用方法(JDK 8 及以下):<font style="color:#117CEE;">-XX:+PrintGCDetails -Xloggc:文件名</font>

使用方法(JDK 9+):<font style="color:#117CEE;">-Xlog:gc*:file=文件名</font>

GC Viewer

github 地址:https://github.com/chewiebug/GCViewer

使用方法:<font style="color:#117CEE;">java -jar gcviewer_1.3.4.jar 日志文件.log</font>

GCeasy

官方网站:https://gceasy.io/

GCeasy 图示

常见的 GC 模式

特点:呈现锯齿状,对象创建之后内存上升,一旦发生垃圾回收之后下降到底部,并且每次下降之后的内存大小接近,存留的对象较少。

二、缓存对象过多

特点:呈现锯齿状,对象创建之后内存上升,一旦发生垃圾回收之后下降到底部,并且每次下降之后的内存大小接近,处于比较高的位置。

问题产生原因: 程序中保存了大量的缓存对象,导致 GC 之后无法释放,可以使用 MAT 或者 HeapHero 等工具进行分析内存占用的原因。

三、内存泄漏

特点:呈现锯齿状,每次垃圾回收之后下降到的内存位置越来越高,最后由于垃圾回收无法释放空间导致对象无法分配产生 OutOfMemory 的错误。

问题产生原因: 程序中保存了大量的内存泄漏对象,导致 GC 之后无法释放,可以使用 MAT 或者 HeapHero 等工具进行分析是哪些对象产生了内存泄漏。

四、持续的 Full GC

特点:在某个时间点产生多次 Full GC,CPU 使用率同时飙高,用户请求基本无法处理。一段时间之后恢复正常。

问题产生原因: 在该时间范围请求量激增,程序开始生成更多对象,同时垃圾收集无法跟上对象创建速率,导致持续地在进行 FULL GC。GC 分析报告

五、元空间不足导致的 FULL GC

特点:堆内存的大小并不是特别大,但是持续发生 FULL GC。

问题产生原因: 元空间大小不足,导致持续 FULL GC 回收元空间的数据。

GC 分析报告

解决问题

解决 GC 问题的手段

优化基础 JVM 参数

-Xmx 参数设置的是最大堆内存,但是由于程序是运行在服务器或者容器上,计算可用内存时,要将元空间、操作系统、其它软件占用的内存排除掉。

案例: 服务器内存 4G,操作系统 + 元空间最大值 + 其它软件占用 1.5G,-Xmx 可以设置为 2g。

最合理的设置方式应该是根据最大并发量估算服务器的配置,然后再根据服务器配置计算最大堆内存的值。

-Xms 用来设置初始堆大小,建议将 -Xms 设置的和 -Xmx 一样大
  • 运行时性能更好,堆的扩容是需要向操作系统申请内存的,这样会导致程序性能短期下降。
  • 可用性问题,如果在扩容时其他程序正在使用大量内存,很容易因为操作系统内存不足分配失败。
  • 启动速度更快,Oracle 官方文档的原话:如果初始堆太小,Java 应用程序启动会变得很慢,因为 JVM 被迫频繁执行垃圾收集,直到堆增长到更合理的大小。为了获得最佳启动性能,请将初始堆大小设置为与最大堆大小相同。

参数 2 : -XX:MaxMetaspaceSize 和 –XX:MetaspaceSize

<font style="color:#117CEE;">-XX:MaxMetaspaceSize=值</font> 参数指的是最大元空间大小,默认值比较大,如果出现元空间内存泄漏会让操作系统可用内存不可控,建议根据测试情况设置最大值,一般设置为 256m。

<font style="color:#117CEE;">-XX:MetaspaceSize=值</font> 参数指的是到达这个值之后会触发 FULL GC(网上很多文章的初始元空间大小是错误的),后续什么时候再触发 JVM 会自行计算。如果设置为和 MaxMetaspaceSize 一样大,就不会 FULL GC,但是对象也无法回收。

参数 3 : -Xss 虚拟机栈大小

如果我们不指定栈的大小,JVM 将创建一个具有默认大小的栈。大小取决于操作系统和计算机的体系结构。

比如 Linux x86 64 位 : 1MB,如果不需要用到这么大的栈内存,完全可以将此值调小节省内存空间,合理值为 256k – 1m 之间。

使用:-Xss256k

参数 4 : 不建议手动设置的参数

由于 JVM 底层设计极为复杂,一个参数的调整也许让某个接口得益,但同样有可能影响其他更多接口。

-Xmn 年轻代的大小,默认值为整个堆的 1/3,可以根据峰值流量计算最大的年轻代大小,尽量让对象只存放在年轻代,不进入老年代。但是实际的场景中,接口的响应时间、创建对象的大小、程序内部还会有一些定时任务等不确定因素都会导致这个值的大小并不能仅凭计算得出,如果设置该值要进行大量的测试。

G1 垃圾回收器尽量不要设置该值,G1 会动态调整年轻代的大小。

<font style="color:#117CEE;">‐XX:SurvivorRatio</font> 伊甸园区和幸存者区的大小比例,默认值为 8。

<font style="color:#117CEE;">‐XX:MaxTenuringThreshold</font> 最大晋升阈值,年龄大于此值之后,会进入老年代。另外 JVM 有动态年龄判断机制:将年龄从小到大的对象占据的空间加起来,如果大于 survivor 区域的 50%,然后把等于或大于该年龄的对象,放入到老年代。

其他参数 :

<font style="color:#117CEE;">-XX:+DisableExplicitGC</font>

禁止在代码中使用 System.gc(), System.gc() 可能会引起 FULL GC,在代码中尽量不要使用。使用 DisableExplicitGC 参数可以禁止使用 System.gc() 方法调用。

<font style="color:#117CEE;">-XX:+HeapDumpOnOutOfMemoryError</font>:发生 OutOfMemoryError 错误时,自动生成 hprof 内存快照文件。

<font style="color:#117CEE;">-XX:HeapDumpPath=<path></font>:指定 hprof 文件的输出路径。

打印 GC 日志

JDK8 及之前 :<font style="color:#117CEE;"> -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:文件路径</font>

JDK9 及之后 :<font style="color:#117CEE;"> -Xlog:gc*:file=文件路径</font>

JVM 参数模板:

-Xms1g
-Xmx1g
-Xss256k
-XX:MaxMetaspaceSize=512m
-XX:+DisableExplicitGC
-XX:+HeapDumpOnOutOfMemoryError
-XX:HeapDumpPath=/opt/logs/my-service.hprof
-XX:+PrintGCDetails
-XX:+PrintGCDateStamps
-Xloggc:文件路径

注意:

JDK9 及之后 GC 日志输出修改为 <font style="color:#117CEE;">-Xlog:gc*:file=文件名</font>

堆内存大小和栈内存大小根据实际情况灵活调整。

优化垃圾回收器的参数

这部分优化效果未必出色,仅当前边的一些手动无效时才考虑。

一个优化的案例:

CMS 的 并发模式失败(concurrent mode failure)现象。由于 CMS 的垃圾清理线程和用户线程是并行进行的,如果在并发清理的过程中老年代的空间不足以容纳放入老年代的对象,会产生并发模式失败。

并发模式失败会导致 Java 虚拟机使用 Serial Old 单线程进行 FULL GC 回收老年代,出现长时间的停顿。

解决方案:

1)减少对象的产生以及对象的晋升。

2)增加堆内存大小

3)优化垃圾回收器的参数,比如<font style="color:#117CEE;">-XX:CMSInitiatingOccupancyFraction=值</font>,当老年代大小到达该阈值时,会自动进行 CMS 垃圾回收,通过控制这个参数提前进行老年代的垃圾回收,减少其大小。

JDK8 中默认这个参数值为 -1,根据其他几个参数计算出阈值:

<font style="color:#117CEE;">((100 - MinHeapFreeRatio) + (double)(CMSTriggerRatio * MinHeapFreeRatio) / 100.0)</font>

该参数设置完是不会生效的,必须开启<font style="color:#117CEE;">-XX:+UseCMSInitiatingOccupancyOnly</font>参数。

实战中调整之后的效果:

案例实战

3、性能调优

性能调优解决的问题

应用程序在运行过程中经常会出现性能问题,比较常见的性能问题现象是:

1、通过 top 命令查看 CPU 占用率高,接近 100 甚至多核 CPU 下超过 100 都是有可能的。

2、请求单个服务处理时间特别长,多服务使用 skywalking 等监控系统来判断是哪一个环节性能低下。

3、程序启动之后运行正常,但是在运行一段时间之后无法处理任何的请求(内存和 GC 正常)。

性能调优的方法

线程转储的查看方式

解决 CPU 占用率高、死锁等问题

线程转储(Thread Dump)中的几个核心内容:

名称: 线程名称,通过给线程设置合适的名称更容易“见名知意”

优先级(prio):线程的优先级

Java ID(tid):JVM 中线程的唯一 ID

本地 ID(nid):操作系统分配给线程的唯一 ID

状态:线程的状态,分为:

  • NEW – 新创建的线程,尚未开始执行
  • RUNNABLE –正在运行或准备执行
  • BLOCKED – 等待获取监视器锁以进入或重新进入同步块/方法
  • WAITING – 等待其他线程执行特定操作,没有时间限制
  • TIMED_WAITING – 等待其他线程在指定时间内执行特定操作
  • TERMINATED – 已完成执行

栈追踪: 显示整个方法的栈帧信息

线程转储的可视化在线分析平台:

1、 https://jstack.review/

2、 https://fastthread.io/

Arthas 的 trace 命令

命令: trace 类名 方法名

  • 添加 --skipJDKMethod false 参数可以输出 JDK 核心包中的方法及耗时。
  • 添加‘#cost > 毫秒值’ 参数,只会显示耗时超过该毫秒值的调用。
  • 添加 –n 数值 参数,最多显示该数值条数的数据。
  • 所有监控都结束之后,输入 stop 结束监控,重置 arthas 增强的对象。

Arthas 的 watch 命令

命令: watch 类名 方法名 ‘{params, returnObj}’ ‘#cost>毫秒值' -x 2

  • ‘{params, returnObj}‘ 代表打印参数和返回值。
  • -x 代表打印的结果中如果有嵌套(比如对象里有属性),最多只展开 2 层。允许设置的最大值为 4。

Arthas 的 profile 命令

命令 1: profiler start 开始监控方法执行性能

命令 2: profiler stop --format html 以 HTML 的方式生成火焰图

火焰图中一般找绿色部分 Java 中栈顶上比较平的部分,很可能就是性能的瓶颈。

JIT 对程序性能的影响

JMH 工具

正确地测试代码性能

OpenJDK 中提供了一款叫 JMH(Java Microbenchmark Harness)的工具,可以准确地对 Java 代码进行基准测试,量化方法的执行性能。

官网地址:https://github.com/openjdk/jmh

JMH 会首先执行预热过程,确保 JIT 对代码进行优化之后再进行真正的迭代测试,最后输出测试的结果。

JMH 环境搭建:

创建基准测试项目,在 CMD 窗口中,使用以下命令创建 JMH 环境项目:

$ mvn archetype:generate \
-DinteractiveMode=false \
-DarchetypeGroupId=org.openjdk.jmh \
-DarchetypeArtifactId=jmh-java-benchmark-archetype \
-DgroupId=org.sample \
-DartifactId=test \
-Dversion=1.0

修改 POM 文件中的 JDK 版本号和 JMH 版本号,JMH 最新版本号参考 Github。

编写测试方法,几个需要注意的点:

  • 死代码问题
  • 黑洞的用法

优点

  • 通过 maven 的 verify 命令,检测代码问题并打包成 jar 包。通过

<font style="color:#117CEE;">java -jar target/benchmarks.jar</font> 命令执行基准测试。

案例实战