如何低开销的监控JVM对象分配及分配对象的线程

开发 前端
JTreg框架中针对该特性有16个测试:使用多个线程打开/关闭,同时分配多个线程,测试数据是否以正确的间隔采样,以及收集的堆栈是否反映正确的程序信息。

概要

提供一种低开销的Java堆分配采样方式,可通过JVMTI访问。

目标

提供一种从JVM获取Java对象堆分配信息的方法:

  • 开销足够低,可以在默认情况下连续启用,
  • 可以通过定义良好的编程接口访问,
  • 可以对所有的分配进行抽样(即,不局限于一个特定堆区域中的分配或以一种特定方式分配的分配),
  • 可以以一种与实现无关的方式定义(即,不依赖于任何特定的GC算法或VM实现),以及
  • 可以提供有关活的和死的Java对象的信息。

动机

用户非常需要理解堆的内容。糟糕的堆管理可能会导致堆耗尽和GC抖动等问题。因此,人们开发了许多工具来允许用户自省他们的堆,例如Java Flight Recorder、jmap、YourKit和VisualVM工具。

大多数现有工具缺少的一个信息是特定分配的调用站点。堆转储和堆直方图不包含此信息。此信息对于调试内存问题非常重要,因为它告诉开发人员代码中发生特定(特别糟糕的)分配的确切位置。

目前有两种方法从热点获取这些信息:

  • 首先,您可以使用字节码重写器(例如Allocation Instrumenter)来检测应用程序中的所有分配。然后,您可以让插装进行堆栈跟踪(当您需要时)。
  • 其次,您可以使用Java Flight Recorder,它在TLAB重新填充和直接分配到老一代时进行堆栈跟踪。这样做的缺点是:a)它绑定到特定的分配实现(TLABs),并且错过了不符合该模式的分配;B)它不允许用户自定义采样间隔;c)它只记录分配,所以你无法区分活对象和死对象。

该建议通过提供可扩展的JVMTI接口来缓解这些问题,该接口允许用户定义采样间隔并返回一组活动堆栈跟踪。

描述

新的JVMTI事件和方法

这里提出的面向用户的堆采样特性API由JVMTI的扩展组成,该扩展允许进行堆分析。以下系统依赖于提供回调的事件通知系统,例如:

void JNICALL
SampledObjectAlloc(jvmtiEnv *jvmti_env,
JNIEnv* jni_env,
jthread thread,
jobject object,
jclass object_klass,
jlong size)

说明:

  • thread是分配对象的线程
  • object是对采样对象的引用
  • object_klass是jobject的类
  • size是分配的大小

新的API还包括一个新的JVMTI方法:

jvmtiError  SetHeapSamplingInterval(jvmtiEnv* env, jint sampling_interval)

其中sampling_interval是两次采样之间分配的平均字节数。该方法的规格为:

  • 如果不为零,采样间隔将被更新,并将用sampling_interval字节的新平均采样间隔发送回调给用户
  • 例如,如果用户希望每兆字节采样一次,则sampling_interval将是1024 * 1024。
  • 如果将0传递给方法,采样器在考虑到新的间隔后对每个分配进行采样,这可能需要一定数量的分配

注意,采样间隔是不精确的。每次出现一个样本时,在下一个样本被选择之前的字节数将是给定平均间隔的伪随机。这是为了避免抽样偏差;例如,如果相同的分配每512KB发生一次,512KB采样间隔将始终对相同的分配进行采样。因此,虽然采样间隔并不总是选择的间隔,但在大量的样本之后,它会趋向于它。

用例示例

要启用此功能,用户将使用通常的事件通知调用来操作:

jvmti->SetEventNotificationMode(jvmti, JVMTI_ENABLE, JVMTI_EVENT_SAMPLED_OBJECT_ALLOC, NULL)

该事件将在分配初始化并正确设置时发送,因此略晚于实际代码执行分配之后。缺省情况下,平均采样间隔为512KB。

启用采样事件系统的最低要求是使用JVMTI_ENABLE和事件类型
JVMTI_EVENT_SAMPLED_OBJECT_ALLOC调用SetEventNotificationMode。要修改采样间隔,用户调用SetHeapSamplingInterval方法。

禁用方式,

jvmti->SetEventNotificationMode(jvmti, JVMTI_DISABLE, JVMTI_EVENT_SAMPLED_OBJECT_ALLOC, NULL)

禁用事件通知并自动禁用采样器。

通过SetEventNotificationMode再次调用采样器将使用当前设置的采样间隔重新启用采样器(默认为512KB或用户通过SetHeapSamplingInterval传递的最后一个值)。

新功能

为了保护新特性并使其成为VM实现的可选特性,在jvmtiCapabilities中引入了名为
can_generate_sampled_object_alloc_events的新功能。

全局/线程级采样

使用通知系统提供了一种仅为特定线程发送事件的直接方法。这是通过SetEventNotificationMode完成的,并提供第三个参数,其中包含要修改的线程。

完整的例子

下面的部分提供代码片段来演示采样器的API。首先,启用功能和事件通知:

jvmtiEventCallbacks callbacks;
memset(&callbacks, 0, sizeof(callbacks));
callbacks.SampledObjectAlloc = &SampledObjectAlloc;

jvmtiCapabilities caps;
memset(&caps, 0, sizeof(caps));
caps.can_generate_sampled_object_alloc_events = 1;
if (JVMTI_ERROR_NONE != (*jvmti)->AddCapabilities(jvmti, &caps)) {
return JNI_ERR;
}

if (JVMTI_ERROR_NONE != (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE,
JVMTI_EVENT_SAMPLED_OBJECT_ALLOC, NULL)) {
return JNI_ERR;
}

if (JVMTI_ERROR_NONE != (*jvmti)->SetEventCallbacks(jvmti, &callbacks, sizeof(jvmtiEventCallbacks)) {
return JNI_ERR;
}

// Set the sampler to 1MB.
if (JVMTI_ERROR_NONE != (*jvmti)->SetHeapSamplingInterval(jvmti, 1024 * 1024)) {
return JNI_ERR;
}

禁用采样器(禁用事件和采样器):

if (JVMTI_ERROR_NONE != (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_DISABLE,
JVMTI_EVENT_SAMPLED_OBJECT_ALLOC, NULL)) {
return JNI_ERR;
}

要重新启用1024 * 1024字节采样间隔的采样器,需要一个简单的调用来启用事件:

if (JVMTI_ERROR_NONE != (*jvmti)->SetEventNotificationMode(jvmti, JVMTI_ENABLE,
JVMTI_EVENT_SAMPLED_OBJECT_ALLOC, NULL)) {
return JNI_ERR;
}

抽样分配的用户存储

当事件生成时,回调可以使用JVMTI GetStackTrace方法捕获堆栈跟踪。回调获得的jobject引用也可以包装成JNI弱引用,以帮助确定对象何时已被垃圾收集。这种方法允许用户收集关于采样对象的数据,以及仍然被认为是活动的对象的数据,这是了解作业行为的好方法。

例如,可以这样做:

extern "C" JNIEXPORT void JNICALL SampledObjectAlloc(jvmtiEnv *env,
JNIEnv* jni,
jthread thread,
jobject object,
jclass klass,
jlong size) {
jvmtiFrameInfo frames[32];
jint frame_count;
jvmtiError err;

err = global_jvmti->GetStackTrace(NULL, 0, 32, frames, &frame_count);
if (err == JVMTI_ERROR_NONE && frame_count >= 1) {
jweak ref = jni->NewWeakGlobalRef(object);
internal_storage.add(jni, ref, size, thread, frames, frame_count);
}
}

如果internal_storage是一个可以处理采样对象的数据结构,请考虑是否需要清理任何垃圾收集的样本,等等。该实现的内部是特定于使用的,超出了这个JEP的范围。

采样间隔可以用作减少分析开销的一种手段。使用512KB的采样间隔,开销应该足够低,用户可以合理地在默认情况下打开系统。

实现细节

目前的原型和实现证明了该方法的可行性。它包括五个部分:

  1. 由于ThreadLocalAllocationBuffer (TLAB)结构中字段名称的更改,导致架构相关的更改。这些更改是最小的,因为它们只是名称更改。
  2. TLAB结构增加了一个新的allocation_end指针,以补充现有的结束指针。如果禁用采样,则两个指针始终相等,代码将像以前一样执行。如果启用了采样,end将被修改为请求下一个采样点的位置。然后,任何快速路径都会“认为”TLAB在此时已经满了,然后沿着慢路径走,这在(3)中解释过。
  3. gc/shared/collectedHeap代码被更改,因为它被用作分配慢路径的入口点。当TLAB被认为已满(因为分配已传递结束指针)时,代码进入collectedHeap并尝试分配一个新的TLAB。此时,TLAB将恢复到其原始大小,并尝试进行分配。如果分配成功,代码对分配进行采样,然后返回。如果没有,则意味着TLAB的分配已经结束,需要一个新的TLAB。代码路径继续其对新TLAB的正常分配,并确定该分配是否需要示例。如果分配被认为对TLAB来说太大,系统也会对分配进行抽样,从而覆盖TLAB分配内和TLAB分配外进行抽样。
  4. 当请求一个示例时,堆栈上有一个收集器对象设置在一个安全的位置,用于将信息发送到本机代理。收集器跟踪采样分配,并在销毁自己的帧时向代理发送回调。该机制确保对象被正确初始化。
  5. 如果JVMTI代理为SampledObjectAlloc事件注册了回调,则该事件将被触发,并且它将获得抽样分配。在libHeapMonitorTest.c文件中可以找到一个示例实现,该文件用于JTreg测试。

选择

对于这个JEP中提出的系统,有多种替代方案。介绍中已经介绍了两个:Flight Recorder提供了一个有趣的替代方案。这个实现提供了几个优点。首先,JFR不允许设置抽样大小或提供回调。其次,当缓冲区耗尽时,JFR使用缓冲区系统可能导致分配丢失。最后,JFR事件系统没有提供跟踪已被垃圾收集的对象的方法,这意味着不可能使用它来提供有关活动对象和垃圾收集对象的信息。

另一种替代方法是使用ASM的字节码插装。它的开销让人望而却步,不是一个可行的解决方案。

这个JEP向JVMTI添加了一个新特性,JVMTI是用于各种开发和监视工具的重要API/框架。有了它,JVMTI代理可以使用低开销的堆分析API以及其他JVMTI功能,这为工具提供了极大的灵活性。例如,由代理决定是否需要在每个事件点收集堆栈跟踪。

测试

JTreg框架中针对该特性有16个测试:使用多个线程打开/关闭,同时分配多个线程,测试数据是否以正确的间隔采样,以及收集的堆栈是否反映正确的程序信息。

风险和假设

禁用该特性不会造成性能损失或风险。没有启用系统的用户不会感知到性能差异。

但是,启用该特性会有潜在的性能/内存损失。在最初的原型实现中,开销是最小的(<2%)。这使用了一个更重量级的机制来修改JIT代码。在这里给出的最终版本中,系统依赖于TLAB代码,并且不应该经历这种回归。

目前对Dacapo基准测试的评估显示开销为:

  • 禁用时为0%
  • 1%,当以默认的512KB间隔启用该特性,但不执行回调动作(即SampledAllocEvent方法为空,但已注册到JVM)。
  • 3%开销,使用抽样回调,执行简单的实现来存储数据(使用测试中的实现)
责任编辑:武晓燕 来源: 今日头条
相关推荐

2018-04-08 08:45:53

对象内存策略

2023-09-27 08:49:23

.Net分配对象

2018-02-08 14:57:22

对象内存分配

2019-07-29 10:10:06

Java内存线程安全

2012-01-11 10:45:57

JavaJVM

2015-11-16 11:22:05

Java对象内存分配

2021-07-30 07:22:51

JVM虚拟机栈 Stack

2022-12-12 08:42:06

Java对象栈内存

2019-09-04 15:31:04

JVM内存String

2010-09-25 15:40:52

配置JVM内存

2013-11-07 09:42:42

对象对象池加速

2021-08-03 09:02:58

LinuxSlab算法

2009-07-09 10:01:26

设置JVM内存分配

2017-08-28 10:13:09

国家分配对象

2020-12-18 11:50:17

AI 数据人工智能

2021-03-22 11:51:22

Java内存栈上

2009-10-27 17:39:39

Oracle用户权限

2023-10-14 17:49:25

Java存储

2022-10-08 08:01:07

JVMTLABPLAB

2023-08-24 07:46:21

服务器JVM
点赞
收藏

51CTO技术栈公众号