如果真的要回忆关于印象深刻的bug,可能就要追溯到我刚实习的时候。而之所以那么深刻,可能是源自初入社会那份求知的信,亦或者是那时候遇到的人,对我如今选择的这条路影响深远。
2017年,实习,大数据组。
对于二三线城市来说,很多公司的计算机实习岗位,约等于无门槛免费劳动力。当然,我也不例外,在大数据实习的前两个月,除了整理工作文档,就是自己悄摸摸的学习。后来在机缘巧合之下,以运维的角色接触到了一个数据接入的项目。
程序主要分为三个部分:TCP数据接入、数据解析、写kafka。当时的问题就是程序启动之后,运行一段时间就内存溢出,进而造成的结果就是TCP阻塞以及数据量核对不上。当时怀疑是解析这部分的代码性能效率不高,因为解析这一块比较复杂,每条数据平局100个字段,每个字段都要根据规范中的字节长度和数据类型,将二进制转换成明文数据。
所以当时看他们就是铆足了劲,如何去优化解码逻辑。后来就来了一位大佬,就用jvisualvm定位出了问题所在:因为当时平台都是集中化建设,Hadoop还是1.x版本,所以kakfa也是0.8版本,正因为kafka版本太低,写入效率太慢,虽然整个程序占用cpu过高,但实际上二进制数一直阻塞在程序的queue中,最后导致内存溢出。
后来找到问题之后,在Hadoop2.x建设阶段,将kafka升级到了0.10版本,就解决了这个问题。当时自己空有jvm的理论知识,后来在这次经历中也算是将理论映进了现实,所以到至今都印象深刻,现在也特别推崇通过jvm来分析程序性能的方式。所以,本篇文章就主要大概复现上述问题,看看如何使用jvisualvm来分析程序性能问题。
为了复现上面的问题,原来程序的TCP数据接入、数据解析、写kafka三个部分,我分别进行了简单的代码替换。首先我们定义数据接入的模块,这里主要模拟数据写入queue的一个过程。
这里主要是讲data写入queue,并为了防止内存溢出使用sleep来写入控制频率。然后我们实现数据解析以及写入kafka模块。
这里我启动了10个线程,并给每个线程编号。在run方法中,通过输出4万次空字符串来模拟数据解析过程,通过sleep来模拟写入kafka的效率问题。
启动程序之后,就看到了一个名为Test的java程序进程。点击进入,选择线程选项卡。
如图,我们可以看到thread0-10线程是由紫色、红色以及绿色组成。大部分都处于紫色,紫色代表线程休眠,少量的绿色部分表示是出于运行状态,也就是上述代码中的数据解析,红色是jvisualvm采样导致的,这个不用管。从最后的运行占比中也能看出,线程处理活动状态(解析)的比例都在10%之内,也就说明如果写入kafka效率过慢,会导致前面解析逻辑无法运行,从而最后导致性能下降。
当然,这里是不严谨的,为什么这么说呢。是因为我在代码中本来就显式使用了Thread.sleep,所以最后在jvisualvm中显示紫色也是很正常的。其实在生产中,写入kafka的线程是独立的,但是和数据解析的线程(thread0-10)是“串行的”。
数据解析之后会立即写入到kafka,本次循环需要再kafka写入成功之后返回响应,才会结束并重新执行上面的数据解析逻辑,但是写入kafka太慢了,需要一直等待。所以当时实际看到的现象是:thread0-10是如图中这种休眠状态,而写入kafka的线程一直是处于绿色运行状态。
进而推断出程序的效率低下,并不是因为计算资源不足或者数据解析逻辑导致的(如果一直处于绿色活动状态,那就是解析逻辑有问题或者计算资源不足)。
jvisualvm 是一个强大的性能分析工具,它是 JDK 自带的一部分,通常与 JDK 一起安装。jvisualvm 主要用于监控 Java 应用程序的性能,帮助开发人员了解应用程序的内存、CPU 使用情况,分析线程的执行情况,发现潜在的性能瓶颈,以及进行垃圾回收分析等。
换个理解方式,jvisualvm是jstat、jmap、jstack、jps等命令的可视化方案。在配置好jdk之后,在命令行中运行 jvisualvm 命令即可启动。
监控 JVM 性能
在jvisualvm中,你可以看到不同的选项卡,如 Monitor(监控)、Profiler(性能分析)、Heap Dump(堆转储)等。每个选项卡提供了不同的性能数据,帮助开发者分析 Java 应用的运行状态。
监控选项卡(Monitor)
Monitor 选项卡提供了 JVM 的基本性能数据,包括:
-
CPU 使用率:显示 Java 应用的 CPU 使用情况。通过查看 CPU 使用率,可以了解应用是否存在 CPU 密集型任务。
-
堆内存使用:显示 Java 堆内存的使用情况。包括 Eden 区、Survivor 区、Old 区等内存区域的使用量。通过分析堆内存,可以判断应用是否频繁进行垃圾回收,是否存在内存泄漏等问题。
-
线程使用情况:显示 Java 应用中活跃线程的数量。通过查看线程数,能够快速了解应用是否存在线程饥饿或者线程过多等问题。
性能分析(Profiler)
Profiler 选项卡可以帮助开发者进行更深入的性能分析,主要通过以下几个方面进行:
-
CPU 性能分析:通过监控应用中各个方法的 CPU 占用情况,帮助开发者定位性能瓶颈。会显示各个方法的执行时间,帮助你发现哪些方法消耗了大量 CPU 资源。
-
内存分析:通过监控 JVM 堆内存的分配情况,帮助开发者了解哪些对象占用了大量内存,是否有内存泄漏等问题。jvisualvm 还可以生成堆分析图,展示内存的使用情况和对象的引用关系。
-
对象分配分析:展示 Java 应用中对象的创建和销毁情况,帮助开发者发现不必要的对象创建和内存泄漏等问题。
垃圾回收分析(Garbage Collection)
垃圾回收是 Java 中的一个重要环节,jvisualvm 提供了垃圾回收的监控功能。通过 Garbage Collector 选项卡,可以查看垃圾回收的详细信息,包括:
-
GC 时间:展示垃圾回收的总时间。通过监控 GC 时间,开发者可以判断垃圾回收是否频繁,是否影响了应用的性能。
-
GC 类型:显示应用使用的垃圾回收器类型,如 SerialGC、ParallelGC、G1GC 等。
-
GC 频率:显示垃圾回收的触发频率。如果 GC 过于频繁,可能会影响应用性能,需要进行调优。
堆转储(Heap Dump)
堆转储是对 JVM 堆内存的快照,jvisualvm 可以生成堆转储文件并进行分析。堆转储可以帮助开发者了解 Java 堆中的对象分布、引用关系等,有助于诊断内存泄漏、过度的对象创建等问题。
-
生成堆转储:在 Heap Dump 选项卡中点击“Dump”按钮,即可生成堆转储文件。
-
分析堆转储:堆转储生成后,jvisualvm 会显示堆内存中的所有对象,并可以按类、大小、引用关系等进行排序和过滤。
同时也可以通过抽样器选项卡的内存选项,可以看到上面的这些指标:
通过 jvisualvm 提供的性能分析工具,开发者可以定位到性能瓶颈,从而进行优化。以下是一些常见的优化建议:
-
减少 GC 频率:
如果应用频繁触发垃圾回收,可能会影响性能。优化方法包括:- 增加堆内存大小。
- 使用适当的垃圾回收器,如 G1GC,它在较大的堆内存环境下表现较好。
- 优化对象的生命周期,减少短时间内创建大量对象。
-
优化 CPU 密集型代码:
- 对于 CPU 密集型任务,可以考虑将任务拆分成多个线程,并使用多核 CPU 来提高性能。
- 使用 分析方法调用的执行时间,发现消耗 CPU 资源较高的方法,并进行优化。
-
内存泄漏检测:
- 如果发现内存使用持续增长且垃圾回收没有明显改善,可以考虑使用堆转储分析工具检查是否有内存泄漏。
-
避免不必要的对象创建:
- 通过 监控对象的创建情况,避免在频繁调用的方法中创建不必要的对象。
jvisualvm 是一个非常强大的 Java 性能分析和调优工具,它为开发者提供了详细的内存使用情况、垃圾回收情况、CPU 使用情况等多维度的信息,帮助开发者发现和解决 Java 应用中的性能问题。