JVMTI开发教程之一个简单的Agent一文中介绍了如何搭建JVMTI开发环境,并实现了一个简单的打印所有已装载class签名的agent。

本文将主要介绍JVMTI的Heap系API,并利用这些API,实现一个类似jmap-histo的Class统计信息柱状图。

103340241.png

在上图中,我们可以获知某个class的实例数量,实例的总占用空间,以及classname。

所用到的JVMTIHeap系API介绍

注意:下文中提及的函数定义,均以C++版作为参照。

Get/SetTag
函数定义如下:

jvmtiError
GetTag(jobject object,
jlong* tag_ptr)
jvmtiError
SetTag(jobject object,
jlong tag)

标签用于与某一个对象建立关联。之后,可通过标签来查找对象。
SetTag为对象添加标签,GetTag获取对象上的标签。
标签在Heap系API中得到了广泛使用。

FollowReferences

函数定义如下

jvmtiError FollowReferences(jint heap_filter,
jclass klass,
jobject initial_object,
const jvmtiHeapCallbacks* callbacks,
const void* user_data)

这个函数的作用是,从roots或initial_obj开始扫描所有可达(有引用)的对象,按扫描顺序将这些对象做为入参回调给jvmtiHeapCallbacks里定义的所有回调函数。

函数本身提供了5个入参。
第一个入参heap_filter是过滤传递给回调函数的对象的标记。JVMTI定义了4个常量来表示4种不同的过滤规则,如下表所示(标签概念,详见Get/SetTag函数的解释):

HeapFilterFlags
ConstantValueDescription
JVMTI_HEAP_FILTER_TAGGED0×4滤除已打标签的对象。
JVMTI_HEAP_FILTER_UNTAGGED0×8滤除未打标签的对象。
JVMTI_HEAP_FILTER_CLASS_TAGGED0×10滤除已打标签的class下的所有对象。
JVMTI_HEAP_FILTER_CLASS_UNTAGGED0×20滤除未打标签的class下的所有对象。

如果这个入参为0,则不做任何过滤。

第二个入参klass作用是只传递所属指定入参class的对象给回调函数,其他对象一律滤除。如果这个入参为NULL,则不做任何限制。
注意:如果klass是接口,则不会有任何对象被传递给回调函数,klass的子类同样会被滤除。

第三个入参initial_obj,告诉函数该从哪个对象开始扫描引用。如果这个入参为NULL,则从HeapRoots开始扫描。HeapRoots一般为线程堆栈引用,系统class,JNI全局变量等。

第四个入参callbacks为回调函数。它是一个结构体,内部定义了多种类型的callbacks。

第五个入参user_data为外部传递给回调函数的数据。这里要注意,如果传递的是非指针类型的值,则可能在对象传递给回调函数时,发生一次值拷贝。如果每个对象的传递都需要值拷贝,将严重影响性能。解决办法就是定义方法体外全局变量。

HeapReferenceCallback

typedef jint (JNICALL *jvmtiHeapReferenceCallback)
(jvmtiHeapReferenceKind reference_kind,
const jvmtiHeapReferenceInfo* reference_info,
jlong class_tag,
jlong referrer_class_tag,
jlong size,
jlong* tag_ptr,
jlong* referrer_tag_ptr,
jint length,
void* user_data);

这是FollowReference的主要回调函数。各个入参的含义见下表:

Parameters
NameTypeDescription
reference_kindjvmtiHeapReferenceKind当前对象的类型。jvmtiHeapReferenceKind是一个枚举,请参见定义。
reference_infoconst
jvmtiHeapReferenceInfo
*
引用的详细信息。
reference_kind
JVMTI_HEAP_REFERENCE_FIELD,
JVMTI_HEAP_REFERENCE_STATIC_FIELD,
JVMTI_HEAP_REFERENCE_ARRAY_ELEMENT,
JVMTI_HEAP_REFERENCE_CONSTANT_POOL,
JVMTI_HEAP_REFERENCE_STACK_LOCAL,
orJVMTI_HEAP_REFERENCE_JNI_LOCAL.
时可以设置它,否则设NULL.
class_tagjlong当前对象所属class的标签值(如果是0,则未打标签)
如果当前对象是一个运行时的class,则class_tag的值为Java.lang.Class的标签值(如果java.lang.Class未打标签,则为0)。
referrer_class_tagjlong引用当前对象的对象所属class的标签值(如果是0,则未打标签或者引用当前对象的是heaproot)
如果引用当前对象的对象是一个运行时的class,则referrer_class_tag的值为Java.lang.Class的标签值(如果java.lang.Class未打标签,则为0)。
sizejlong当前对象的大小(单位bytes)
GetObjectSize.
tag_ptrjlong*当前对象的标签值,注意,这不同于对象所属class的标签值,除非当前对象就是java.lang.Class。如果标签值为0,说明当前对象未打标签。
在回调函数中,你可以为这个值赋值。这个操作类似调用了SetTag。
referrer_tag_ptrjlong*引用当前对象的对象标签值。如果为0,说明未打标签。如果是NULL,说明引用当前对象的对象是来自heaproot。
在回调函数中,你可以为这个值赋值。这个操作类似调用了SetTag。
如果回调函数的入参referrer_tag_ptr==tag_ptr,则说明他们是一个对象内的递归引用。比如A内部的成员变量仍旧是A。
lengthjint如果这个对象是一个数组,则这个入参表示数组的长度。如果不是的话,它的值为-1.
user_datavoid*外部传递给回调函数的数据。详见上文的入参说明。

IterateThroughHeap
函数定义如下:

jvmtiError
IterateThroughHeap(jint heap_filter,
jclass klass,
const jvmtiHeapCallbacks* callbacks,
const void* user_data)

这个函数的入参和功能非常类似FollowReferences,不同之点在于,它不从Heaproots或initial_obj开始扫描,它扫描了整个Heap里所有还未被GC的对象。换句话讲就是遍历了整个堆。

这个函数对应的主要回调函数是jvmtiHeapIterationCallback。因为该回调函数的定义与FollowReference如出一辙,所以本文不再赘述,请自行参照官方文档和FollowReference的入参含义表格。

Deallocate
函数定义如下:

jvmtiError
Deallocate(unsigned char* mem)

如果有jvmti函数内部分配了内存的字串交由agent使用,用完后需要agent手动调用jvmti的Deallocate释放,否则会造成MemoryLeak。

AddCapabilities
函数定义如下:

jvmtiError
AddCapabilities(const jvmtiCapabilities* capabilities_ptr)

jvmti的总控配置。提供了jvmti大部分功能的开关。它一般作为初始化完jvmti环境后,第一个被调用的函数。如果不设置capa,则某些函数功能将失效。本文中用到FollowReference,就需要在capa中开启can_tag_objects=1.

下面是一个使用例子:

jvmtiCapabilities capa;
capa.can_tag_objects = 1; // 允许给object打标签
jvmti->AddCapabilities(&capa);

DisposeEnvironment
函数定义如下:

jvmtiError
DisposeEnvironment(jvmtiEnv* env)

关闭JVMTI连接,销毁jvmti上下文的所有资源。

Class统计信息柱状图开发实例

在上文中,已经给出了本文实例的最终显示效果。

为了实现这个Class统计信息柱状图,我们来分析一下:
1,首先,我们需要得到所有已经装载的Class及相关信息,比如classname。
2,然后,需要定义一个c++class或结构体,用以存储classname,实例数量,实例占用空间这三个属性,和一个全局的队列,存储这些数据结构。
3,使用FollowReference函数,遍历整个从Heaproot可达的对象树(假设先打印live对象)。
从上文jvmtiHeapReferenceCallback函数中了解到,该函数提供了对象的实例数量,加上FollowReference的对象遍历特性。我们就可以累加class的实例数量和总占用空间。

在遍历过程中,如何将对象和我们的自定义class数据结构关联起来呢?
这里我们就需要用到Tag功能。大致思路是这样的:
在第一步得到所有已装载Class对象后,需要为每个Class对象打标签。标签值,就用遍历class时的序号1-n来表示吧。
在jvmtiHeapReferenceCallback回调函数中,我们利用入参class_tag(因为上一步我们为所有class打了标签,所以这就是当前对象所属class的标签值)在全局队列中找到对应标签值的class自定数据结构,将对象和自定class数据结构最终关联起来。

4,最后,按实例占用空间排序class自定义数据结构队列,遍历并打印最终显示效果。
5,清理内存,销毁相关资源。

下面让我们来看源码吧!

/*
* JVMTI Tutorial - jmap -histo:live
*
*  Created on: 2011-3-3
*      Author: kenwu
*/
#include
#include
#include
#include
#include
#include
#include
#include
#include
class ClassInfo {
public:
ClassInfo() {
name = NULL;
cls_id = 0;
instance_cnt = 0;
instance_size = 0;
cls_obj_flag = 0;
}
~ClassInfo() {
cls_id = 0;
free(name);
instance_cnt = 0;
instance_size = 0;
cls_obj_flag = 0;
}
int cls_id;
char *name;
int instance_cnt;
long instance_size;
int cls_obj_flag;
};
ClassInfo **ci_map;
jvmtiEnv *jvmti;
int seq;
int total_cls_size;
/**
* 解析class符号,抽取出class name并格式化。
* @return class name
*/
char* getClassName(jclass cls) {
int xl = 0;
char *sig;
char *data;
jvmti->GetClassSignature(cls, &sig, NULL);
if (sig) {
xl = strlen(sig);
data = (char*) malloc(256);
if (xl > 1 && sig[xl - 1] == ';')
xl--;
int arrlen = 0;
while (sig[arrlen] == '[')
arrlen++;
switch (sig[arrlen]) {
case 'Z':
strcpy(data, "boolean");
xl = 7;
break;
case 'B':
strcpy(data, "byte");
xl = 4;
break;
case 'C':
strcpy(data, "char");
xl = 4;
break;
case 'S':
strcpy(data, "short");
xl = 5;
break;
case 'I':
strcpy(data, "int");
xl = 3;
break;
case 'J':
strcpy(data, "long");
xl = 4;
break;
case 'F':
strcpy(data, "float");
xl = 5;
break;
case 'D':
strcpy(data, "double");
xl = 6;
break;
case 'L': {
strncpy(data, sig + arrlen + 1, xl - arrlen - 1);
xl = xl - arrlen - 1;
break;
}
default: {
strncpy(data, sig + arrlen, xl - arrlen);
xl = xl - arrlen;
break;
}
}
while (arrlen--) {
data[xl++] = '[';
data[xl++] = ']';
}
data[xl] = '';
jvmti->Deallocate((unsigned char*) sig);
char *tmp = data;
while (*tmp) {
if (*tmp == '/') {
*tmp = '.';
}
tmp++;
}
}
return data;
}
jint JNICALL heapFRCallback(jvmtiHeapReferenceKind reference_kind,
const jvmtiHeapReferenceInfo* reference_info, jlong class_tag,
jlong referrer_class_tag, jlong size, jlong* tag_ptr,
jlong* referrer_tag_ptr, jint length, void* user_data) {
// clean duplicate
int act_obj = 0;
if (*tag_ptr == 0) {
*tag_ptr = ++seq;
act_obj = 1;
} else if (*tag_ptr cls_obj_flag == 0) {
ci->cls_obj_flag = 1;
act_obj = 1;
}
}
// statistic
if (act_obj) {
ClassInfo *ci = ci_map[class_tag];
ci->instance_cnt++;
ci->instance_size += size;
}
return JVMTI_VISIT_OBJECTS;
}
jint JNICALL untagCallback(jlong class_tag, jlong size, jlong* tag_ptr,
jint length, void* user_data) {
*tag_ptr = 0;
return JVMTI_VISIT_OBJECTS;
}
JNIEXPORT jint JNICALL Agent_OnAttach(JavaVM *jvm, char *options,
void *reserved) {
/**
* 初始化JVMTI环境
*/
jint result = jvm->GetEnv((void **) &jvmti, JVMTI_VERSION_1_1);
if (result != JNI_OK) {
printf("ERROR: Unable to access JVMTI!n");
return result;
}
jvmtiCapabilities capa;
memset(&capa, 0, sizeof(capa));
capa.can_tag_objects = 1; // 允许给object打标签
jvmti->AddCapabilities(&capa);
/**
* 获取所有已装载的class,并填充ClassInfo容器ci_map,供后面的程序快速查找。
*/
jvmtiError err = (jvmtiError) 0;
jclass *classes;
jint count;
err = jvmti->GetLoadedClasses(&count, &classes);
if (err) {
printf("ERROR: JVMTI GetLoadedClasses failed!n");
return err;
}
ci_map = (ClassInfo**) malloc(sizeof(ClassInfo*) * (count + 1));
ci_map[0] = NULL; // 0表示java.lang.Class
for (int i = 0; i name = getClassName(classes[i]);
ci->cls_id = i + 1;
ci_map[i + 1] = ci;
jvmti->SetTag(classes[i], i + 1);
}
seq = count + 2; // 因为上面的递归使用count+1,所以作为后续seq起始值,需要+2
total_cls_size = count;
/**
* 遍历所有对象,通过ci_map查找ClassInfo,然后累加实例数量和实例占用大小。
*/
jvmtiHeapCallbacks heapCallbacks;
(void) memset(&heapCallbacks, 0, sizeof(heapCallbacks));
heapCallbacks.heap_reference_callback = &heapFRCallback;
err = jvmti->FollowReferences(0, NULL, NULL, &heapCallbacks, (void*) NULL);
if (err) {
printf("%dn", err);
return err;
}
// class id sort by instance size
int ncount = total_cls_size;
int cls_ids[ncount + 1];
cls_ids[0] = 0;
for (int i = 1; i cls_id;
}
int max = ncount + 1, tmp;
for (int i = 1; i < max; i++) {
for (int j = 1; j instance_size
instance_size) {
tmp = cls_ids[j];
cls_ids[j] = cls_ids[j + 1];
cls_ids[j + 1] = tmp;
}
}
}
printf("%5s  %12s  %13s  %10sn", "num ", "#instances", "#bytes",
"class name");
printf("----------------------------------------------n");
int n = 1;
for (int i = 1; i instance_cnt,
c->instance_size, c->name);
}
/**
* 清理内存:untag object, clear ci_map, dispose jvmti env
*/
(void) memset(&heapCallbacks, 0, sizeof(heapCallbacks));
heapCallbacks.heap_iteration_callback = &untagCallback;
err = jvmti->IterateThroughHeap(0, NULL, &heapCallbacks, (void*) NULL);
if (err)
return err;
int len = sizeof(ci_map) / sizeof(ClassInfo*);
for (int i = 0; i DisposeEnvironment();
jvmti = NULL;
seq = 0;
return JNI_OK;
}
JNIEXPORT void JNICALL Agent_OnUnload(JavaVM *vm) {
// nothing to do
}

源码比较长,我尽量在关键部分加了注释,如果还有疑问,欢迎给我留言。

最后就是编译和attach运行了。具体的编译和attach步骤,上一节中我已经详细介绍过了。
不太明白的朋友,请点击JVMTI开发教程之一个简单的Agent

总结

通过本节实例的学习,你掌握了Heap系的几个重要函数的使用。并完成了一个类似jmap-histo:live功能的工具。
下一节中,我们将在本文实例的基础上,添加一些更为高级的功能。比如引用关系,ShallowSize,引用树的筛检去重等。