「译文」Java 垃圾收集参考手册(十):GC 调优工具篇

本文涉及的产品
日志服务 SLS,月写入数据量 50GB 1个月
简介: 「译文」Java 垃圾收集参考手册(十):GC 调优工具篇

进行 GC 性能调优时, 需要明确了解, 当前的 GC 行为对系统和用户有多大的影响。有多种监控 GC 的工具和方法, 本章将逐一介绍常用的工具。

您应该已经阅读了前面的章节.

JVM 在程序执行的过程中, 提供了 GC 行为的原生数据。那么, 我们就可以利用这些原生数据来生成各种报告。原生数据(raw data) 包括:

  • 各个内存池的当前使用情况,
  • 各个内存池的总容量,
  • 每次 GC 暂停的持续时间,
  • GC 暂停在各个阶段的持续时间。

可以通过这些数据算出各种指标, 例如: 程序的内存分配率, 提升率等等。本章主要介绍如何获取原生数据。 后续的章节将对重要的派生指标 (derived metrics) 展开讨论, 并引入 GC 性能相关的话题。

JMX API

从 JVM 运行时获取 GC 行为数据, 最简单的办法是使用标准 JMX API 接口 . JMX 是获取 JVM 内部运行时状态信息 的标准 API. 可以编写程序代码, 通过 JMX API 来访问本程序所在的 JVM,也可以通过 JMX 客户端执行(远程) 访问。

最常见的 JMX 客户端是 JConsoleJVisualVM (可以安装各种插件, 十分强大)。两个工具都是标准 JDK 的一部分, 而且很容易使用. 如果使用的是 JDK 7u40 及更高版本, 还可以使用另一个工具: Java Mission Control(大致翻译为 Java 控制中心, jmc.exe)。

JVisualVM 安装 MBeans 插件的步骤: 通过 工具(T) – 插件(G) – 可用插件 – 勾选 VisualVM-MBeans – 安装 – 下一步 – 等待安装完成…… 其他插件的安装过程基本一致。

所有 JMX 客户端都是独立的程序, 可以连接到目标 JVM 上。目标 JVM 可以在本机, 也可能是远端 JVM. 如果要连接远端 JVM, 则目标 JVM 启动时必须指定特定的环境变量, 以开启远程 JMX 连接 / 以及端口号。 示例如下:

java -Dcom.sun.management.jmxremote.port=5432 com.yourcompany.YourApp
STYLUS

在此处, JVM 打开端口 5432 以支持 JMX 连接。

通过 JVisualVM 连接到某个 JVM 以后, 切换到 MBeans 标签, 展开 “java.lang/GarbageCollector” . 就可以看到 GC 行为信息, 下图是 JVisualVM 中的截图:

06_01_JMX-view.png

下图是 Java Mission Control 中的截图:

06_02_JMX-view-Mbean.png

从以上截图中可以看到两款垃圾收集器。其中一款负责清理年轻代(PS Scavenge),另一款负责清理老年代(PS MarkSweep); 列表中显示的就是垃圾收集器的名称。可以看到 , jmc 的功能和展示数据的方式更强大。

对所有的垃圾收集器, 通过 JMX API 获取的信息包括:

  • CollectionCount : 垃圾收集器执行的 GC 总次数,
  • CollectionTime: 收集器运行时间的累计。这个值等于所有 GC 事件持续时间的总和,
  • LastGcInfo: 最近一次 GC 事件的详细信息。包括 GC 事件的持续时间(duration), 开始时间(startTime) 和 结束时间(endTime), 以及各个内存池在最近一次 GC 之前和之后的使用情况,
  • MemoryPoolNames: 各个内存池的名称,
  • Name: 垃圾收集器的名称
  • ObjectName: 由 JMX 规范定义的 MBean 的名字,
  • Valid: 此收集器是否有效。本人只见过 “true“的情况 (^_^)

根据经验, 这些信息对 GC 的性能来说, 不能得出什么结论. 只有编写程序, 获取 GC 相关的 JMX 信息来进行统计和分析。 在下文可以看到, 一般也不怎么关注 MBean , 但 MBean 对于理解 GC 的原理倒是挺有用的。

JVisualVM

JVisualVM 工具的 “VisualGC” 插件提供了基本的 JMX 客户端功能, 还实时显示出 GC 事件以及各个内存空间的使用情况。

Visual GC 插件常用来监控本机运行的 Java 程序, 比如开发者和性能调优专家经常会使用此插件, 以快速获取程序运行时的 GC 信息。

06_03_jvmsualvm-garbage-collection-monitoring.png

左侧的图表展示了各个内存池的使用情况: Metaspace/ 永久代, 老年代, Eden 区以及两个存活区。

在右边, 顶部的两个图表与 GC 无关, 显示的是 JIT 编译时间 和 类加载时间。下面的 6 个图显示的是内存池的历史记录, 每个内存池的 GC 次数,GC 总时间, 以及最大值,峰值, 当前使用情况。

再下面是 HistoGram, 显示了年轻代对象的年龄分布。至于对象的年龄监控(objects tenuring monitoring), 本章不进行讲解。

与纯粹的 JMX 工具相比, VisualGC 插件提供了更友好的界面, 如果没有其他趁手的工具, 请选择 VisualGC. 本章接下来会介绍其他工具, 这些工具可以提供更多的信息, 以及更好的视角. 当然, 在“Profilers(分析器)”一节中,也会介绍 JVisualVM 的适用场景 —— 如: 分配分析(allocation profiling), 所以我们绝不会贬低哪一款工具, 关键还得看实际情况。

jstat

jstat 也是标准 JDK 提供的一款监控工具(Java Virtual Machine statistics monitoring tool), 可以统计各种指标。既可以连接到本地 JVM, 也可以连到远程 JVM. 查看支持的指标和对应选项可以执行 “jstat -options” 。例如:

+-----------------+---------------------------------------------------------------+
|     Option      |                          Displays...                          |
+-----------------+---------------------------------------------------------------+
|class            | Statistics on the behavior of the class loader                |
|compiler         | Statistics  on  the behavior of the HotSpot Just-In-Time com- |
|                 | piler                                                         |
|gc               | Statistics on the behavior of the garbage collected heap      |
|gccapacity       | Statistics of the capacities of  the  generations  and  their |
|                 | corresponding spaces.                                         |
|gccause          | Summary  of  garbage collection statistics (same as -gcutil), |
|                 | with the cause  of  the  last  and  current  (if  applicable) |
|                 | garbage collection events.                                    |
|gcnew            | Statistics of the behavior of the new generation.             |
|gcnewcapacity    | Statistics of the sizes of the new generations and its corre- |
|                 | sponding spaces.                                              |
|gcold            | Statistics of the behavior of the old and  permanent  genera- |
|                 | tions.                                                        |
|gcoldcapacity    | Statistics of the sizes of the old generation.                |
|gcpermcapacity   | Statistics of the sizes of the permanent generation.          |
|gcutil           | Summary of garbage collection statistics.                     |
|printcompilation | Summary of garbage collection statistics.                     |
+-----------------+---------------------------------------------------------------+
GHERKIN

jstat 对于快速确定 GC 行为是否健康非常有用。启动方式为: “jstat -gc -t PID 1s” , 其中,PID 就是要监视的 Java 进程 ID。可以通过 jps 命令查看正在运行的 Java 进程列表。

jps
 
jstat -gc -t 2428 1s
APACHE

以上命令的结果, 是 jstat 每秒向标准输出输出一行新内容, 比如:

Timestamp  S0C    S1C    S0U    S1U      EC       EU        OC         OU       MC     MU    CCSC   CCSU   YGC     YGCT    FGC    FGCT     GCT 
200.0    8448.0 8448.0 8448.0  0.0   67712.0  67712.0   169344.0   169344.0  21248.0 20534.3 3072.0 2807.7     34    0.720  658   133.684  134.404
201.0    8448.0 8448.0 8448.0  0.0   67712.0  67712.0   169344.0   169343.2  21248.0 20534.3 3072.0 2807.7     34    0.720  662   134.712  135.432
202.0    8448.0 8448.0 8102.5  0.0   67712.0  67598.5   169344.0   169343.6  21248.0 20534.3 3072.0 2807.7     34    0.720  667   135.840  136.559
203.0    8448.0 8448.0 8126.3  0.0   67712.0  67702.2   169344.0   169343.6  21248.0 20547.2 3072.0 2807.7     34    0.720  669   136.178  136.898
204.0    8448.0 8448.0 8126.3  0.0   67712.0  67702.2   169344.0   169343.6  21248.0 20547.2 3072.0 2807.7     34    0.720  669   136.178  136.898
205.0    8448.0 8448.0 8134.6  0.0   67712.0  67712.0   169344.0   169343.5  21248.0 20547.2 3072.0 2807.7     34    0.720  671   136.234  136.954
206.0    8448.0 8448.0 8134.6  0.0   67712.0  67712.0   169344.0   169343.5  21248.0 20547.2 3072.0 2807.7     34    0.720  671   136.234  136.954
207.0    8448.0 8448.0 8154.8  0.0   67712.0  67712.0   169344.0   169343.5  21248.0 20547.2 3072.0 2807.7     34    0.720  673   136.289  137.009
208.0    8448.0 8448.0 8154.8  0.0   67712.0  67712.0   169344.0   169343.5  21248.0 20547.2 3072.0 2807.7     34    0.720  673   136.289  137.009
APACHE

稍微解释一下上面的内容。参考 jstat manpage , 我们可以知道:

  • jstat 连接到 JVM 的时间, 是 JVM 启动后的 200 秒。此信息从第一行的 “Timestamp” 列得知。继续看下一行, jstat 每秒钟从 JVM 接收一次信息, 也就是命令行参数中 “1s” 的含义。
  • 从第一行的 “YGC” 列得知年轻代共执行了 34 次 GC, 由 “FGC” 列得知整个堆内存已经执行了 658 次 full GC。
  • 年轻代的 GC 耗时总共为 0.720 秒, 显示在“YGCT” 这一列。
  • Full GC 的总计耗时为 133.684 秒 , 由“FGCT”列得知。 这立马就吸引了我们的目光, 总的 JVM 运行时间只有 200 秒, 但其中有 66% 的部分被 Full GC 消耗了

再看下一行, 问题就更明显了。

  • 在接下来的一秒内共执行了 4 次 Full GC。参见 “FGC” 列.
  • 这 4 次 Full GC 暂停占用了差不多 1 秒的时间 (根据 FGCT 列的差得知)。与第一行相比, Full GC 耗费了928 毫秒, 即 92.8% 的时间。
  • 根据 “OC 和 “OU” 列得知, 整个老年代的空间 169,344.0 KB (“OC“), 在 4 次 Full GC 后依然占用了 169,344.2 KB (“OU“)。用了 928ms 的时间却只释放了 800 字节的内存, 怎么看都觉得很不正常。

只看这两行的内容, 就知道程序出了很严重的问题。继续分析下一行, 可以确定问题依然存在, 而且变得更糟。

JVM 几乎完全卡住了(stalled), 因为 GC 占用了 90% 以上的计算资源。GC 之后, 所有的老代空间仍然还在占用。事实上, 程序在一分钟以后就挂了, 抛出了 “java.lang.OutOfMemoryError: GC overhead limit exceeded” 错误。

可以看到, 通过 jstat 能很快发现对 JVM 健康极为不利的 GC 行为。一般来说, 只看 jstat 的输出就能快速发现以下问题:

  • 最后一列 “GCT”, 与 JVM 的总运行时间 “Timestamp” 的比值, 就是 GC 的开销。如果每一秒内, “GCT” 的值都会明显增大, 与总运行时间相比, 就暴露出 GC 开销过大的问题. 不同系统对 GC 开销有不同的容忍度, 由性能需求决定, 一般来讲, 超过 10% 的 GC 开销都是有问题的。
  • YGC” 和 “FGC” 列的快速变化往往也是有问题的征兆。频繁的 GC 暂停会累积, 并导致更多的线程停顿(stop-the-world pauses), 进而影响吞吐量。
  • 如果看到 “OU” 列中, 老年代的使用量约等于老年代的最大容量(OC), 并且不降低的话, 就表示虽然执行了老年代 GC, 但基本上属于无效 GC。

GC 日志(GC logs)

通过日志内容也可以得到 GC 相关的信息。因为 GC 日志模块内置于 JVM 中, 所以日志中包含了对 GC 活动最全面的描述。 这就是事实上的标准, 可作为 GC 性能评估和优化的最真实数据来源。

GC 日志一般输出到文件之中, 是纯 text 格式的, 当然也可以打印到控制台。有多个可以控制 GC 日志的 JVM 参数。例如, 可以打印每次 GC 的持续时间, 以及程序暂停时间(-XX:+PrintGCApplicationStoppedTime), 还有 GC 清理了多少引用类型(-XX:+PrintReferenceGC)。

要打印 GC 日志, 需要在启动脚本中指定以下参数:

-XX:+PrintGCTimeStamps -XX:+PrintGCDateStamps -XX:+PrintGCDetails -Xloggc:<filename>
RUBY

以上参数指示 JVM: 将所有 GC 事件打印到日志文件中, 输出每次 GC 的日期和时间戳。不同 GC 算法输出的内容略有不同. ParallelGC 输出的日志类似这样:

199.879: [Full GC (Ergonomics) [PSYoungGen: 64000K->63998K(74240K)] [ParOldGen: 169318K->169318K(169472K)] 233318K->233317K(243712K), [Metaspace: 20427K->20427K(1067008K)], 0.1473386 secs] [Times: user=0.43 sys=0.01, real=0.15 secs]
200.027: [Full GC (Ergonomics) [PSYoungGen: 64000K->63998K(74240K)] [ParOldGen: 169318K->169318K(169472K)] 233318K->233317K(243712K), [Metaspace: 20427K->20427K(1067008K)], 0.1567794 secs] [Times: user=0.41 sys=0.00, real=0.16 secs]
200.184: [Full GC (Ergonomics) [PSYoungGen: 64000K->63998K(74240K)] [ParOldGen: 169318K->169318K(169472K)] 233318K->233317K(243712K), [Metaspace: 20427K->20427K(1067008K)], 0.1621946 secs] [Times: user=0.43 sys=0.00, real=0.16 secs]
200.346: [Full GC (Ergonomics) [PSYoungGen: 64000K->63998K(74240K)] [ParOldGen: 169318K->169318K(169472K)] 233318K->233317K(243712K), [Metaspace: 20427K->20427K(1067008K)], 0.1547695 secs] [Times: user=0.41 sys=0.00, real=0.15 secs]
200.502: [Full GC (Ergonomics) [PSYoungGen: 64000K->63999K(74240K)] [ParOldGen: 169318K->169318K(169472K)] 233318K->233317K(243712K), [Metaspace: 20427K->20427K(1067008K)], 0.1563071 secs] [Times: user=0.42 sys=0.01, real=0.16 secs]
200.659: [Full GC (Ergonomics) [PSYoungGen: 64000K->63999K(74240K)] [ParOldGen: 169318K->169318K(169472K)] 233318K->233317K(243712K), [Metaspace: 20427K->20427K(1067008K)], 0.1538778 secs] [Times: user=0.42 sys=0.00, real=0.16 secs]
MATHEMATICA

在 “「译文」Java 垃圾收集参考手册(四):Serial GC” 中详细介绍了这些格式, 如果对此不了解, 可以先阅读该章节。

分析以上日志内容, 可以得知:

  • 这部分日志截取自 JVM 启动后 200 秒左右。
  • 日志片段中显示, 在 780 毫秒 以内, 因为垃圾回收 导致了 5 次 Full GC 暂停(去掉第六次暂停, 这样更精确一些)。
  • 这些暂停事件的总持续时间是 777 毫秒, 占总运行时间的 99.6%
  • 在 GC 完成之后, 几乎所有的老年代空间 (169,472 KB) 依然被占用(169,318 KB)。

通过日志信息可以确定, 该应用的 GC 情况非常糟糕。JVM 几乎完全停滞, 因为 GC 占用了超过 99% 的 CPU 时间。 而 GC 的结果是, 老年代空间仍然被占满, 这进一步肯定了我们的结论。 示例程序和 jstat 小节中的是同一个, 几分钟之后系统就挂了, 抛出 “java.lang.OutOfMemoryError: GC overhead limit exceeded” 错误, 不用说, 问题是很严重的.

从此示例可以看出, GC 日志对监控 GC 行为和 JVM 是否处于健康状态非常有用。一般情况下, 查看 GC 日志就可以快速确定以下症状:

  • GC 开销太大。如果 GC 暂停的总时间很长, 就会损害系统的吞吐量。不同的系统允许不同比例的 GC 开销, 但一般认为, 正常范围在 10%以内。
  • 极个别的 GC 事件暂停时间过长。当某次 GC 暂停时间太长, 就会影响系统的延迟指标. 如果延迟指标规定交易必须在 1,000 ms内完成, 那就不能容忍任何超过 1000 毫秒 的 GC 暂停。
  • 老年代的使用量超过限制。如果老年代空间在 Full GC 之后仍然接近全满, 那么 GC 就成为了性能瓶颈, 可能是内存太小, 也可能是存在内存泄漏。这种症状会让 GC 的开销暴增。

可以看到,GC 日志中的信息非常详细。但除了这些简单的小程序, 生产系统一般都会生成大量的 GC 日志, 纯靠人工是很难阅读和进行解析的。

GCViewer

我们可以自己编写解析器, 来将庞大的 GC 日志解析为直观易读的图形信息。 但很多时候自己写程序也不是个好办法, 因为各种 GC 算法的复杂性, 导致日志信息格式互相之间不太兼容。那么神器来了: GCViewer

GCViewer 是一款开源的 GC 日志分析工具。项目的 GitHub 主页对各项指标进行了完整的描述. 下面我们介绍最常用的一些指标。

第一步是获取 GC 日志文件。这些日志文件要能够反映系统在性能调优时的具体场景. 假若运营部门 (operational department) 反馈: 每周五下午, 系统就运行缓慢, 不管 GC 是不是主要原因, 分析周一早晨的日志是没有多少意义的。

获取到日志文件之后, 就可以用 GCViewer 进行分析, 大致会看到类似下面的图形界面:

06_04_gcviewer-screenshot.png

使用的命令行大致如下:

java -jar gcviewer_1.3.4.jar gc.log
MIPSASM

当然, 如果不想打开程序界面, 也可以在后面加上其他参数, 直接将分析结果输出到文件。

命令大致如下:

java -jar gcviewer_1.3.4.jar gc.log summary.csv chart.png
STYLUS

以上命令将信息汇总到当前目录下的 Excel 文件 summary.csv 之中, 将图形信息保存为 chart.png 文件。

点击下载: gcviewer 的 jar 包及使用示例

上图中, Chart 区域是对 GC 事件的图形化展示。包括各个内存池的大小和 GC 事件。上图中, 只有两个可视化指标: 蓝色线条表示堆内存的使用情况, 黑色的 Bar 则表示每次 GC 暂停时间的长短。

从图中可以看到, 内存使用量增长很快。一分钟左右就达到了堆内存的最大值. 堆内存几乎全部被消耗, 不能顺利分配新对象, 并引发频繁的 Full GC 事件. 这说明程序可能存在内存泄露, 或者启动时指定的内存空间不足。

从图中还可以看到 GC 暂停的频率和持续时间。30 秒 之后, GC 几乎不间断地运行, 最长的暂停时间超过1.4 秒

在右边有三个选项卡。“Summary(摘要)” 中比较有用的是 “Throughput”(吞吐量百分比) 和 “Number of GC pauses”(GC 暂停的次数), 以及“Number of full GC pauses”(Full GC 暂停的次数). 吞吐量显示了有效工作的时间比例, 剩下的部分就是 GC 的消耗。

以上示例中的吞吐量为 6.28%。这意味着有 93.72% 的 CPU 时间用在了 GC 上面. 很明显系统所面临的情况很糟糕 —— 宝贵的 CPU 时间没有用于执行实际工作, 而是在试图清理垃圾。

下一个有意思的地方是“Pause”(暂停)选项卡:

06_05_gviewer-screenshot-pause.png

Pause” 展示了 GC 暂停的总时间, 平均值, 最小值和最大值, 并且将 total 与 minor/major 暂停分开统计。如果要优化程序的延迟指标, 这些统计可以很快判断出暂停时间是否过长。另外, 我们可以得出明确的信息: 累计暂停时间为 634.59 秒 , GC 暂停的总次数为 3,938 次, 这在11 分钟 /660 秒 的总运行时间里那不是一般的高。

更详细的 GC 暂停汇总信息, 请查看主界面中的 “Event details” 标签:

06_06_gcviewer-screenshot-eventdetails.png

从“Event details” 标签中, 可以看到日志中所有重要的 GC 事件汇总: 普通 GC 停顿 Full GC 停顿次数, 以及 并发执行数 , 非 stop-the-world 事件 等。此示例中, 可以看到一个明显的地方, Full GC 暂停严重影响了吞吐量和延迟, 依据是: 3,928 次 Full GC, 暂停了634 秒

可以看到, GCViewer 能用图形界面快速展现异常的 GC 行为。一般来说, 图像化信息能迅速揭示以下症状:

  • 低吞吐量。当应用的吞吐量下降到不能容忍的地步时, 有用工作的总时间就大量减少. 具体有多大的 “容忍度”(tolerable) 取决于具体场景。按照经验, 低于 90% 的有效时间就值得警惕了, 可能需要好好优化下 GC。
  • 单次 GC 的暂停时间过长。只要有一次 GC 停顿时间过长, 就会影响程序的延迟指标. 例如, 延迟需求规定必须在 1000 ms 以内完成交易, 那就不能容忍任何一次 GC 暂停超过 1000 毫秒。
  • 堆内存使用率过高。如果老年代空间在 Full GC 之后仍然接近全满, 程序性能就会大幅降低, 可能是资源不足或者内存泄漏。这种症状会对吞吐量产生严重影响。

业界良心 —— 图形化展示的 GC 日志信息绝对是我们重磅推荐的。不用去阅读冗长而又复杂的 GC 日志, 通过容易理解的图形, 也可以得到同样的信息。

分析器(Profilers)

下面介绍分析器 (profilers, Oracle 官方翻译是: 抽样器)。相对于前面的工具, 分析器只关心 GC 中的一部分领域. 本节我们也只关注分析器相关的 GC 功能。

首先警告 —— 不要认为分析器适用于所有的场景。分析器有时确实作用很大, 比如检测代码中的 CPU 热点时。但某些情况使用分析器不一定是个好方案。

对 GC 调优来说也是一样的。要检测是否因为 GC 而引起延迟或吞吐量问题时, 不需要使用分析器. 前面提到的工具 (jstat 或 原生 / 可视化 GC 日志)就能更好更快地检测出是否存在 GC 问题. 特别是从生产环境中收集性能数据时, 最好不要使用分析器, 因为性能开销非常大。

如果确实需要对 GC 进行优化, 那么分析器就可以派上用场了, 可以对 Object 的创建信息一目了然. 换个角度看, 如果 GC 暂停的原因不在某个内存池中, 那就只会是因为创建对象太多了。 所有分析器都能够跟踪对象分配 (via allocation profiling), 根据内存分配的轨迹, 让你知道 实际驻留在内存中的是哪些对象

分配分析能定位到在哪个地方创建了大量的对象. 使用分析器辅助进行 GC 调优的好处是, 能确定哪种类型的对象最占用内存, 以及哪些线程创建了最多的对象。

下面我们通过实例介绍 3 种分配分析器: hprof, JVisualVMAProf。实际上还有很多分析器可供选择, 有商业产品, 也有免费工具, 但其功能和应用基本上都是类似的。

hprof

hprof 分析器 内置于 JDK 之中。 在各种环境下都可以使用, 一般优先使用这款工具。

要让 hprof 和程序一起运行, 需要修改启动脚本, 类似这样:

java -agentlib:hprof=heap=sites com.yourcompany.YourApplication
ROUTEROS

在程序退出时, 会将分配信息 dump(转储)到工作目录下的 java.hprof.txt 文件中。使用文本编辑器打开, 并搜索 “SITES BEGIN” 关键字, 可以看到:

SITES BEGIN (ordered by live bytes) Tue Dec  8 11:16:15 2015
          percent          live          alloc'ed  stack class
 rank   self  accum     bytes objs     bytes  objs trace name
    1  64.43% 4.43%   8370336 20121  27513408 66138 302116 int[]
    2  3.26% 88.49%    482976 20124   1587696 66154 302104 java.util.ArrayList
    3  1.76% 88.74%    241704 20121   1587312 66138 302115 eu.plumbr.demo.largeheap.ClonableClass0006
    ... 部分省略 ...
 
SITES END
1C

从以上片段可以看到, allocations 是根据每次创建的对象数量来排序的。第一行显示所有对象中有 64.43% 的对象是整型数组(int[]), 在标识为 302116 的位置创建。搜索 “TRACE 302116” 可以看到:

TRACE 302116: 
    eu.plumbr.demo.largeheap.ClonableClass0006.<init>(GeneratorClass.java:11)
    sun.reflect.GeneratedConstructorAccessor7.newInstance(<Unknown Source>:Unknown line)
    sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
    java.lang.reflect.Constructor.newInstance(Constructor.java:422)
STYLUS

现在, 知道有 64.43% 的对象是整数数组, 在 ClonableClass0006 类的构造函数中, 第 11 行的位置, 接下来就可以优化代码, 以减少 GC 的压力。

Java VisualVM

本章前面的第一部分, 在监控 JVM 的 GC 行为工具时介绍了 JVisualVM , 本节介绍其在分配分析上的应用。

JVisualVM 通过 GUI 的方式连接到正在运行的 JVM。 连接上目标 JVM 之后 :

  1. 打开 “工具” –> “选项” 菜单, 点击 性能分析(Profiler) 标签, 新增配置, 选择 Profiler 内存, 确保勾选了 “Record allocations stack traces”(记录分配栈跟踪)。
  2. 勾选 “Settings”(设置) 复选框, 在内存设置标签下, 修改预设配置。
  3. 点击 “Memory”(内存) 按钮开始进行内存分析。
  4. 让程序运行一段时间, 以收集关于对象分配的足够信息。
  5. 单击下方的 “Snapshot”(快照) 按钮。可以获取收集到的快照信息。

06_07_01_trace.png

完成上面的步骤后, 可以得到类似这样的信息:

06_07_jvisualvm-top-objects.png

上图按照每个类被创建的对象数量多少来排序。看第一行可以知道, 创建的最多的对象是 int[] 数组. 鼠标右键单击这行, 就可以看到这些对象都在哪些地方创建的:

06_08_jvisualvm-allocation-traces.png

hprof 相比, JVisualVM 更加容易使用 —— 比如上面的截图中, 在一个地方就可以看到所有int[] 的分配信息, 所以多次在同一处代码进行分配的情况就很容易发现。

AProf

最重要的一款分析器, 是由 Devexperts 开发的 AProf。 内存分配分析器 AProf 也被打包为 Java agent 的形式。

用 AProf 分析应用程序, 需要修改 JVM 启动脚本, 类似这样:

java -javaagent:/path-to/aprof.jar com.yourcompany.YourApplication
MIPSASM

重启应用之后, 工作目录下会生成一个 aprof.txt 文件。此文件每分钟更新一次, 包含这样的信息:

========================================================================================================================
TOTAL allocation dump for 91,289 ms (0h01m31s)
Allocated 1,769,670,584 bytes in 24,868,088 objects of 425 classes in 2,127 locations
========================================================================================================================
 
Top allocation-inducing locations with the data types allocated from them
------------------------------------------------------------------------------------------------------------------------
eu.plumbr.demo.largeheap.ManyTargetsGarbageProducer.newRandomClassObject: 1,423,675,776 (80.44%) bytes in 17,113,721 (68.81%) objects (avg size 83 bytes)
    int[]: 711,322,976 (40.19%) bytes in 1,709,911 (6.87%) objects (avg size 416 bytes)
    char[]: 369,550,816 (20.88%) bytes in 5,132,759 (20.63%) objects (avg size 72 bytes)
    java.lang.reflect.Constructor: 136,800,000 (7.73%) bytes in 1,710,000 (6.87%) objects (avg size 80 bytes)
    java.lang.Object[]: 41,079,872 (2.32%) bytes in 1,710,712 (6.87%) objects (avg size 24 bytes)
    java.lang.String: 41,063,496 (2.32%) bytes in 1,710,979 (6.88%) objects (avg size 24 bytes)
    java.util.ArrayList: 41,050,680 (2.31%) bytes in 1,710,445 (6.87%) objects (avg size 24 bytes)
          ... cut for brevity ...
MEL

上面的输出是按照 size 进行排序的。可以看出, 80.44% 的 bytes 和 68.81% 的 objects 是在 ManyTargetsGarbageProducer.newRandomClassObject() 方法中分配的。 其中, int[] 数组占用了 40.19% 的内存, 是最大的一个。

继续往下看, 会发现 allocation traces(分配痕迹)相关的内容, 也是以 allocation size 排序的:

1
2
3
4
5
6
7
8
Top allocated data types with reverse location traces
------------------------------------------------------------------------------------------------------------------------
int[]: 725,306,304 (40.98%) bytes in 1,954,234 (7.85%) objects (avg size 371 bytes)
    eu.plumbr.demo.largeheap.ClonableClass0006.: 38,357,696 (2.16%) bytes in 92,206 (0.37%) objects (avg size 416 bytes)
        java.lang.reflect.Constructor.newInstance: 38,357,696 (2.16%) bytes in 92,206 (0.37%) objects (avg size 416 bytes)
            eu.plumbr.demo.largeheap.ManyTargetsGarbageProducer.newRandomClassObject: 38,357,280 (2.16%) bytes in 92,205 (0.37%) objects (avg size 416 bytes)
            java.lang.reflect.Constructor.newInstance: 416 (0.00%) bytes in 1 (0.00%) objects (avg size 416 bytes)
... cut for brevity ...
STYLUS

可以看到, int[] 数组的分配, 在 ClonableClass0006 构造函数中继续增大。

和其他工具一样, AProf 揭露了 分配的大小以及位置信息(allocation size and locations), 从而能够快速找到最耗内存的部分。在我们看来, AProf 是最有用的分配分析器, 因为它只专注于内存分配, 所以做得最好。 当然, 这款工具是开源免费的, 资源开销也最小。

相关实践学习
日志服务之使用Nginx模式采集日志
本文介绍如何通过日志服务控制台创建Nginx模式的Logtail配置快速采集Nginx日志并进行多维度分析。
相关文章
|
21天前
|
缓存 算法 Java
Java 实现的局域网管控软件的性能调优
局域网管控软件在企业网络管理中至关重要,但随着网络规模扩大和功能需求增加,其性能可能受影响。文章分析了数据处理效率低下、网络通信延迟和资源占用过高等性能瓶颈,并提出了使用缓存、优化算法、NIO库及合理管理线程池等调优措施,最终通过性能测试验证了优化效果,显著提升了软件性能。
30 1
|
29天前
|
算法 Java 测试技术
java性能调优涉及哪些方面
本文详细探讨了性能调优的各个方面,包括Java编程、多线程、JVM监控、设计模式和数据库调优。文章还介绍了性能调优的标准制定、介入时机、系统性能的影响因素,以及如何衡量和判断系统的性能与负载承受能力。最后,提出了性能调优的具体策略,包括代码、设计、算法优化及参数调整,并讨论了限流、智能化扩容等兜底策略。
java性能调优涉及哪些方面
|
2天前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
15 6
|
9天前
|
监控 前端开发 Java
Java SpringBoot –性能分析与调优
Java SpringBoot –性能分析与调优
|
1月前
|
监控 Java Linux
Java 性能调优:调整 GC 线程以获得最佳结果
Java 性能调优:调整 GC 线程以获得最佳结果
67 11
|
16天前
|
监控 安全 Java
Java Z 垃圾收集器如何彻底改变内存管理
大家好,我是V哥。今天聊聊Java的ZGC(Z Garbage Collector)。ZGC是一个低延迟垃圾收集器,专为大内存应用场景设计。其核心优势包括:极低的暂停时间(通常低于10毫秒)、支持TB级内存、使用着色指针实现高效对象管理、并发压缩和去碎片化、不分代的内存管理。适用于实时数据分析、高性能服务器和在线交易系统等场景,能显著提升应用的性能和稳定性。如何启用?只需在JVM启动参数中加入`-XX:+UseZGC`即可。
143 0
|
11天前
|
安全 Java
java 中 i++ 到底是否线程安全?
本文通过实例探讨了 `i++` 在多线程环境下的线程安全性问题。首先,使用 100 个线程分别执行 10000 次 `i++` 操作,发现最终结果小于预期的 1000000,证明 `i++` 是线程不安全的。接着,介绍了两种解决方法:使用 `synchronized` 关键字加锁和使用 `AtomicInteger` 类。其中,`AtomicInteger` 通过 `CAS` 操作实现了高效的线程安全。最后,通过分析字节码和源码,解释了 `i++` 为何线程不安全以及 `AtomicInteger` 如何保证线程安全。
java 中 i++ 到底是否线程安全?
|
1天前
|
存储 安全 Java
Java多线程编程的艺术:从基础到实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及其实现方式,旨在帮助开发者理解并掌握多线程编程的基本技能。文章首先概述了多线程的重要性和常见挑战,随后详细介绍了Java中创建和管理线程的两种主要方式:继承Thread类与实现Runnable接口。通过实例代码,本文展示了如何正确启动、运行及同步线程,以及如何处理线程间的通信与协作问题。最后,文章总结了多线程编程的最佳实践,为读者在实际项目中应用多线程技术提供了宝贵的参考。 ####
|
1天前
|
Java
JAVA多线程通信:为何wait()与notify()如此重要?
在Java多线程编程中,`wait()` 和 `notify()/notifyAll()` 方法是实现线程间通信的核心机制。它们通过基于锁的方式,使线程在条件不满足时进入休眠状态,并在条件满足时被唤醒,从而确保数据一致性和同步。相比其他通信方式,如忙等待,这些方法更高效灵活。 示例代码展示了如何在生产者-消费者模型中使用这些方法实现线程间的协调和同步。
7 3
|
1天前
|
Java
java小知识—进程和线程
进程 进程是程序的一次执行过程,是系统运行的基本单位,因此进程是动态的。系统运行一个程序即是一个进程从创建,运行到消亡的过程。简单来说,一个进程就是一个执行中的程序,它在计算机中一个指令接着一个指令地执行着,同时,每个进程还占有某些系统资源如CPU时间,内存空间,文件,文件,输入输出设备的使用权等等。换句话说,当程序在执行时,将会被操作系统载入内存中。 线程 线程,与进程相似,但线程是一个比进程更小的执行单位。一个进程在其执行的过程中产生多个线程。与进程不同的是同类的多个线程共享同一块内存空间和一组系统资源,所以系统在产生一个线程,或是在各个线程之间做切换工作时,负担要比
6 1
下一篇
无影云桌面