Btrace详细指南(JDK7,监控HashMap扩容)

  1. 云栖社区>
  2. 博客>
  3. 正文

Btrace详细指南(JDK7,监控HashMap扩容)

mjb 2016-03-07 21:44:47 浏览13146
展开阅读全文
背景
    JAVA中如何排查疑难杂症,如何动态获取应用信息,我们有BTrace!
    PS:集团有大杀器arthas,这里我们先从最原始最广泛的BTrace开始,后面可以玩玩Greys(开源,强于BTrace)。
    应用被tracle后,相关class会被改变,恢复需要重新编译,请自行把握。

一、载安装BTtrace
wiki:https://github.com/jbachorik/btrace/wiki
JIRA(深入必看):https://kenai.com/jira/browse/BTRACE/?selectedTab=com.atlassian.jira.jira-projects-plugin:summary-panel
IDE环境:引入BTrace相关Jar,在eclipse/IDEA编写;使用jvisualvm编写。
代码自动生成工具(使用必备):https://btrace.org/btrace/

下载最新版(1.3,JDK7以上):https://github.com/jbachorik/btrace/releases/tag/v1.3
BTrace Maven插件:https://github.com/btraceio/btrace-maven
BTrace1.2 API DOC:https://btrace.kenai.com/javadoc/1.2/index.html

配置环境变量:
BTRACE_HOME:~/user/btrace
PATH:$BTRACE_HOME/bin
JAVA_VERSION:MAC特有,详见btrace脚本内容,后文有说明。

Maven仓库(只有1.2.X版本,更高版本请自行编译发布到私服):

 <!-- 1.2版本-->
<dependency>
    <groupId>com.sun.tools.btrace</groupId>
    <artifactId>btrace-agent</artifactId>
    <version>1.2.3</version>
</dependency>
<dependency>
    <groupId>com.sun.tools.btrace</groupId>
    <artifactId>btrace-boot</artifactId>
    <version>1.2.3</version>
</dependency>

注意:应用程序端不需要引入BTrace相关Jar,但为了编写脚本方便(IDE提示),可以以Provided引入它,不必Complie。

二、BTtrace常用脚本
解压btrace发现在%BTRACE_HOME%\bin有三个脚本:btrace、btracec、btracer,我们通过这三个脚本使用BTrace。
中文简述:
btrace:对运行中的JAVA程序执行btrace脚本。
btracec:编译btrace脚本。
btracer:让BTrace随JAVA应用启动加载。

btracer注意事项:
原理:java -javaagent:btrace-agent.jar=[<agent-arg>[,<agent-arg>]*]? <launch-args>
场景:When no custom agent arguments are required the btracer utility script may be used instead.

英文完整说明:
<btrace>/bin/btrace <PID> <trace_script> will attach to the java application with the given PID and compile and submit the trace script
<btrace>/bin/btracec <trace_script> will compile the provided trace script
<btrace>/bin/btracer <compiled_script> <args to launch a java app> will start the specified java application with the btrace agent running and the script previously compiled by btracec loaded

详细看看btrace(1.3.4)脚本内容:

#! /bin/sh
if [ -z "$BTRACE_HOME" -o ! -d "$BTRACE_HOME" ] ; then
  # resolve links - $0 could be a link to btrace's home
  PRG="$0"
  progname=`basename "$0"`
  BTRACE_HOME=`dirname "$PRG"`/..
  BTRACE_HOME=`cd "$BTRACE_HOME" && pwd`
fi
if [ -f "${BTRACE_HOME}/build/btrace-client.jar" ] ; then
    if [ "${JAVA_HOME}" != "" ]; then
         TOOLS_JAR="${JAVA_HOME}/lib/tools.jar"
         if [ ! -f ${TOOLS_JAR} ] ; then
           case "`uname`" in
             Darwin*)
               # In older JDK versions for Mac OS X, tools.jar is classes.jar
               # and is kept in a different location. Check if we can locate
               # classes.jar based on ${JAVA_VERSION}
               TOOLS_JAR="/System/Library/Frameworks/JavaVM.framework/Versions/${JAVA_VERSION}/Classes/classes.jar"
               # if we can't find, try relative path from ${JAVA_HOME}. Usually,
               # /System/Library/Frameworks/JavaVM.framework/Versions/1.6.0/Home
               # is JAVA_HOME. (or whatever version beyond 1.6.0!)
               if [ ! -f ${TOOLS_JAR} ] ; then
                 TOOLS_JAR="${JAVA_HOME} /../Classes/classes.jar"
               fi
               # If we still can't find, tell the user to set JAVA_VERSION.
               # This way, we can avoid zip file errors from the agent side
               # and "connection refused" message from client.
               if [ ! -f ${TOOLS_JAR} ] ; then
                 echo "Please set JAVA_VERSION to the target java version"
                 exit 1
               fi
               ;;
           esac
         fi
       ${JAVA_HOME}/bin/java -cp ${BTRACE_HOME}/build/btrace-client.jar:${TOOLS_JAR}:/usr/share/lib/java/dtrace.jar com.sun.btrace.client.Main $*
    else
       echo "Please set JAVA_HOME before running this script"
       exit 1
    fi
else
    echo "Please set BTRACE_HOME before running this script"
    exit 1
fi

MAC 版本如果遇到错误,请留意%JAVA_VERSION%这个环境变量,原因见上面btrace脚本源码。
【${JAVA_HOME}/bin/java -cp ${BTRACE_HOME}/build/btrace-client.jar:${TOOLS_JAR}:/usr/share/lib/java/dtrace.jar com.sun.btrace.client.Main $*】btrace启动核心靠它!。
三、使用示例(监控HashMap扩容时的上下文情况)
简要说明:
table:哈希表数组;newCapacity:哈希表空间容量,即table数组长度;size:哈希表有效数据长度;threshold:有效数据个数阀值;

    void addEntry(int hash, K key, V value, int bucketIndex) {
        if ((size >= threshold) && (null != table[bucketIndex])) {    //达到阀值 && 当前key对应的bucket被占用,触发扩容
            resize(2 * table.length);
            hash = (null != key) ? hash(key) : 0;
            bucketIndex = indexFor(hash, table.length);
        }
        createEntry(hash, key, value, bucketIndex);
    }

原始代码:

public class ExpandCapacity {
    public static final int ONE_MB = 1024 * 1024;
    public static void main(String[] args) throws Exception {
        mapExpandCapacity();
    }
    /**
     * java.util.HashMap#resize(int)
     * 何时扩容,发生扩容时候的上下文情况
     */
    public static void mapExpandCapacity() throws Exception {
        new Thread(new Runnable() {
            public void run() {
                while (true) {
                    //这是模拟主要逻辑
                    Map<Integer, Byte[]> map = new HashMap<Integer, Byte[]>();//16,0.75 16*0.75=12
                    int size = 100;
                    for (int i = 0; i < size; i++) {
                        map.put(i, new Byte[ONE_MB]);
                    }
                    System.out.println("Expand SIZE = " + map.size());
                    try {
                        TimeUnit.SECONDS.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "mapExpandCapacity").start();
        TimeUnit.HOURS.sleep(1);
    }
}

trace脚本,特别注意OnMethod的中@Location#Where默认值是Before,代表方法被调用,但还未执行其内容:

@BTrace
public class ExpandCapacityBtrace {
    @OnMethod(clazz = "java.util.HashMap", method = "resize",
            location = @Location(value = Kind.CALL, clazz = "/.*/", method = "/.*/"))
    public static void traceMapExpandCapacity(@ProbeClassName String probeClass, @ProbeMethodName String probeMethod,
                                              @Self Object self,int newCapacity) {
        String point = Strings.strcat(Strings.strcat(probeClass, "."), probeMethod);//java/util/HashMap.resize
        Class clazz = classForName("java.util.HashMap");
        println(Strings.strcat(point, "======"));
        //获取实例protected变量
        Map.Entry[] table= (Map.Entry[]) get(field(clazz, "table", true), self);
        int threshold = getInt(field(clazz, "threshold", true), self);
        int size = getInt(field(clazz, "size", true), self);
        println(Strings.strcat("newCapacity:", str(newCapacity)));
        println(Strings.strcat("table.length:", str(table.length)));
        println(Strings.strcat("size:", str(size)));
        println(Strings.strcat("threshold:", str(threshold)));
        println(Strings.strcat(point, "------------"));
    }
}

输出(摘录核心,并进行格式化):

java/util/HashMap.resize======
newCapacity:32
table.length:16
size:16
threshold:12
java/util/HashMap.resize------------
java/util/HashMap.resizee======
newCapacity:64
table.length:32
size:32
threshold:24
java/util/HashMap.resize------------
java/util/HashMap.resizee======
newCapacity:128
table.length:64
size:64
threshold:48
java/util/HashMap.resize------------

初始(New HashMap()):newCapacity=16,size=0,table.length=16,threshold=12;
添加100个有效数据,会发生多次扩容,调用resize,首次调用:newCapacity=32,size=16,entry.length:16,threshold=12;
threshold每次执行完resize会改变为(int)Math.min(newCapacity * loadFactor, MAXIMUM_CAPACITY + 1)。
四、常见问题

Q:btrace默认将信息打印到控制台,如何将信息打印到文件?
btrace pid btrace_scrpit.java > console.log
>代表覆盖,>>代表追加,其实就是把控制台的东东重定向到文件。

Q:典型错误NullPointerException:

......
Exception in thread "main" java.lang.NullPointerException
        at com.sun.btrace.client.Client.submit(Client.java:361)
        at com.sun.btrace.client.Main.main(Main.java:189)
DEBUG: sending exit command
Exception in thread "Thread-0" java.lang.IllegalStateException
        at com.sun.btrace.client.Client.send(Client.java:466)
        at com.sun.btrace.client.Client.sendExit(Client.java:400)
        at com.sun.btrace.client.Main$2.run(Main.java:229)
        at java.lang.Thread.run(Thread.java:745)

原因一:
应用程序端使用BTrace版本和客户端不一致。
    为了方便BTrace脚本编写,IDE引入BTrace Jar,设置了Compile范围,应用启动加载时会加载相关class。
    而运行btrace命令时,其BTrace版本和应用端的BTrace版本不一致,产生异常。
这里直接将IDE引入的BTrace设置成Provided。

原因二:
    主要由于Btrace默认使用%JAVA_HOME%/lib/tools.jar,而应用程序类加载路径(-cp/-classpath)没有tools.jar,因此我们需要把tools.jar加到类路径下。
    相关git issue:


五、BTrace详解

BTrace 的使用通过Annotations,详情:https://github.com/jbachorik/btrace/wiki/BTrace-Annotations
中文翻译(个人水平有限,建议中英对照):
  • 方法注解说明 
@OnMethod:指定使用当前注解的方法应该在什么情况下触发,规范[@OnMethod(clazz=<cname_spec>[, method=<mname_spec>]? [, type=<signature>]? [, location=<location_spec>]?)]
    claszz属性,指定要匹配的类的全限定类名,可以用正则表达式:"/类名的Pattern/"匹配,用”+类名”匹配所有子类,用”@某某注解”匹配用该注解注解过的类。
    method属性,指定要匹配的方法名称,可以用正则表达式:/方法名称的Pattern/匹配。
    type属性,类似JAVA方法声明,但不包括方法名,参数名,异常。如void(java.lang.String)可以用于匹配:public void funcName(String param) throws Exception。
    location属性,可简单认为指明该BTrace方法在何时执行(如THROW/RETURN/ENTRY,详情参见@Location) 。
@OnTimer:指定一个定时任务。
@OnExit:当脚本运行Sys.exit(code)时触发。
@OnError:当脚本运行抛出异常时触发。
@OnEvent:脚本运行时Ctrl+C可以发送事件。
@OnLowMemory:让你指定一个阀值,内存低于阀值触发。

@Location注意点:
Kind.ENTRY意指进入匹配probe点,与@Location设置的clazz和method没有任何关系。
Kind.CALL意指从某个匹配probe的方法中调用了匹配A class method的点,一定要和clazz,method配合使用。clazz和method的默认值为"",所以不能被匹配。

 Kind.ENTRY只关注调用X方法(OnMethod的clazz,method),Kind.CALL还关注X方法中,还调用了哪些类哪些方法(Location的class,method)。

  • 方法参数注解说明 
@Self:用来指定被trace方法的this,可参考例子AWTEventTracer.java 和 AllCalls1.java。
@ProbeClassName:目标类名。
@ProbeMethodName:目标方法名。
@TargetInstance:配合@Location#Kind.CALL使用,匹配@Location#clazz的Class实例。
@TargetMethodOrField:配合@Location#Kind.CALL使用,匹配@Location#method的MethodOrField值。
@Retrun:配合@Location#Kind.Return使用,用来指定被trace方法的返回对象,可参考例子Classload.java。
@Duration:配合@Location#Kind.Return/ Kind.ERROR使用,目标方法执行时间,单位是纳秒。

如何获取方法参数?可参见@Kind#CALL说明。
如方法java.util.HashMap#resize(int newCapacity),要获取newCapacity,则可以在trace方法当参数注入,如下:

public static void traceMapExpandCapacity(@ProbeClassName String probeClass, @ProbeMethodName String probeMethod, @Self Object self,int newCapacity)

其中newCapacity则是resize调用时的newCapacity参数。

 @TargetInstance 和 @TargetMethodOrField的理解
如果是probe A class方法调用匹配了B class,@TargetInstance 返回的就是B class,@Self返回的就是A class。

@Retrun:用来指定被trace方法的返回对象,可参考例子Classload.java。

Trace Scripts 说明,主要是一些约束,描述了能做什么,不能做什么:https://github.com/jbachorik/btrace/wiki/Trace-Scripts

samples目录有很多样例,想写又不会写时,可以进去看看,这有中文说明:http://mgoann.iteye.com/blog/1409667

六、参考文献
2. btrace一些你不知道的事(源码入手),这篇很有料,请看:http://agapple.iteye.com/blog/1005918
附:思维导图(转载)
f91f9054980ef9cdea0e0b2862a0eb8579171e00

网友评论

登录后评论
0/500
评论
mjb
+ 关注