perf-map-agent源码解析

 目录

1、create-java-perf-map.sh

2、AttachOnce

3、Agent_OnAttach

4、generate_single_entry

5、generate_unfolded_entries


    perf-map-agent会生成一个/tmp/perf-<pid>.map映射文件,里面包含被JIT即时编译器编译过的Java方法的方法名与编译后生成的机器码的地址映射,perf命令在获取未知的内存地址对应的方法名时会默认读取该文件,从而将perf命令采集到热点方法的内存地址转换成对应的方法名。perf-map-agent的代码包含两部分内容,一个是用C写的agent,负责生成映射文件,一个是Java写的jar包,负责将C写的agent attach到Java进程上,下面来详细研究这两部分的实现源码。

1、create-java-perf-map.sh

     其主要逻辑是校验入参中pid对应的Java进程是否存在,校验JAVA_HOME是否存在,最后执行Java命令attach到目标进程上。其实现如下:

#!/bin/bash
#设置shell的执行模式,-e表示若指令传回值不等于0,则立即退出shell。
set -e
#set -x

CUR_DIR=`pwd`
PID=$1
OPTIONS=$2
ATTACH_JAR=attach-main.jar
#获取当前脚本所在的具体位置
PERF_MAP_DIR="$(cd "$(dirname "$0")" && pwd -P)"/..
ATTACH_JAR_PATH=$PERF_MAP_DIR/out/$ATTACH_JAR
PERF_MAP_FILE=/tmp/perf-$PID.map

#uname命令获取当前操作系统的类型
if [[ `uname` == 'Linux' ]]; then
  LINUX=1;
else
  LINUX=2;
fi

if [[ "$LINUX" == "1" ]]; then
  #如果是linux系统,判断对应的进程ID是否存在
  if [ ! -d /proc/$PID ]; then
    echo "PID $PID not found"
    exit 1
  fi
  #获取uid和gid
  TARGET_UID=$(awk '/^Uid:/{print $2}' /proc/$PID/status)
  TARGET_GID=$(awk '/^Gid:/{print $2}' /proc/$PID/status)
fi

if [ -z "$JAVA_HOME" ]; then
  if [[ "$LINUX" == "1" ]]; then
    #如果是Linux系统,判断JAVA_HOME是否存在
    JAVA_HOME=/usr/lib/jvm/default-java
    #如果不存在则指定为
    [ -d "$JAVA_HOME" ] || JAVA_HOME=/etc/alternatives/java_sdk
  else
    JAVA_HOME=`/usr/libexec/java_home -v 1.8`
  fi
fi
#校验JAVA_HOME,不存在则打印日志并返回false,退出
[ -d "$JAVA_HOME" ] || (echo "JAVA_HOME directory at '$JAVA_HOME' does not exist." && false)


if [[ "$LINUX" == "1" ]]; then
  #移除原来的映射文件
  sudo rm $PERF_MAP_FILE -f
  #通过Java命令attach到目标进程上,-cp指定查找目标类的文件目录或者jar包
  ##注意多个命令行参数是作为一个参数传递给java命令的
  (cd $PERF_MAP_DIR/out && sudo -u \#$TARGET_UID -g \#$TARGET_GID $JAVA_HOME/bin/java -cp $ATTACH_JAR_PATH:$JAVA_HOME/lib/tools.jar net.virtualvoid.perf.AttachOnce $PID "$OPTIONS")
  sudo chown root:root $PERF_MAP_FILE
else
  #移除原来的映射文件
  rm -f $PERF_MAP_FILE
  (cd $PERF_MAP_DIR/out && $JAVA_HOME/bin/java -cp $ATTACH_JAR_PATH:$JAVA_HOME/lib/tools.jar net.virtualvoid.perf.AttachOnce $PID "$OPTIONS")
fi

2、AttachOnce

   AttachOnce是负责attach到目标Java进程的Java类,主要逻辑就是校验通过C编写的agent是否存在,如果存在则通过tools.jar中的VirtualMachine类attach到目标进程上,源码如下:

package net.virtualvoid.perf;

import java.io.File;

import com.sun.tools.attach.VirtualMachine;
import java.lang.management.ManagementFactory;
import java.util.Locale;

public class AttachOnce {
    public static void main(String[] args) throws Exception {
        String pid = args[0];
        String options = "";
        if (args.length > 1) options = args[1];
        //获取启动参数
        loadAgent(pid, options);
    }

    static void loadAgent(String pid, String options) throws Exception {
    	//attach到目标进程上
        VirtualMachine vm = VirtualMachine.attach(pid);
        try {
            final File lib;
            if (System.getProperty("os.name", "").toLowerCase(Locale.US).contains("os x")) {
            	//如果是非linux系统
                lib = new File("libperfmap.dylib");
            } else {
            	//如果是linux系统,该文件正常是跟jar包在同一个目录下
                lib = new File("libperfmap.so");
            }
            String fullPath = lib.getAbsolutePath();
            if (!lib.exists()) {
            	//如果文件不存在则退出
                System.out.printf("Expected %s at '%s' but it didn't exist.\n", lib.getName(), fullPath);
                System.exit(1);
            }
            //如果文件存在则执行特定逻辑
            else vm.loadAgentPath(fullPath, options);
        } catch(com.sun.tools.attach.AgentInitializationException e) {
            // rethrow all but the expected exception
            if (!e.getMessage().equals("Agent_OnAttach failed")) throw e;
        } finally {
        	//解除attach
            vm.detach();
        }
    }
}

3、Agent_OnAttach

     这个是Agent的核心方法,位于perf-map-agent.c中,主要用于设置JVMTI的回调事件类型并注册对应的回调方法。其源码如下:

JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM *vm, char *options, void *reserved) {
	//创建映射文件
    open_map_file();
    //初始化配置参数
    unfold_simple = strstr(options, "unfoldsimple") != NULL;
    unfold_all = strstr(options, "unfoldall") != NULL;
    unfold_inlined_methods = strstr(options, "unfold") != NULL || unfold_simple || unfold_all;
    print_method_signatures = strstr(options, "msig") != NULL;
    print_source_loc = strstr(options, "sourcepos") != NULL;
    dotted_class_names = strstr(options, "dottedclass") != NULL;
    clean_class_names = strstr(options, "cleanclass") != NULL;
    annotate_java_frames = strstr(options, "annotate_java_frames") != NULL;

    bool use_semicolon_unfold_delimiter = strstr(options, "use_semicolon_unfold_delimiter") != NULL;
    unfold_delimiter = use_semicolon_unfold_delimiter ? ";" : "->";

    debug_dump_unfold_entries = strstr(options, "debug_dump_unfold_entries") != NULL;

    jvmtiEnv *jvmti;
    //获取jvmtiEnv的引用
    (*vm)->GetEnv(vm, (void **)&jvmti, JVMTI_VERSION_1);
    //注册jvmtiCapabilities
    enable_capabilities(jvmti);
    //注册回调函数
    set_callbacks(jvmti);
    //设置回调通知的事件类型并开启事件监听
    set_notification_mode(jvmti, JVMTI_ENABLE);
    //生成对应的事件,因为我们注册回调方法时,对应的事件可能已经被触发过了,这里实际是重新触发一遍
    //比如JVMTI_EVENT_COMPILED_METHOD_LOAD事件,就会为当前已经编译完成还未卸载的Java方法重新触发一遍事件
    (*jvmti)->GenerateEvents(jvmti, JVMTI_EVENT_DYNAMIC_CODE_GENERATED);
    (*jvmti)->GenerateEvents(jvmti, JVMTI_EVENT_COMPILED_METHOD_LOAD);
    //关闭事件监听
    set_notification_mode(jvmti, JVMTI_DISABLE);
    //关闭映射文件的文件描述符
    close_map_file();

    return 0;
}

void open_map_file() {
    if (!method_file)
    	//创建映射文件,获取文件描述符
        method_file = perf_map_open(getpid());
}
void close_map_file() {
	//关闭映射文件的文件描述符
    perf_map_close(method_file);
    method_file = NULL;
}

//设置回调的事件
void set_notification_mode(jvmtiEnv *jvmti, jvmtiEventMode mode) {
    (*jvmti)->SetEventNotificationMode(jvmti, mode, JVMTI_EVENT_COMPILED_METHOD_LOAD, (jthread)NULL);
    (*jvmti)->SetEventNotificationMode(jvmti, mode, JVMTI_EVENT_DYNAMIC_CODE_GENERATED, (jthread)NULL);
}

jvmtiError enable_capabilities(jvmtiEnv *jvmti) {
    jvmtiCapabilities capabilities;
    
    //内存初始化
    memset(&capabilities,0, sizeof(capabilities));
    capabilities.can_generate_all_class_hook_events  = 1;
    capabilities.can_tag_objects                     = 1;
    capabilities.can_generate_object_free_events     = 1;
    capabilities.can_get_source_file_name            = 1;
    capabilities.can_get_line_numbers                = 1;
    capabilities.can_generate_vm_object_alloc_events = 1;
    capabilities.can_generate_compiled_method_load_events = 1;

    //注册 jvmtiCapabilities
    return (*jvmti)->AddCapabilities(jvmti, &capabilities);
}

jvmtiError set_callbacks(jvmtiEnv *jvmti) {
    jvmtiEventCallbacks callbacks;
    //内存初始化
    memset(&callbacks, 0, sizeof(callbacks));
    //注册回调函数
    callbacks.CompiledMethodLoad  = &cbCompiledMethodLoad;
    callbacks.DynamicCodeGenerated = &cbDynamicCodeGenerated;
    return (*jvmti)->SetEventCallbacks(jvmti, &callbacks, (jint)sizeof(callbacks));
}

static void JNICALL
cbCompiledMethodLoad(
            jvmtiEnv *jvmti,
            jmethodID method,
            jint code_size,
            const void* code_addr,
            jint map_length,
            const jvmtiAddrLocationMap* map,
            const void* compile_info) {
    if (unfold_inlined_methods && compile_info != NULL)
        //如果需要还原内联优化的方法,比如方法A调用方法B,B被内联优化成A的一部分,最后映射文件中只有A没有B,还原就是把B也写入映射文件
    	//unfold_inlined_methods默认为false
        generate_unfolded_entries(jvmti, method, code_size, code_addr, compile_info);
    else
        generate_single_entry(jvmti, method, code_addr, code_size);
}

void JNICALL
cbDynamicCodeGenerated(jvmtiEnv *jvmti,
            const char* name,
            const void* address,
            jint length) {
    //传递进来的name就是方法名可以直接使用,无需再做处理
    perf_map_write_entry(method_file, address, (unsigned int) length, name);
}

//这三个方法的实现位于perf-map-file.c中
FILE *perf_map_open(pid_t pid) {
    char filename[500];
    //格式化字符串,获取文件名
    snprintf(filename, sizeof(filename), "/tmp/perf-%d.map", pid);
    //创建映射文件
    FILE * res = fopen(filename, "w");
    if (!res) {
    	//创建失败,打印日志
        fprintf(stderr, "Couldn't open %s: errno(%d)", filename, errno);
        exit(0);
    }
    return res;
}

int perf_map_close(FILE *fp) {
    if (fp)
    	//关闭文件描述符
        return fclose(fp);
    else
        return 0;
}

void perf_map_write_entry(FILE *method_file, const void* code_addr, unsigned int code_size, const char* entry) {
    if (method_file)
    	//将机器码地址,大小和对应的方法名写入到文件中
        fprintf(method_file, "%lx %x %s\n", (unsigned long) code_addr, code_size, entry);
}

perf-map-agent关注的事件主要是两个JVMTI_EVENT_COMPILED_METHOD_LOAD和JVMTI_EVENT_DYNAMIC_CODE_GENERATED,前者是在一个Java方法被编译完成并加载到内存中时触发,后者是在一个本地方法根据命令行参数被动态编译完成后触发的。

参考:JVMTM Tool Interface

4、generate_single_entry

       generate_single_entry是默认配置下用来生成被JIT编译器编译过的Java方法的方法名同生成的机器码的内存地址的映射,其主要逻辑是根据jmvtiEnv的方法获取Java方法的方法名,方法签名,所属的类名,源代码文件名和方法的行数等属性,将这些信息格式化拼装成一个字符串,同编译生成的机器码的内存地址,机器码的大小一同写入文件中。    

void generate_single_entry(
        jvmtiEnv *jvmti,
        jmethodID method,
        const void *code_addr,
        jint code_size) {
    char entry[STRING_BUFFER_SIZE];
    //获取一条内存地址与方法名的映射的字符串
    sig_string(jvmti, method, entry, sizeof(entry), frame_annotation(false));
    //将字符串写入文件中
    perf_map_write_entry(method_file, code_addr, (unsigned int) code_size, entry);
}

static void sig_string(jvmtiEnv *jvmti, jmethodID method, char *output, size_t noutput, char *annotation) {
    char *sourcefile = NULL;
    char *method_name = NULL;
    char *msig = NULL;
    char *csig = NULL;
    jvmtiLineNumberEntry *lines = NULL;

    jclass class;
    jint entrycount = 0;
    
    //将字符串复制到output中
    strncpy(output, "<error writing signature>", noutput);

    if (!(*jvmti)->GetMethodName(jvmti, method, &method_name, &msig, NULL)) {
    	//如果成功获取方法名
        if (!(*jvmti)->GetMethodDeclaringClass(jvmti, method, &class) &&
            !(*jvmti)->GetClassSignature(jvmti, class, &csig, NULL)) {
        	//如果成功获取所属的类名及完整的类描述符

            char source_info[1000] = "";
            char *method_signature = "";

            if (print_source_loc) {
            	//如果需要答应源代码文件名以及该方法对应的代码行数
                if (!(*jvmti)->GetSourceFileName(jvmti, class, &sourcefile)) {
                	//获取源代码文件名
                    if (!(*jvmti)->GetLineNumberTable(jvmti, method, &entrycount, &lines)) {
                    	//获取该方法对应的源代码的行数
                        int lineno = -1;
                        //获取该方法第一行的行数
                        if(entrycount > 0) lineno = lines[0].line_number;
                        //字符串格式化,结果放在source_info中
                        snprintf(source_info, sizeof(source_info), "(%s:%d)", sourcefile, lineno);
                        //释放内存
                        deallocate(jvmti, lines);
                    }
                    //释放内存
                    deallocate(jvmti, (unsigned char *) sourcefile);
                }
            }
            
            //print_method_signatures表示打印完整的方法签名
            if (print_method_signatures && msig)
                method_signature = msig;

            char class_name[STRING_BUFFER_SIZE];
            //根据配置做类名转换
            class_name_from_sig(class_name, sizeof(class_name), csig);
            //把格式化后的字符串写入到output中
            snprintf(output, noutput, "%s::%s%s%s%s",
                     class_name, method_name, method_signature, source_info, annotation);

            deallocate(jvmti, (unsigned char *) csig);
        }
        //释放内存
        deallocate(jvmti, (unsigned char *) method_name);
        deallocate(jvmti, (unsigned char *) msig);
    }
}

void class_name_from_sig(char *dest, size_t dest_size, const char *sig) {
	//clean_class_names或者 dotted_class_names表示将类名描述符转换成正常的类名
    if ((clean_class_names || dotted_class_names) && sig[0] == 'L') {
        const char *src = clean_class_names ? sig + 1 : sig;
        int i;
        for(i = 0; i < (dest_size - 1) && src[i]; i++) {
            char c = src[i];
            // /转换成.
            if (dotted_class_names && c == '/') c = '.';
            if (clean_class_names && c == ';') break;
            dest[i] = c;
        }
        dest[i] = 0;
    } else
    	//不需要做转换
        strncpy(dest, sig, dest_size);
}

char *frame_annotation(bool inlined) {
    return annotate_java_frames ? (inlined ? "_[i]" : "_[j]") : "";
}

5、generate_unfolded_entries

      generate_unfolded_entries方法跟generate_single_entry方法的用途一样,区别在于前者会把已经内联优化掉的子方法的方法名同内存地址的映射也写入到映射文件中, 而后者不会,一般情况不需要把内联优化掉的子方法单独拎出来。  

void generate_unfolded_entries(
        jvmtiEnv *jvmti,
        jmethodID root_method,
        jint code_size,
        const void* code_addr,
        const void* compile_info) {
    const jvmtiCompiledMethodLoadRecordHeader *header = compile_info;
    char root_name[STRING_BUFFER_SIZE];
    
    //获取根方法,即被内联优化后的方法的方法名
    sig_string(jvmti, root_method, root_name, sizeof(root_name), "");

    if (debug_dump_unfold_entries)
    	//如果需要debug,则将根方法的调用栈dump出来
        dump_entries(jvmti, root_method, code_size, code_addr, compile_info);

    if (header->kind == JVMTI_CMLR_INLINE_INFO) {
    	//如果是内联优化方法
        const jvmtiCompiledMethodLoadInlineRecord *record = (jvmtiCompiledMethodLoadInlineRecord *) header;

        const void *start_addr = code_addr;
        jmethodID cur_method = root_method;

        // walk through the method meta data per PC to extract address range
        // per inlined method.
        int i;
        for (i = 0; i < record->numpcs; i++) {
            PCStackInfo *info = &record->pcinfo[i];
            jmethodID top_method = info->methods[0];

            // as long as the top method remains the same we delay recording
            if (cur_method != top_method) {
                // top method has changed, record the range for current method
                void *end_addr = info->pc;

                if (i > 0)
                    write_unfolded_entry(jvmti, &record->pcinfo[i - 1], root_method, root_name, start_addr, end_addr);
                else
                    generate_single_entry(jvmti, root_method, start_addr, (unsigned int) (end_addr - start_addr));
                //更新top_method
                start_addr = info->pc;
                cur_method = top_method;
            }
        }

        // record the last range if there's a gap
        if (start_addr != code_addr + code_size) {
            // end_addr is end of this complete code blob
            const void *end_addr = code_addr + code_size;

            if (i > 0)
                write_unfolded_entry(jvmti, &record->pcinfo[i - 1], root_method, root_name, start_addr, end_addr);
            else
                generate_single_entry(jvmti, root_method, start_addr, (unsigned int) (end_addr - start_addr));
        }
    } else {
    	//如果不是内联优化方法
        generate_single_entry(jvmti, root_method, code_addr, code_size);
    }
}

void dump_entries(
        jvmtiEnv *jvmti,
        jmethodID root_method,
        jint code_size,
        const void *code_addr,
        const void *compile_info) {
    const jvmtiCompiledMethodLoadRecordHeader *header = compile_info;
    char root_name[STRING_BUFFER_SIZE];
    //获取根方法的完整方法名
    sig_string(jvmti, root_method, root_name, sizeof(root_name), "");
    //打印日志
    printf("At %s size %x from %p to %p", root_name, code_size, code_addr, code_addr + code_size);
    if (header->kind == JVMTI_CMLR_INLINE_INFO) {
    	//如果是内联优化
        const jvmtiCompiledMethodLoadInlineRecord *record = (jvmtiCompiledMethodLoadInlineRecord *) header;
        printf(" with %d entries\n", record->numpcs);

        int i;
        //循环打印调用堆栈
        for (i = 0; i < record->numpcs; i++) {
            PCStackInfo *info = &record->pcinfo[i];
            printf("  %p has %d stack entries\n", info->pc, info->numstackframes);

            int j;
            for (j = 0; j < info->numstackframes; j++) {
                char buf[2000];
                sig_string(jvmti, info->methods[j], buf, sizeof(buf), "");
                printf("    %s\n", buf);
            }
        }
    } else printf(" with no inline info\n");
}

/* Generates and writes a single entry for a given inlined method. */
void write_unfolded_entry(
        jvmtiEnv *jvmti,
        PCStackInfo *info,
        jmethodID root_method,
        const char *root_name,
        const void *start_addr,
        const void *end_addr) {
    // needs to accommodate: entry_name + " in " + root_name
    char inlined_name[STRING_BUFFER_SIZE * 2 + 4];
    const char *entry_p;

    if (unfold_all) {
        //如果是还原所有的内联方法
        char full_name[BIG_STRING_BUFFER_SIZE];
        full_name[0] = '\0';
        int i;
        // the stack is ordered from leaf@[0] to root@[length-1], so we traverse backwards to construct the unfolded string
        const jint first_frame = info->numstackframes - 1;
        for (i = first_frame; i >= 0; i--) {
            sig_string(jvmti, info->methods[i], inlined_name, sizeof(inlined_name), frame_annotation(i != first_frame));
            strncat(full_name, inlined_name, sizeof(full_name) - 1 - strlen(full_name)); // TODO optimize
            if (i != 0) strncat(full_name, unfold_delimiter, sizeof(full_name) -1 - strlen(full_name));
        }
        entry_p = full_name;
    } else {
    	//默认只还原第一层的内联方法,如A->B->C,如果是还原所有的,则A,B,C都会在映射文件中,默认只有B
        jmethodID cur_method = info->methods[0]; // top of stack
        if (cur_method != root_method) {
            generate_unfolded_entry(jvmti, cur_method, inlined_name, sizeof(inlined_name), root_name);
            entry_p = inlined_name;
        } else {
            entry_p = root_name;
        }
    }
    //把结果写入到文件中
    perf_map_write_entry(method_file, start_addr, (unsigned int) (end_addr - start_addr), entry_p);
}

   


版权声明:本文为qq_31865983原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。