探针
CPU 和内存分析主要关注对象和方法调用,这是 JVM 上应用程序的基本构建块。对于某些技术,需要一种更高级别的方法来从运行的应用程序中提取语义数据并在分析器中显示。
最显著的例子是使用 JDBC 对数据库调用进行分析。调用树显示您何时使用 JDBC API 以及这些调用需要多长时间。然而,不同的 SQL 语句可能会为每个调用执行,您不知道哪些调用导致了性能瓶颈。此外,JDBC 调用通常源自应用程序中的许多不同位置,因此重要的是要有一个显示所有数据库调用的单一视图,而不是在通用调用树中搜索它们。
为了解决这个问题,JProfiler 为 JRE 中的重要子系统提供了许多探针。探针在特定类中添加了检测,以收集它们的数据并在“数据库”和“JEE & Probes”视图部分的专用视图中显示。此外,探针可以将数据注释到调用树中,以便您可以同时看到通用 CPU 分析和高级数据。
如果您有兴趣获取 JProfiler 不直接支持的技术的更多信息,您可以为其编写自己的探针。一些库、容器或数据库驱动程序可能会附带自己的嵌入式探针,当您的应用程序使用它们时,这些探针会在 JProfiler 中可见。
探针事件
由于探针会增加开销,因此默认情况下不会记录它们,但您必须为每个探针单独开始记录,可以手动或自动进行。
根据探针的功能,探针数据会显示在多个视图中。最低级别是探针事件。其他视图显示累积探针事件的数据。默认情况下,即使正在记录探针,也不会保留探针事件。当单个事件变得重要时,您可以在探针事件视图中记录它们。对于某些探针,如文件探针,通常不建议这样做,因为它们通常以高频率生成事件。其他探针,如“HTTP 服务器”探针或 JDBC 探针,可能会以较低的频率生成事件,因此记录单个事件可能是合适的。
探针事件从多种来源捕获探针字符串,包括方法参数、返回值、检测对象和抛出的异常。探针可以从多个方法调用中收集数据,例如,JDBC 探针必须拦截所有预处理语句的 setter 调用以构建实际的 SQL 字符串。探针字符串是探针测量的高级子系统的基本信息。此外,事件包含开始时间、可选的持续时间、关联的线程和堆栈跟踪。
在表格的底部,有一行特殊行显示显示的事件总数并汇总表格中的所有数字列。对于默认列,这仅包括持续时间列,结合表格上方的过滤器选择器,您可以分析所选事件子集的收集数据。默认情况下,文本过滤器适用于所有文本字段列,但您可以从文本字段前的下拉菜单中选择特定的过滤列。过滤选项也可以从上下文菜单中获得,例如,过滤所有持续时间大于所选事件的事件。
其他探针视图也提供过滤探针事件的选项:在探针遥测视图中,您可以选择一个时间范围;在探针调用树视图中,您可以过滤来自所选调用栈的事件;探针热点视图提供基于所选回溯或热点的探针事件过滤器;控制对象和时间线视图提供操作以过滤所选控制对象的探针事件。
所选探针事件的堆栈跟踪显示在底部。如果选择了多个探针事件,则堆栈跟踪会累积并显示为调用树、带有回溯的探针热点或带有回溯的 CPU 热点。
在堆栈跟踪视图旁边,显示事件持续时间的直方图视图和可选的记录吞吐量。您可以使用鼠标在这些直方图中选择一个持续时间范围,以便在上面的表格中过滤探针事件。
探针可以记录不同类型的活动,并将事件类型与其探针事件关联。例如,JDBC 探针显示语句、预处理语句和批处理执行作为具有不同颜色的事件类型。
为了防止在记录单个事件时过度使用内存,JProfiler 会合并事件。事件上限在分析设置中配置,并适用于所有探针。仅保留最新的事件,较旧的事件会被丢弃。这种合并不会影响高级视图。
探针调用树和热点
探针记录与 CPU 记录密切配合。探针事件被聚合到探针调用树中,其中探针字符串是叶节点,称为“有效负载”。仅包含创建了探针事件的调用栈才会包含在该树中。方法节点上的信息指的是记录的有效负载名称。例如,如果在特定调用栈上执行了 42 次 SQL 语句,总时间为 9000 毫秒,这会将事件计数 42 和时间 9000 毫秒添加到所有祖先调用树节点。所有记录的有效负载的累积形成调用树,显示哪些调用路径消耗了大部分探针特定的时间。探针树的重点是有效负载,因此视图过滤器默认搜索有效负载,尽管其上下文菜单也提供过滤类的模式。
如果关闭 CPU 记录,回溯将仅包含一个“未记录 CPU 数据”节点。如果仅部分记录了 CPU 数据,可能会混合这些节点与实际回溯。即使启用了采样,JProfiler 默认情况下也会记录探针有效负载的精确调用跟踪。如果您想避免这种开销,可以在分析设置中关闭它。探针记录还有其他几个调整选项,可以调整以增加数据收集或减少开销。
可以从探针调用树中计算热点。热点节点现在是有效负载,而不是CPU 视图部分
中的方法调用。这通常是探针最直接有用的视图。如果 CPU 记录处于活动状态,您可以打开顶级热点并分析方法回溯,就像在常规
CPU 热点视图中一样。回溯节点上的数字指示沿从最深节点到热点下方节点的调用栈测量了多少探针事件及其总持续时间。
探针调用树和探针热点视图都允许您选择线程或线程组、线程状态和方法节点的聚合级别,就像在相应的 CPU 视图中一样。当您从 CPU 视图中比较数据时,重要的是要记住探针视图中的默认线程状态是“所有状态”,而不是 CPU 视图中的“可运行”。这是因为探针事件通常涉及外部系统,如数据库调用、socket 操作或进程执行,重要的是查看总时间,而不仅仅是当前 JVM 在其上花费的时间。
控制对象
许多提供对外部资源访问的库为您提供一个连接对象,您可以使用它与资源进行交互。例如,当启动一个进程时,java.lang.Process
对象允许您从输出流中读取并写入输入流。使用 JDBC
时,您需要一个 java.sql.Connection
对象来执行 SQL 查询。在 JProfiler 中用于此类对象的通用术语是“控制对象”。
将探针事件与其控制对象分组并显示其生命周期可以帮助您更好地理解问题的来源。此外,创建控制对象通常很昂贵,因此您需要确保您的应用程序不会创建太多并正确关闭它们。为此,支持控制对象的探针具有“时间线”和“控制对象”视图,其中后者可能更具体地命名,例如,JDBC 探针的“连接”。当控制对象打开或关闭时,探针会创建特殊的探针事件,这些事件显示在事件视图中,以便您可以检查关联的堆栈跟踪。
在时间线视图中,每个控制对象显示为一个条,其着色显示控制对象何时处于活动状态。探针可以记录不同的事件类型,时间线会相应地着色。此状态信息不是从事件列表中获取的,事件列表可能已合并或甚至不可用,而是每 100 毫秒从最后一个状态中采样。控制对象有一个名称,允许您识别它们。例如,文件探针使用文件名创建控制对象,而 JDBC 探针显示连接字符串作为控制对象的名称。
控制对象视图以表格形式显示所有控制对象。默认情况下,打开和关闭的控制对象都存在。您可以使用顶部的控件将显示限制为仅打开或关闭的控制对象,或过滤特定列的内容。除了控制对象的基本生命周期数据外,表格还显示每个控制对象的累积活动数据,例如事件计数和平均事件持续时间。
不同的探针在此处显示不同的列,例如,进程探针显示用于读取和写入事件的单独列集。如果禁用单个事件记录,此信息也可用。就像事件视图一样,底部的总行可以与过滤一起使用,以获取部分控制对象集的累积数据。
探针可以在嵌套表中发布某些属性。这是为了减少主表中的信息过载,并为表列提供更多空间。如果存在嵌套表,例如文件和进程探针,每行左侧都有一个展开句柄,可以在原地打开属性值表。
时间线、控制对象视图和事件视图通过导航操作连接。例如,在时间线视图中,您可以右键单击一行并跳转到其他视图,以便仅显示所选控制对象的数据。这是通过将控制对象 ID 过滤到所选值来实现的。
遥测和跟踪器
从探针收集的累积数据中记录了多个遥测。对于任何探针,每秒的探针事件数和探针事件的一些平均度量(如平均持续时间或 I/O 操作的吞吐量)都是可用的。对于具有控制对象的探针,打开的控制对象数量也是一个标准遥测。每个探针可以添加额外的遥测,例如,JPA 探针显示查询计数和实体操作计数的单独遥测。
热点视图和控制对象视图显示累积数据,这些数据可能在一段时间内跟踪很有趣。这些特殊的遥测由探针跟踪器记录。设置跟踪的最简单方法是从热点或控制对象视图中使用添加选择到跟踪器操作添加新的遥测。在这两种情况下,您都必须选择是要跟踪时间还是计数。当跟踪控制对象时,遥测是所有不同探针事件类型的堆叠区域图。对于跟踪的热点,跟踪的时间被分成不同的线程状态。
探针遥测可以添加到“遥测”部分,以便将它们与系统遥测或自定义遥测进行比较。然后,您还可以通过遥测概览中的上下文菜单操作控制探针记录。
JDBC 和 JPA
JDBC 和 JPA 探针协同工作。在 JPA 探针的事件视图中,您可以展开单个事件以查看关联的 JDBC 事件(如果 JDBC 探针与 JPA 探针一起记录)。
类似地,热点视图向所有热点添加一个特殊的“JDBC 调用”节点,其中包含由 JPA 操作触发的 JDBC 调用。一些 JPA 操作是异步的,并不会立即执行,而是在会话刷新时的某个任意时间点执行。在寻找性能问题时,该刷新堆栈跟踪没有帮助,因此 JProfiler 记住了现有实体的获取位置或新实体的持久化位置的堆栈跟踪,并将它们与探针事件联系起来。在这种情况下,热点的回溯包含在标记为“延迟操作”的节点中,否则会插入一个“直接操作”节点。
其他探针(如 MongoDB 探针)支持直接和异步操作。异步操作不是在当前线程上执行,而是在同一 JVM 中的一个或多个其他线程上或在另一个进程中执行。对于此类探针,热点中的回溯被排序到“直接操作”和“异步操作”容器节点中。
JDBC 探针中的一个特殊问题是,如果 SQL 字符串中包含字面数据(如 ID),您只能获得良好的热点。如果使用预处理语句,这将自动发生,但如果执行常规语句,则不会。在后一种情况下,您可能会得到一个热点列表,其中大多数查询仅执行一次。作为补救措施,JProfiler 在 JDBC 探针配置中提供了一个非默认选项,用于替换未准备好的语句中的字面量。出于调试目的,您可能仍然希望在事件视图中看到字面量。禁用该选项可以减少内存开销,因为 JProfiler 不必缓存那么多不同的字符串。
另一方面,JProfiler 收集预处理语句的参数,并在事件视图中显示完整的 SQL 字符串而不带占位符。同样,这在调试时很有用,但如果您不需要它,可以在探针设置中关闭它以节省内存。
JDBC 连接泄漏
JDBC 探针具有“连接泄漏”视图,显示未返回到其数据库池的打开虚拟数据库连接。这仅影响由池化数据库源创建的虚拟连接。虚拟连接会阻塞物理连接,直到它们关闭。
有两种类型的泄漏候选者,“未关闭”连接和“未关闭收集”连接。两种类型都是虚拟连接,数据库池发放的连接对象仍在堆上,但尚未调用close()
。已被垃圾收集的“未关闭收集”连接是确定的连接泄漏。
“未关闭”连接对象仍在堆上。自打开以来的持续时间越长,这样的虚拟连接就越可能是泄漏候选者。当虚拟连接已打开超过 10 秒时,它被视为潜在泄漏。然而,close()
仍然可以在其上调用,然后“连接泄漏”视图中的条目将被移除。
连接泄漏表包括一个类名列,显示连接类的名称。这将告诉您哪个类型的池创建了连接。JProfiler 明确支持大量数据库驱动程序和连接池,并知道哪些类是虚拟和物理连接。对于未知的池或数据库驱动程序,JProfiler 可能会误将物理连接视为虚拟连接。由于物理连接通常是长寿命的,因此它会显示在“连接泄漏”视图中。在这种情况下,连接对象的类名将帮助您将其识别为误报。
默认情况下,当您开始探针记录时,连接泄漏分析未启用。在连接泄漏视图中有一个单独的记录按钮,其状态对应于 JDBC 探针设置中的记录打开的虚拟连接以进行连接泄漏分析复选框。就像事件记录一样,按钮的状态是持久的,因此如果您启动分析一次,它将自动为下一个探针记录会话启动。
调用树中的有效负载数据
在查看 CPU 调用树时,查看探针记录的有效负载数据的位置很有趣。这些数据可能会帮助您解释测量的 CPU 时间。这就是为什么许多探针在 CPU 调用树中添加交叉链接的原因。例如,类加载器探针可以显示类加载触发的位置。这在调用树中是不可见的,并且可能会增加意外的开销。调用树视图中不透明的数据库调用可以通过单击进一步在相应的探针中分析。这甚至适用于调用树分析,当您单击探针链接时,分析会在探针调用树视图的上下文中自动重复。
另一种可能性是直接在 CPU 调用树中内联显示有效负载信息。所有相关探针在其配置中都有一个在调用树中注释选项。这样就没有可用的探针调用树链接。每个探针都有自己的有效负载容器节点。具有相同有效负载名称的事件被聚合,并显示调用次数和总时间。有效负载名称在每个调用栈的基础上合并,最旧的条目被聚合到一个“[早期调用]”节点中。每个调用栈记录的有效负载名称的最大数量可以在分析设置中配置。
调用树拆分
一些探针不使用其探针字符串将有效负载数据注释到调用树中。相反,它们为每个不同的探针字符串拆分调用树。这对于服务器类型的探针特别有用,您希望为每种不同类型的传入请求分别查看调用树。“HTTP 服务器”探针拦截 URL,并为您提供对 URL 的哪些部分用于拆分调用树的细粒度控制。默认情况下,它仅使用请求 URI 路径而不带任何参数。
为了更灵活,您可以定义一个脚本来确定拆分字符串。在脚本中,您可以将当前javax.servlet.http.HttpServletRequest
作为参数并返回所需的字符串。
更重要的是,您不仅限于单个拆分级别,还可以定义多个嵌套拆分。例如,您可以先按请求 URI 路径拆分,然后按从 HTTP 会话对象中提取的用户名拆分。或者,您可以按请求方法对请求进行分组,然后按请求 URI 拆分。
通过使用嵌套拆分,您可以在调用树中查看每个级别的单独数据。在查看调用树时,某个级别可能会妨碍您,您可能需要在“HTTP 服务器”探针配置中消除它。更方便的是,在不丢失记录数据的情况下,您可以通过在相应的拆分节点上使用上下文菜单,动态合并和取消合并调用树中的拆分级别。
拆分调用树可能会导致相当大的内存开销,因此应谨慎使用。为了避免内存过载,JProfiler 限制了拆分的最大数量。如果特定拆分级别的拆分上限已达到,将添加一个特殊的“[受限节点]”拆分节点,并带有一个超链接以重置上限计数器。如果默认上限对您的用途来说太低,您可以在分析设置中增加它。