探针
CPU和内存分析主要涉及对象和方法调用,这是JVM上应用程序的基本构件。对于某些技术,需要采用更高级别的方法, 从运行的应用程序中提取语义数据,并将其显示在分析器中。
这种情况最典型的例子就是分析JDBC对数据库的调用。调用树显示了你何时使用JDBC API,以及这些调用花费了多长时间。 然而,每次调用可能会执行不同的SQL语句,而你并不知道这些调用中的哪些调用是造成性能瓶颈的原因。 此外,通常JDBC调用从应用程序中许多不同位置发起,因此,拥有一个显示所有数据库调用的单一视图很重要, 而不是必须在普通调用树中搜索它们。
为了解决这个问题,JProfiler为JRE中的重要子系统提供了一些探针。探针将instrumentation添加到特定的类中,以便收集其数据, 并在"数据库"和"JEE & 探针"视图部分的专用视图中显示。此外,探针还可以将数据注解到调用树中, 这样你就可以同时看到普通的CPU分析以及高级数据。
如果你要获取的更多技术信息JProfiler不支持,你可以为其编写 你自己的探针。 一些库、容器或数据库驱动程序可能会附带他们自己的嵌入式探针, 当它们被你的应用程序使用时,在JProfiler中可以看到。
探针事件
因为探针会增加开销,所以默认情况下不会记录, 但你必须为每个探针分别开始记录,无论是手动还是自动。
根据探针的功能,探针数据显示在多个视图中。最低级别的是探针事件。其他视图显示累积探针事件的数据。 默认情况下,即使在记录探针时,也不会保留探针事件。当单个事件变得重要时,你可以在探针事件视图中记录它们。 对于某些探针,如文件探针,一般不建议这样做,因为它们通常会以很高的速度生成事件。 其他探针,如“HTTP服务器”探针或JDBC探针,可能会以低得多的速度生成事件,因此记录单个事件可能是合适的。
探针事件从各种来源捕获探针字符串,包括方法参数、返回值、被插入指令的对象和抛出的异常。 探针可能会从多个方法调用中收集数据,例如,像JDBC探针必须拦截所有PreparedStatement语句的setter调用才能构造实际的SQL字符串。 探针字符串是探针所测量的上层子系统的基本信息。此外,一个事件还包含一个开始时间、一个可选的持续时间、相关的线程和一个堆栈跟踪。
在表格的底部,有一个特殊的行,它展示显示的事件总数,以及表格中所有数字列的总和。默认列中,只有持续时间列是数值型, 通过与表格上方的过滤器选择器一起,你可以分析收集到的选定子集事件的数据。默认情况下,文本过滤器应用于所有文本字段列,但是你可以从文本 字段前面的下拉列表中选择一个特定过滤列。上下文菜单中也有过滤选项,例如,过滤出持续时间大于选定事件持续时间的所有事件。
其他探针视图也提供过滤探针事件的选项:在探针遥测视图中,你可以选择一个事件范围;在探针调用树视图中,你可以从选定调用堆栈中 过滤事件;探针热点视图提供了基于选定回溯跟踪或热点和控制对象的探针事件过滤器;时间线视图为选定控制对象提供了过滤探针事件的操作。
选定探针事件的堆栈跟踪显示在底部。如果选择了多个探针事件,堆栈跟踪将被累积,然后以调用树,带有回溯跟踪的探针热点或以带有回溯跟踪 的CPU热点显示。
在堆栈跟踪视图旁边,还显示了事件持续时间和可选的记录吞吐量的直方图视图。你可以使用鼠标选择这些直方图中持续时间的范围 以过滤上方表格中的探针事件。
探针可以记录不同种类的活动,并将事件类型与其探针事件相关联,例如JDBC探针将Statement、 PreparedStatement和批量执行作为事件类型以不同颜色显示。
为了防止在记录单个事件时过度使用内存,JProfiler会合并事件。事件上限是在分析设置中配置的,应用于所有探针。 只有最近的事件被保留,旧的事件会被丢弃。这种合并不会影响更高级别的视图。
探针调用树和热点
探针记录与CPU记录紧密配合。探针事件被聚合成探针调用树,其中探针字符串是叶节点,称为"有效负载"。 只有创建了探针事件的调用堆栈才会被包含在该树中。方法节点上的信息指的是记录的有效负载名称。 例如,如果一条SQL语句在某个调用堆栈执行了42次,总时间为9000毫秒,那么这就将事件数为42, 时间为9000毫秒的事件添加到所有祖先调用树节点上。所有记录的有效负载的累积形成了调用树, 它向你展示了哪些调用路径消耗了大部分特定探针的时间。探针树的重点是有效负载,因此视图过滤器默认搜索有效负载, 尽管其上下文菜单也提供了过滤类的模式。
如果CPU记录被关闭,回溯跟踪将只包含一个"没有记录CPU数据"的节点。如果CPU数据只有部分被记录,可能会有这些节点与实际回溯跟踪的混合。 即使启用了采样,JProfiler也会默认记录探针有效负载的精确调用跟踪。如果你想避免这个开销,可以在分析设置中关闭它。 探针记录还有其他几个调整选项,可以调整这些选项来增加数据收集或减少开销。
热点可以从探针调用树计算出来。热点节点现在是有效负载,而不是像CPU视图部分中那样的方法调用。 这往往是探针最直接有用的视图。如果CPU记录处于活动状态,你可以打开顶层热点并分析方法回溯跟踪,就像在常规CPU热点视图中一样。 回溯跟踪节点上的数字表示沿着从最深的节点到热点下方的节点延伸的调用堆栈测量了多少个探针事件,总持续时间是多少。
探针调用树以及探针热点视图都允许你选择一个线程或线程组、线程状态以及方法节点的聚合级别,就像在相应的CPU视图中一样。 当你从CPU视图中来比较数据时,需要注意的是,探针视图中默认的线程状态是"所有状态",而不是像CPU视图中的"就绪(Runnable)"。 这是因为一个探针事件往往涉及到外部系统,如数据库调用、Socket操作或进程执行,在这些情况下,重要的是要看总的时间, 而不是只看当前JVM在上面工作的时间。
控制对象
许多提供访问外部资源的库都会给你一个连接对象,你可以用它来与资源进行交互。例如,当启动一个进程时,
java.lang.Process
对象可以让你从输出流中读取并向输入流写入。当使用JDBC时,
你需要一个java.sql.Connection
对象来执行SQL查询。在JProfiler中,这种对象的通用术语叫做"控制对象"。
将探针事件通过它们的控制对象进行分组,并显示其生命周期,可以帮助你更好地了解问题的来源。另外,创建控制对象通常成本很昂贵, 所以你要确保你的应用程序不会创建太多,并正确关闭它们。为此,支持控制对象的探针有一个"时间线"和一个"控制对象"视图, 其中后者的命名可能更具体,例如,JDBC探针的"连接"。当打开或关闭控制对象时,探针会创建特殊的探针事件,这些事件会显示在事件视图中, 因此你可以检查相关的堆栈跟踪。
在时间线视图中,每个控制对象显示为一个条形图,其着色显示控制对象何时处于活动状态。探针可以记录不同的事件类型,时间线也会相应地着色。 这种状态信息不是从事件列表中获取的,事件列表可能是合并的,甚至是不可用的,而是从最后状态中每100毫秒采样一次获取的。 控制对象有一个名称,可以用来识别它们,例如,文件探针以文件名创建控制对象,而JDBC探针则将连接字符串显示为控制对象的名称。
控件对象视图以表格形式显示所有控制对象。默认情况下,打开和关闭的控制对象都会显示。你可以使用顶部的控件将显示限制为只显示打开或关闭的控制对象, 或过滤特定列的内容。除了控制对象的基本生命周期数据外,该表还显示了每个控制对象的累计活动数据,例如,事件数和平均事件持续时间。
不同的探针在这里显示不同的列,例如,进程探针,就为读和写事件显示为单独的列。如果禁用单个事件记录,也可以获得这些信息。 就像事件视图一样,底部的总和行可以和过滤一起使用,获得部分控制对象集的累积数据。
探针可以在嵌套表中发布某些属性。这样做是为了减少主表的信息过载,给表列提供更多的空间。如果有嵌套表,如文件和进程探针, 每一行的左侧都有一个扩展柄,可以就地打开一个属性值表。
时间线、控制对象视图和事件视图通过导航操作相连接。例如,在时间线视图中,你可以右键单击某行并跳转到其他每个视图, 以便只显示所选控制对象的数据。这是通过过滤控制对象 ID 为选定的值来实现的。
遥测和跟踪器
每个探针收集的累积数据中,记录了若干遥测数据。对于任何一个探针,都有每秒的探针事件数和一些探针事件的平均测量值, 如平均持续时间或I/O操作的吞吐量。对于带有控制对象的探针,打开的控制对象的数量也是一个典型的遥测数据。 每个探针可以添加额外的遥测数据,例如,JPA探针可以显示查询次数和实体操作次数的单独遥测数据。
热点视图和控制对象视图显示的累积数据可以被跟踪一段时间内的动态变化。这些特殊的遥测可以通过探针跟踪器记录。 设置跟踪的最简单方法是从热点或控制对象视图通过向跟踪器添加所选内容操作添加新遥测。 在这两种情况下,你必须选择是要跟踪时间还是计数。当跟踪控制对象时,该遥测是一个所有不同探针事件类型的叠加面积图。 对于跟踪热点,跟踪的时间会被拆分为不同线程状态。
探针遥测可以被添加到“遥测”部分,以便与系统遥测或自定义遥测 进行比较。然后你也可以通过遥测概览中的上下文菜单操作控制探针记录。
JDBC和JPA
JDBC探针和JPA探针携手合作。在JPA探针的事件视图中,如果JDBC探针与JPA探针一起被记录,则可以展开单个事件, 查看相关的JDBC事件。
类似地,热点视图为所有热点添加了一个特殊的"JDBC调用"节点,该节点包含由JPA操作触发的JDBC调用。 有些JPA操作是异步的,并不是立即执行的,而是在会话刷新后的某个任意时间点执行的。 当寻找性能问题时,该刷新的堆栈跟踪是没有帮助的,所以JProfiler会记住现有实体被获取的地方或新实体被持久化的地方的堆栈跟踪, 并将它们与探针事件联系起来。在这种情况下,热点的回溯跟踪被包含在一个标记为"延迟操作"的节点内, 而不是插入一个"直接操作"节点。
其他探针如MongoDB探针支持直接操作和异步操作。异步操作不在当前线程上执行,而是在其他地方执行, 可以是在同一个JVM中的一个或多个其他线程上,也可以是在另一个进程中。对于这样的探针,热点中的回溯跟踪会被分类 到"直接操作"和"异步操作"容器节点。
JDBC探针中的一个特殊问题是,只有在SQL字符串中不包含ID等常量文字数据的情况下,你才能得到好的热点。 如果使用预编译(PreparedStatement)语句,就会自动出现这种情况,但如果执行常规语句,就不会出现这种情况。 在后一种情况下,你很可能会得到一个热点列表,其中大多数查询只执行一次。作为一种改进措施, JProfiler在JDBC探针配置中提供了一个非默认选项,用于替换非预编译(PreparedStatement)语句中的常量文字。 出于调试的目的,你可能仍然希望在事件视图中查看常量文字。停用该选项可以减少内存开销,因为JProfiler将不必缓存这么多不同的字符串。
另一方面,JProfiler收集预编译(PreparedStatement)语句的参数,并在事件视图中显示一个完整的没有占位符的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对拆分的最大数量进行了限制。 如果已经达到了某个拆分级别的拆分上限,则会添加一个特殊的"[超过的节点]"拆分节点,并带有一个超链接来重置上限计数器。 如果默认的上限值对你来说太低,你可以在分析设置中增加。