JVM学习笔记4-实战篇

参考资料:【黑马程序员JVM虚拟机入门到实战全套视频教程,java大厂面试必会的jvm一套搞定(丰富的实战案例及最热面试题)】BV1r94y1b7eS

———————————————————↓实战篇↓———————————————————

1.内存调优

内存溢出和内存泄露

内存泄漏(memory leak):在Java中如果不再使用一个对象,但是该对象依然在GC Root的引用链上, 这个对象就不会被垃圾回收器回收,这种情况就称之为内存泄漏

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

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

内存泄露的常见场景

第一种常见场景:

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

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

第二种常见场景:

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

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

解决内存溢出的方法

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

发现问题

发现问题——发现问题的工具

(1)Top命令

top命令是linux下用来查看系统信息的一个命令,它让我们能够去实时地去查看系统的资源,比如执行时的进程、线程和系统参数等信息。

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

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

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

(2)VisualVM

VisualVM是多功能合一的Java故障排除工具并且他是一款可视化工具,整合了命令行 JDK 工具和轻量级分析功能,功能非常强大。

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

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

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

(3)Arthas

Arthas是一款线上监控诊断产品,通过全局视角实时查看应用 load、内存、 gc、线程的状态信息,并能在不修改应用代码的情况下,对业务问题进行诊断, 包括查看方法调用的出入参、异常,监测方法执行耗时,类加载信息等,大大提升 线上问题排查效率。

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

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

(4)Prometheus + Grafana

Prometheus+Grafana是企业中运维常用的监控方案,其中Prometheus用来采集系统或者应用的相关数据,同时具备告警功能。Grafana可以将Prometheus采集到的数据以可视化的方式进行展示。

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

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

(5)堆内存状况的对比

发现问题——常见问题汇总

(1)代码中的内存泄漏

案例1:equals()和hashCode()导致的内存泄漏(出现频率:2星)

问题:

在定义新类时没有重写正确的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:内部类引用外部类(出现频率:2星)

问题:

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

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

解决方案:

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

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

案例3:ThreadLocal的使用(出现频率:4星)

问题:

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

解决方案:

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

案例4:String的intern方法(出现频率:2星

问题:

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

解决方案:

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

2.增大永久代空间的大小,根据实际的测试/估算结果进行设置,如:-XX:MaxPermSize=256M

案例5:通过静态字段保存对象(出现频率:5星

问题:

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

解决方案:

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

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

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

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

问题:

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

解决方案:

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

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

(2)并发请求问题

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

但是由于用户的并发请求量有可能很大,同时处理数据的时间很长,导致大量的数据存在于 内存中,最终超过了内存的上限,导致内存溢出。

这类问题的处理思路和内存泄漏类似,首先要定位到对象产生的根源。

诊断原因

内存快照(Heap Profile)

当堆内存溢出时,需要在堆内存溢出时将整个堆内存保存下来,生成内存快照(Heap Profile)文件。

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

-XX:+HeapDumpOnOutOfMemoryError:发生OutOfMemoryError错误时,自动生成hprof内存快照文件。

-XX:HeapDumpPath=<path>:指定hprof文件的输出路径。

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

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

MAT提供了称为支配树(Dominator Tree)的对象图。

支配树展示的是对象实例间的支配关系。在对象引用图中,所有指向对象B的路径都经过对象A,则认为对象A支配对象B。

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

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

支配树中对象的子树就是所有被该对象支配的内容,这些内容组成了对象的深堆(Retained Heap),也称之为保留集(Retained Set )。深堆的大小表示该对象如果可以被回收,能释放多大的内存空间。

MAT内存泄漏检测的原理——总结

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

练习:使用如下代码生成内存快照,并分析TestClass对象的深堆和浅堆

案例实战

修复问题&测试验证

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

生成内存快照并分析的思路:

1.服务出现OOM内存溢出时,生成内存快照

2.使用MAT分析内存快照,找到内存溢出的对象

3.尝试在开发环境中重现问题,分析代码中问题产生的原因

4.修改代码

5.测试并验证结果

在线定位思路:

使用arthas / btrace工具

案例1——分页查询文章接口的内存溢出

背景:

小李负责的新闻资讯类项目采用了微服务架构,其中有一个文章微服务,这个微服务在业务高峰期出现了内存溢出的现象。

问题根源:

文章微服务中的分页接口没有限制最大单次访问条数,并且单个文章对象占用的内存量较大 ,在业务高峰期并发量较大时这部分从数据库获取到内存之后会占用大量的内存空间。

解决思路:

1.与产品设计人员沟通,限制最大的单次访问条数

2.分页接口如果只是为了展示文章列表,不需要获取文章内容,可以大大减少对象的大小

3.在高峰期对微服务进行限流保护

案例2——Mybatis导致的内存溢出

背景:

小李负责的文章微服务进行了升级,新增加了一个判断id是否存在的接口,第二天业务高峰 期再次出现了内存溢出,小李觉得应该和新增加的接口有关系

问题根源:

Mybatis在使用foreach进行sql拼接时,会在内存中创建对象,如果foreach处理的数组或者 集合元素个数过多,会占用大量的内存空间。

解决思路:

1.限制参数中最大的id个数

2.将id缓存到redis或者内存缓存中,通过缓存进行校验

案例3——导出大文件内存溢出

背景:

小李负责了一个管理系统,这个管理系统支持几十万条数据的excel文件导出。他发现系统在运行时如果有几十个人同时进行大数据量的导出,会出现内存溢出。

小李团队使用的是k8s将管理系统部署到了容器中,所以这一次我们使用阿里云的k8s环境还 原场景,并解决问题。阿里云的k8s整体规划如下:

问题根源:

Excel文件导出如果使用POI的XSSFWorkbook,在大数据量(几十万)的情况下会占用大量的内存。

解决思路:

1.使用poi的SXSSFWorkbook

2.hutool提供的BigExcelWriter减少内存开销

3.使用easy excel,对内存进行大量的优化

案例4——ThreadLocal使用时占用大量内存

背景:

小李负责了一个微服务,但是他发现系统在没有任何用户使用时,也占用了大量的内存。导致可以使用的内存大大减少。

问题根源&解决思路:

很多微服务会选择在拦截器preHandle方法中去解析请求头中的数据,并放入一些数据到 ThreadLocal中方便后续使用。在拦截器的afterCompletion方法中,必须要将ThreadLocal 中的数据清理掉。

案例5——文章内容审核接口的内存问题

背景:

文章微服务中提供了文章审核接口,会调用阿里云的内容安全接口进行文章中文字和图片的 审核,在自测过程中出现内存占用较大的问题

设计1:

使用SpringBoot中的@Async注解进行异步的审核

存在问题:

1.线程池参数设置不当,会导致大量线程的创建或者队列中保存大量的数据

2.任务没有持久化,一旦走线程池的拒绝策略或者服务宕机、服务器掉电等情况很有可能会 丢失任务

设计2:

使用生产者和消费者模式进行处理,队列数据可以实现持久化到数据库

存在问题:
1.队列参数设置不正确,会保存大量的数据

2.实现复杂,需要自行实现持久化的机制,否则数据会丢失

设计3:

使用mq消息队列进行处理,由mq来保存文章的数据。发送消息的服务和拉取消息的服务可 以是同一个,也可以不是同一个

问题根源&解决思路:

在项目中如果要使用异步进行业务处理,或者实现生产者–消费者的模型,如果在Java代码中实现,会占用大量的内存去保存中间数据

尽量使用Mq消息队列,可以很好地将中间数据单独进行保存,不会占用Java的内存。同时也可以将生产者和消费者拆分成不同的微服务

总结

2.GC调优

GC调优指的是对垃圾回收(Garbage Collection)进行调优。

GC调优的主要目标是避免由垃圾回收引起程序性能下降。

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

1.通用JVM参数的设置

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

3.解决由频繁的Full GC引起的程序性能问题

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

GC调优的核心指标

判断GC是否需要调优,需要从三方面来考虑,与GC算法的评判标准类似

吞吐量(Throughput)

吞吐量分为业务吞吐量垃圾回收吞吐量

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

比如企业中对于吞吐量的要求可能会是这样的:

1.支持用户每天生成10000笔订单;

2.在晚上8点到10点,支持用户查询50000条商品信息

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

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

2.优化垃圾回收吞吐量

垃圾回收吞吐量指的是 CPU 用于执行用户代码的时间与 CPU 总执行时间的比值,即:

吞吐量 = 执行用户代 码时间 /(执行用户代码时间 + GC时间)

吞吐量数值越高,垃圾回收的效率就越高,允许更多的CPU时 间去处理用户的业务,相应的业务吞吐量也就越高

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

延迟(Latency)

延迟指的是从用户发起一个请求到收到响应这其中经历的时间。

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

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

如上图,总处理时间就超过了5秒

内存使用量

内存使用量指的是Java应用占用系统内存的最大值,一般通过JVM参数调整,在满足上述两个指标的前提下, 这个值越小越好。

GC调优的方法

发现问题&诊断问题

发现问题——发现问题的工具

(1)Jstat工具

Jstat工具是JDK自带的一款监控工具,可以提供各种垃圾回收、类加载、编译信息等不同的数据

使用方法为:jstat -gc 进程ID每次统计的间隔(毫秒)统计次数

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

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

(2)VisualVm插件

VisualVm中提供了一款Visual Tool插件,实时监控Java进程的堆内存结构、 堆内存变化趋势以及垃圾回收时间的变化趋势。同时还可以监控对象晋升的直方图。

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

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

(3)Prometheus + Grafana

Prometheus+Grafana是企业中运维常用的监控方案,其中Prometheus用来采集系统或者应用的相关数据,同时具备告警功能。Grafana可以将Prometheus采集到的数据以可视化的方式进行展示。

优点:

1.支持系统级别和应用级别的监控,比如linux操作系统、 Redis、MySQL、Java进程

2.支持告警并允许自定义告警指标,通过邮件、短信等方式尽早通知相关人员进行处理

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

(4)GC日志

通过GC日志,可以更好的看到垃圾回收细节上的数据,同时也可以根据每款垃圾回收器的不同特点更好地发现存在的问题。

使用方法(JDK 8及以下):-XX:+PrintGCDetails        -Xloggc:文件名

使用方法(JDK 9+):-Xlog:gc*:file=文件名

(5)GC Viewer

GCViewer是一个将GC日志转换成可视化图表的小工具,github地址:

https://github.com/chewiebug/GCViewer

使用方法:java -jar gcviewer_1.3.4.jar 日志文件.log

(6)GC easy

GCeasy是业界首款使用AI机器学习技术在线进行GC分析和诊断的工具。定位内存泄漏、GC延迟高的问题,提供JVM 参数优化建议,支持在线的可视化工具图表展示

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

诊断问题——常见的GC模式

一、正常情况

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

二、缓存对象过多

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

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

三、内存泄漏

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

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

四、持续的Full GC

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

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

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

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

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

修复问题&测试验证

解决GC问题的手段中,前三种是比较推荐的手段,第四种仅在前三种无法解决时选用:

1.优化基础JVM参数

参数1:-Xmx 和 –Xms

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

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

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

-Xms用来设置初始堆大小,建议将-Xms设置的和-Xmx一样大,好处有以下几点:

1.运行时性能更好,堆的扩容是需要向操作系统申请内存的,这样会导致程序性能短期下降

2.可用性问题,如果在扩容时其他程序正在使用大量内存,很容易因为操作系统内存不足分配失败

3.启动速度更快(Oracle官方建议:如果初始堆太小,Java 应用程序启动会变得很慢,因为 JVM 被迫频繁执行垃圾收集,直到堆增长到更合理的大小。为了获得最佳启动性能,请将初始堆大小设置为与最大堆大小相同)

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

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

-XX:MetaspaceSize = 值        参数指的是到达这个值之后会触发Full GC, 后续什么时候再触发JVM会自行计算。如果设置为和MaxMetaspaceSize一样大,就不会Full GC,但是对象也无法回收。

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

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

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

使用:-Xss256k

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

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

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

‐XX:SurvivorRatio 伊甸园区和幸存者区的大小比例,默认值为8

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

其他参数:

-XX:+DisableExplicitGC

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

-XX:+HeapDumpOnOutOfMemoryError

发生OutOfMemoryError错误时,自动生成hprof内存快照文件。-XX:HeapDumpPath=:指定hprof文件的输

2.减少对象产生

3.更换垃圾回收器

案例——垃圾回收器的选择

背景:

小李负责的程序在高峰期遇到了性能瓶颈,团队从业务代码入手优化了多次也取得了不错的效 果,这次他希望能采用更合理的垃圾回收器优化性能。

思路:

1.编写Jmeter脚本对程序进行压测,同时添加RT响应时间、每秒钟的事务数等指标进行监控。

2.选择不同的垃圾回收器进行测试,并发量分别设置50、100、200,观察数据的变化情况

3.JDK8 下 ParNew + CMS 组合 : -XX:+UseParNewGC-XX:+UseConcMarkSweepGC

默认组合:PS + PO

JDK8使用G1 : -XX:+UseG1GC

JDK11 默认 G1

4.优化垃圾回收器参数

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

一个优化的案例:

问题背景:

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

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

解决方案:

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

2.增加堆内存大小

3.优化垃圾回收器的参数,比如-XX:CMSInitiatingOccupancyFraction=值,当老年代大小到达该阈值时,会自动进行CMS垃圾回收,通过控制这个参数提前进行老年代的垃圾回收,减少其大小。

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

((100 - MinHeapFreeRatio) + (double)(CMSTriggerRatio * MinHeapFreeRatio) / 100.0)

该参数设置完是不会生效的,必须开启-XX:+UseCMSInitiatingOccupancyOnly参数。

案例实战

实战案例——内存调优 + GC调优

背景:

小李负责的程序在高峰期经常会出现接口调用时间特别长的现象,他希望能优化程序的性能。

思路:

1.生成GC报告,通过Gceasy工具进行分析,判断是否存在GC问题或者内存问题

2.存在内存问题,通过jmap或者arthas将堆内存快照保存下来

3.通过MAT或者在线的heaphero工具分析内存问题的原因

4.修复问题,并发布上线进行测试

问题1:发生了连续的Full GC,堆内存1g,如果没有请求的情况下,内存大小在200-300mb之间。

分析:没有请求的情况下,内存大小并没有处于很低的情况,满足缓存对象过多的情况,怀疑内存中缓存了很多数据。需要将堆内存快照保存下来进行分析

问题2:堆内存快照保存到本地之后,使用MAT打开,发现只有几十兆的内存。

分析:有大量的对象不在GC Root引用链上,可以被回收,使用MAT查看这些对象

问题3:由于这些对象已经不在引用链上,无法通过支配树等手段分析创建的位置

分析:在不可达对象列表中,除了发现大量的byte[]还发现了大量的线程,可以考虑跟踪线程的栈信息来判断对象在哪里创建

问题4:修复之后内存基本上处于100m左右,但是当请求发生时,依然有频繁Full GC的发生

分析:请求产生的内存大小比当前最大堆内存大,尝试选择配置更高的服务器,将-Xmx和-Xms参数调大一些

案例总结:

1.压力比较大的服务中,尽量不要存放大量的缓存或者定时任务,会影响到服务的内存使用

2.内存分析发现有大量线程创建时,可以使用导出线程栈来查看线程的运行情况

3.如果请求确实创建了大量的内存超过了内存上限,只能考虑减少请求时创建的对象,或者使用更大的内存

4.推荐使用G1垃圾回收器,并且使用较新的JDK可以获得更好的性能

总结

3.性能调优

性能优化的步骤总共分为四个步骤,其中修复部分要具体问题具体分析且处理方式各不相同。

本章中着重学习发现问题和诊断问题的方法,目标是准确定位到性能问题的根源。

性能调优解决的问题

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

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

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

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

性能调优的方法

线程转储(Thread Dump)的查看方式

线程转储(Thread Dump)提供了对所有运行中的线程当前状态的快照。线程转储可以通过jstack、visualvm等工 具获取。其中包含了线程名、优先级、线程ID、线程状态、线程栈信息等等内容,可以用来解决CPU占用率高、死锁等问题

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

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

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

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

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

5.状态:线程的状态分为:

NEW:新创建的线程,尚未开始执行

RUNNABLE:正在运行或准备执行

BLOCKED:等待获取监视器锁以进入或重新进入同步块/方法

WAITTING:等待其他线程执行特定操作,没有时间限制

TIMED_WAITTING:等待其他线程在指定时间内执行特定操作

TERMINATED:已完成执行

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

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

1. https://jstack.review/

2. https://fastthread.io/

各种案例

案例1:CPU占用率高问题的解决方案

问题:

监控人员通过prometheus的告警发现CPU占用率一直处于很高的情况,通过top命令看到是由 于Java程序引起的,希望能快速定位到是哪一部分代码导致了性能问题。

解决思路:

1.通过top –c 命令找到CPU占用率高的进程,获取它的进程ID

2.使用top -p 进程ID单独监控某个进程,按H可以查看到所有的线程以及线程对应的CPU使用率,找到CPU使用率特别高的线程

3.使用 jstack 进程ID 命令可以查看到所有线程正在执行的栈信息。使用 jstack进程ID > 文件名 保存到文件中方便查看

4.找到nid线程ID相同的栈信息,需要将之前记录下的十进制线程号转换成16进制 。通过

printf ‘%x\n’ 线程ID 命令直接获得16进制下的线程ID

5.找到栈信息对应的源代码,并分析问题产生原因

案例补充:

在定位CPU占用率高的问题时,比较需要关注的是状态为RUNNABLE的线程。但实 际上,有一些线程执行本地方法时并不会消耗CPU,而只是在等待。但 JVM 仍然会 将它们标识成“RUNNABLE”状态。

案例2:接口响应时间很长的问题

问题:

在程序运行过程中,发现有几个接口的响应时间特别长,需要快速定位到是哪一个方法的代码执行过程中出现了性能问题。

解决思路:

已经确定是某个接口性能出现了问题,但是由于方法嵌套比较深,需要借助于arthas定位到具体的方法。

总结:

1.通过arthas的trace命令,首先找到性能较差的具体方法,如果访问量比较大,建议设置最小的耗时,精确的找到耗时比较高的调用。

2.通过watch命令,查看此调用的参数和返回值,重点是参数,这样就可以在开发环境或者测试环境模拟类似的现象,通过debug找到具体的问题根源。

3.使用stop命令将所有增强的对象恢复。

案例3:定位偏底层的性能问题

问题:

有一个接口中使用了for循环向ArrayList中添加数据,但是最终发现执行时间比较长,需要定位是由于什么原因导致的性能低下。

解决思路:

Arthas提供了性能火焰图的功能,可以非常直观地显示所有方法中哪些方法执行时间比较长。

总结:

偏底层的性能问题,特别是由于JDK中某些方法被大量调用导致的性能低下,可以使用火焰图 非常直观的找到原因。

这个案例中是由于创建ArrayList时没有手动指定容量,导致使用默认的容量而在添加对象过程 中发生了多次的扩容,扩容需要将原来数组中的元素复制到新的数组中,消耗了大量的时间。 通过火焰图可以看到大量的调用,修复完之后节省了20% ~ 50%的时间。

案例4:线程被耗尽问题

(该案例对应性能调优问题第三项:程序启动之后运行正常,但是在运行一段时间之后无法处理任何的请求(内存和GC正常))

问题:

程序在启动运行一段时间之后,就无法接受任何请求了。将程序重启之后继续运行,依然会出现相同的情况。

解决思路:

线程耗尽问题,一般是由于执行时间过长,分析方法分成两步:

1.检测是否有死锁产生,无法自动解除的死锁会将线程永远阻塞

2.如果没有死锁,再使用案例1的打印线程栈的方法检测线程正在执行哪个方法,一般这些大量出现的方法就是慢方法

(死锁:两个或以上的线程因为争夺资源而造成互相等待的现象)

解决方案:

线程死锁可以通过三种方法定位问题:

1.jstack-l 进程ID > 文件名 将线程栈保存到本地

在文件中搜索deadlock即可找到死锁位置

2.开发环境中使用visual vm或者Jconsole工具,都可以检测出死锁。使用线程快照生成工具就可以看到死锁的根源。生产环境的服务一般不会允许使用这两种工具连接

3.使用fastthread自动检测线程问题。 https://fastthread.io/

Fastthread和GCeasy类似,是一款在线的AI自动线程问题检测工具,可以提供线程分析报告。 通过报告查看是否存在死锁问题。

更精细化的性能测试

Java程序在运行过程中,JIT即时编译器会实时对代码进行性能优化,所以仅凭少量的测试是无法真实反应运行系统最 终给用户提供的性能。如下图,随着执行次数的增加,程序性能会逐渐优化。

正确地测试代码性能

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

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

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

例子:日期格式化方法性能测试

问题:

在JDK8中,可以使用Date进行日期的格式化,也可以使用LocalDateTime进行格式化,使用JMH对比这两种格式化的性能。

解决思路:

1.搭建JMH测试环境

2.编写JMH测试代码

3.进行测试

4.比对测试结果

总结:

1.Date对象使用的SimpleDateFormatter是线程不安全的,所以每次需要重新创建对象或者将对象放入 ThreadLocal中进行保存。其中每次重新创建对象性能比较差,将对象放入ThreadLocal之后性能相对还是比 较好的

2.LocalDateTime对象使用的DateTimeFormatter线程安全,并且性能较好,如果能将 DateTimeFormatter对象保存下来,性能可以得到进一步的提升

案例实战

问题:

小李的项目中有一个获取用户信息的接口性能比较差,他希望能对这个接口在代码中进行彻底的优化,提升性能。

解决思路:

1.使用trace分析性能瓶颈

2.优化代码,反复使用trace测试性能提升的情况

3.使用JMH在SpringBoot环境中进行测试

4.比对测试结果

总结:

1.本案例中性能问题产生的原因是两层for循环导致的循环次数过多,处理时间在循环次数变大的情况下变得非常长,考虑将一层循环拆出去,创建HashMap用来查询提升性能。

2.使用LocalDateTime替代SimpleDateFormat进行日期的格式化

3.使用stream流改造代码,这一步可能会导致性能下降,主要是为了第四次优化准备

4.使用并行流利用多核CPU的优势并行执行提升性能

总结

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值