BTrace简介及实现浅析

BTrace is a safe, dynamic tracing tool for the Java platform.
BTrace can be used to dynamically trace a running Java program (similar to DTrace for OpenSolaris applications and OS). BTrace dynamically instruments the classes of the target application to inject tracing code (“bytecode tracing”).

上面是BTrace官网对于BTrace的介绍。我们可以用BTrace来探测系统运行时的状况。下面就小试牛刀,写个例子跑跑看。

下面是一个用来探测哪些方法的执行时间超过某个阈值的探针。如果超过阈值,会记录该次调用的调用栈,

import com.sun.btrace.annotations.*;

import static com.sun.btrace.BTraceUtils.*;

@BTrace
public class Tracker {

    @OnMethod(
            clazz = "/me\\.kisimple\\.just4fun\\..*/",
            method = "/.*/",
            location = @Location(Kind.RETURN)
    )
    public static void trace(@ProbeClassName String pcn,
                             @ProbeMethodName String pmn,
                             @Duration long duration) {
        // duration的单位是纳秒
        if(duration > 3000000000l) {
            print(Strings.strcat("#Duration of ",
                            Strings.strcat(Strings.strcat(pcn, "."), pmn)));
            print(" is ");
            print(duration);
            println("#");
            jstack();
        }
    }

}

BTrace支持正则表达式,a regular expression specified within two forward slashes,也就是"/<regexp>/"这样子的。我们的例子是探测me.kisimple.just4fun包下的任何类的任何方法。
模拟一个被探测对象,

public class Main {

    public static void main(String[] args) throws Exception{
        while(true) {
            bar();
            foo();
        }
    }

    private static void bar() throws InterruptedException {
        Thread.sleep(5000);
    }
    private static void foo() throws InterruptedException {
        Thread.sleep(1000);
    }

}

执行btrace命令,

$ btrace 6860 Projects/just4fun/src/me/kisimple/just4fun/Tracker.java 
#Duration of me.kisimple.just4fun.Main.bar is 5000179210#
me.kisimple.just4fun.Main.bar(Main.java:17)
me.kisimple.just4fun.Main.main(Main.java:10)
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.lang.reflect.Method.invoke(Method.java:606)
com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)
#Duration of me.kisimple.just4fun.Main.bar is 5000131459#
me.kisimple.just4fun.Main.bar(Main.java:17)
me.kisimple.just4fun.Main.main(Main.java:10)
sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)
sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
java.lang.reflect.Method.invoke(Method.java:606)
com.intellij.rt.execution.application.AppMain.main(AppMain.java:134)

这么强大的功能是怎么做到的?下面我们就RTFSC一探究竟。
从BTrace的启动脚本中可以找到相关入口,

${JAVA_HOME}/bin/java -Dcom.sun.btrace.probeDescPath=. -Dcom.sun.btrace.dumpClasses=false -Dcom.sun.btrace.debug=false -Dcom.sun.btrace.unsafe=false -cp ${BTRACE_HOME}/build/btrace-client.jar:${TOOLS_JAR}:/usr/share/lib/java/dtrace.jar com.sun.btrace.client.Main $*

所以Main-Class就是com.sun.btrace.client.Main了,来看看它的main方法,

    public static void main(String[] args) {
        
        // 1. 参数解析
        
        if (args.length < 2) {
            usage();
        }

        int port = BTRACE_DEFAULT_PORT;
        String classPath = ".";
        String includePath = null;
        
        int count = 0;
        boolean portDefined = false;
        boolean classpathDefined = false;
        boolean includePathDefined = false;

        for (;;) {
            if (args[count].charAt(0) == '-') {
                if (args.length <= count+1) {
                    usage();
                }  
                if (args[count].equals("-p") && !portDefined) {
                    try {
                        port = Integer.parseInt(args[++count]);
                        if (isDebug()) debugPrint("accepting port " + port);
                    } catch (NumberFormatException nfe) {
                        usage();
                    }
                    portDefined = true;
                } else if ((args[count].equals("-cp") ||
                    args[count].equals("-classpath"))
                    && !classpathDefined) {
                    classPath = args[++count];
                    if (isDebug()) debugPrint("accepting classpath " + classPath);
                    classpathDefined = true;
                } else if (args[count].equals("-I") && !includePathDefined) {
                    includePath = args[++count];
                    if (isDebug()) debugPrint("accepting include path " + includePath);
                    includePathDefined = true;
                } else {
                    usage();
                }
                count++;
                if (count >= args.length) {
                    break;
                }
            } else {
                break;
            }
        }

        if (! portDefined) {
            if (isDebug()) debugPrint("assuming default port " + port);
        }

        if (! classpathDefined) {
            if (isDebug()) debugPrint("assuming default classpath '" + classPath + "'");
        }

        if (args.length < (count + 1)) {
            usage();
        }

        String pid = args[count];
        String fileName = args[count + 1];
        String[] btraceArgs = new String[args.length - count];
        if (btraceArgs.length > 0) {
            System.arraycopy(args, count, btraceArgs, 0, btraceArgs.length);
        }

        try {
            Client client = new Client(port, PROBE_DESC_PATH, 
                DEBUG, TRACK_RETRANSFORM, UNSAFE, DUMP_CLASSES, DUMP_DIR);
            if (! new File(fileName).exists()) {
                errorExit("File not found: " + fileName, 1);
            }

            
            // 2. 编译btrace脚本
            
            byte[] code = client.compile(fileName, classPath, includePath);
            if (code == null) { 
                errorExit("BTrace compilation failed", 1);
            }

            
            // 3. attach到目标VM
            
            client.attach(pid);
            registerExitHook(client);
            if (con != null) {
                registerSignalHandler(client);
            }
            if (isDebug()) debugPrint("submitting the BTrace program");
            
            
            // 4. 提交btrace请求
            
            client.submit(fileName, code, btraceArgs,
                createCommandListener(client));
        } catch (IOException exp) {
            errorExit(exp.getMessage(), 1);
        }
    }

BTrace脚本的编译细节(包括脚本解析等)我们暂不深究。看第3步之前先来看第4步提交的请求,com.sun.btrace.client.Client#submit

    /**
     * Submits the compiled BTrace .class to the VM
     * attached and passes given command line arguments.
     * Receives commands from the traced JVM and sends those
     * to the command listener provided.
     */
    public void submit(String fileName, byte[] code, String[] args,
            CommandListener listener) throws IOException {
        if (sock != null) {
            throw new IllegalStateException();
        }
        submitDTrace(fileName, code, args, listener);
        try {
            if (debug) {
                debugPrint("opening socket to " + port);
            }
            
            // 与目标VM通过Socket进行通信
            
            sock = new Socket("localhost", port);
            oos = new ObjectOutputStream(sock.getOutputStream());
            if (debug) {
                debugPrint("sending instrument command");
            }
            
            // 给目标VM发送InstrumentCommand
            
            WireIO.write(oos, new InstrumentCommand(code, args));
            ois = new ObjectInputStream(sock.getInputStream());
            if (debug) {
                debugPrint("entering into command loop");
            }
            commandLoop(listener);
        } catch (UnknownHostException uhe) {
            throw new IOException(uhe);
        }
    }

现在我们再来看第3步,也就是com.sun.btrace.client.Client#attach

    /**
     * Attach the BTrace client to the given Java process.
     * Loads BTrace agent on the target process if not loaded
     * already.
     */
    public void attach(String pid) throws IOException {
        try {
            String agentPath = "/btrace-agent.jar";
            String tmp = Client.class.getClassLoader().getResource("com/sun/btrace").toString();
            tmp = tmp.substring(0, tmp.indexOf("!"));
            tmp = tmp.substring("jar:".length(), tmp.lastIndexOf("/"));
            agentPath = tmp + agentPath;
            agentPath = new File(new URI(agentPath)).getAbsolutePath();
            attach(pid, agentPath, null, null);
        } catch (RuntimeException re) {
            throw re;
        } catch (IOException ioexp) {
            throw ioexp;
        } catch (Exception exp) {
            throw new IOException(exp.getMessage());
        }
    }
    /**
     * Attach the BTrace client to the given Java process.
     * Loads BTrace agent on the target process if not loaded
     * already. Accepts the full path of the btrace agent jar.
     * Also, accepts system classpath and boot classpath optionally.
     */
    public void attach(String pid, String agentPath, String sysCp, String bootCp) throws IOException {
        try {
            VirtualMachine vm = null;
            if (debug) {
                debugPrint("attaching to " + pid);
            }
            
            // 调用 Attach API
            
            vm = VirtualMachine.attach(pid);
            if (debug) {
                debugPrint("checking port availability: " + port);
            }
            Properties serverVmProps = vm.getSystemProperties();
            int serverPort = Integer.parseInt(serverVmProps.getProperty("btrace.port", "-1"));
            if (serverPort != -1) {
                if (serverPort != port) {
                    throw new IOException("Can not attach to PID " + pid + " on port " + port + ". There is already a BTrace server active on port " + serverPort + "!");
                }
            } else {
                if (!isPortAvailable(port)) {
                    throw new IOException("Port " + port + " unavailable.");
                }
            }

            if (debug) {
                debugPrint("attached to " + pid);
            }

            if (debug) {
                debugPrint("loading " + agentPath);
            }
            String agentArgs = "port=" + port;
            if (debug) {
                agentArgs += ",debug=true";
            }
            if (unsafe) {
                agentArgs += ",unsafe=true";
            }
            if (dumpClasses) {
                agentArgs += ",dumpClasses=true";
                agentArgs += ",dumpDir=" + dumpDir;
            }
            if (trackRetransforms) {
                agentArgs += ",trackRetransforms=true";
            }
            if (bootCp != null) {
                agentArgs += ",bootClassPath=" + bootCp;
            }
            if (sysCp == null) {
                sysCp = getToolsJarPath(
                    serverVmProps.getProperty("java.class.path"),
                    serverVmProps.getProperty("java.home")
                );
            }
            String cmdQueueLimit = System.getProperty(BTraceRuntime.CMD_QUEUE_LIMIT_KEY, null);
            if (cmdQueueLimit != null) {
                agentArgs += ",cmdQueueLimit=" + cmdQueueLimit;
            }
            agentArgs += ",systemClassPath=" + sysCp;
            agentArgs += ",probeDescPath=" + probeDescPath;
            if (debug) {
                debugPrint("agent args: " + agentArgs);
            }
            
            // 加载Instrumentation支持的agent
            
            vm.loadAgent(agentPath, agentArgs);
            if (debug) {
                debugPrint("loaded " + agentPath);
            }
        } catch (RuntimeException re) {
            throw re;
        } catch (IOException ioexp) {
            throw ioexp;
        } catch (Exception exp) {
            throw new IOException(exp.getMessage());
        }
    }

可以看到这个地方使用了Attach API。最后调用了VirtualMachine#loadAgent方法,加载java.lang.instrument指定的agent,$BTRACE_HOME/build/btrace-agent.jar,它的MANIFEST.MF是这样的,

Manifest-Version: 1.0
Ant-Version: Apache Ant 1.8.0
Created-By: 1.7.0_07-b10 (Oracle Corporation)
Premain-Class: com.sun.btrace.agent.Main
Agent-Class: com.sun.btrace.agent.Main
Boot-Class-Path: btrace-boot.jar
Can-Redefine-Classes: true
Can-Retransform-Classes: true

VirtualMachine#loadAgent的时候会调用Agent-Classagentmain方法,这里也就是com.sun.btrace.agent.Main#agentmain

    public static void agentmain(String args, Instrumentation inst) {
        main(args, inst);
    }
    private static synchronized void main(final String args, final Instrumentation inst) {
        if (Main.inst != null) {
            return;
        } else {
            Main.inst = inst;
        }

        
        // 1. 参数解析
        
        if (isDebug()) debugPrint("parsing command line arguments");
        parseArgs(args);
        if (isDebug()) debugPrint("parsed command line arguments");

        /// Boot-Class-Path: btrace-boot.jar
        String bootClassPath = argMap.get("bootClassPath");
        if (bootClassPath != null) {
            if (isDebug()) {
                 debugPrint("Bootstrap ClassPath: " + bootClassPath);
            }
            StringTokenizer tokenizer = new StringTokenizer(bootClassPath, File.pathSeparator);
            try {
                while (tokenizer.hasMoreTokens()) {
                    String path = tokenizer.nextToken();
                    inst.appendToBootstrapClassLoaderSearch(new JarFile(new File(path)));
                }
            } catch (IOException ex) {
                debugPrint("adding to boot classpath failed!");
                debugPrint(ex);
                return;
            }
        }

        String systemClassPath = argMap.get("systemClassPath");
        if (systemClassPath != null) {
            if (isDebug()) {
                 debugPrint("System ClassPath: " + systemClassPath);
            }
            StringTokenizer tokenizer = new StringTokenizer(systemClassPath, File.pathSeparator);
            try {
                while (tokenizer.hasMoreTokens()) {
                    String path = tokenizer.nextToken();
                    inst.appendToSystemClassLoaderSearch(new JarFile(new File(path)));
                }
            } catch (IOException ex) {
                debugPrint("adding to boot classpath failed!");
                debugPrint(ex);
                return;
            }
        }

        String tmp = argMap.get("noServer");
        boolean noServer = tmp != null && !"false".equals(tmp);
        if (noServer) {
            if (isDebug()) debugPrint("noServer is true, server not started");
            return;
        }
        
        
        // 2. 启动agent线程
        
        Thread agentThread = new Thread(new Runnable() {
            public void run() {
                BTraceRuntime.enter();
                try {
                    startServer();
                } finally {
                    BTraceRuntime.leave();
                }
            }
        });
        BTraceRuntime.enter();
        try {
            agentThread.setDaemon(true);
            if (isDebug()) debugPrint("starting agent thread");
            agentThread.start();
        } finally {
            BTraceRuntime.leave();
        }
    }

startServer方法,

    private static void startServer() {
        int port = BTRACE_DEFAULT_PORT;
        String p = argMap.get("port");
        if (p != null) {
            try {
                port = Integer.parseInt(p);
            } catch (NumberFormatException exp) {
                error("invalid port assuming default..");
            }
        }
        ServerSocket ss;
        try {
            if (isDebug()) debugPrint("starting server at " + port);
            System.setProperty("btrace.port", String.valueOf(port));
            if (scriptOutputFile != null && scriptOutputFile.length() > 0) {
                System.setProperty("btrace.output", scriptOutputFile);
            }
            ss = new ServerSocket(port);
        } catch (IOException ioexp) {
            ioexp.printStackTrace();
            return;
        }

        while (true) {
            try {
                if (isDebug()) debugPrint("waiting for clients");
                
                // 等待客户端连接上来
                
                Socket sock = ss.accept();
                if (isDebug()) debugPrint("client accepted " + sock);
                
                // 生成RemoteClient
                
                Client client = new RemoteClient(inst, sock);
                registerExitHook(client);
                
                // 处理客户端请求
                
                handleNewClient(client);
            } catch (RuntimeException re) {
                if (isDebug()) debugPrint(re);
            } catch (IOException ioexp) {
                if (isDebug()) debugPrint(ioexp);
            }
        }
    }

来看下RemoteClient的构造函数,

    RemoteClient(Instrumentation inst, Socket sock) throws IOException {
        super(inst);
        this.sock = sock;
        this.ois = new ObjectInputStream(sock.getInputStream());
        this.oos = new ObjectOutputStream(sock.getOutputStream());
        
        // 读取客户端提交过来的InstrumentCommand
        
        Command cmd = WireIO.read(ois);
        if (cmd.getType() == Command.INSTRUMENT) {
            if (debug) Main.debugPrint("got instrument command");
            
            // 保存编译后的btrace脚本代码到Client#btraceCode
            
            Class btraceClazz = loadClass((InstrumentCommand)cmd);
            if (btraceClazz == null) {
                throw new RuntimeException("can not load BTrace class");
            }
        } else {
            errorExit(new IllegalArgumentException("expecting instrument command!"));
            throw new IOException("expecting instrument command!");
        } 
        ...
    }

处理客户端请求,

    private static void handleNewClient(final Client client) {
        serializedExecutor.submit(new Runnable() {

            public void run() {
                try {
                    if (isDebug()) debugPrint("new Client created " + client);
                    if (client.shouldAddTransformer()) {
                        /
                        // 1. 添加ClassFileTransformer
                        /
                        client.registerTransformer();
                        
                        /
                        // 2. 获取满足脚本中条件的全部类
                        /
                        Class[] classes = inst.getAllLoadedClasses();
                        ArrayList<Class> list = new ArrayList<Class>();
                        if (isDebug()) debugPrint("filtering loaded classes");
                        for (Class c : classes) {
                            if (inst.isModifiableClass(c) && client.isCandidate(c)) {
                                if (isDebug()) debugPrint("candidate " + c + " added");
                                list.add(c);
                            }
                        }
                        list.trimToSize();
                        int size = list.size();
                        if (isDebug()) debugPrint("added as ClassFileTransformer");
                        if (size > 0) {
                            classes = new Class[size];
                            list.toArray(classes);
                            client.startRetransformClasses(size);
                            /
                            // 3. 开始进行retransform
                            /                                      
                            if (isDebug()) {
                                for(Class c : classes) {
                                    try {
                                      inst.retransformClasses(c);
                                    } catch (VerifyError e) {
                                        debugPrint("verification error: " + c.getName());
                                    }
                                }
                            } else {
                                inst.retransformClasses(classes);
                            }
                            client.skipRetransforms();
                        }
                    }
                    client.getRuntime().send(new OkayCommand());
                } catch (UnmodifiableClassException uce) {
                    if (isDebug()) {
                        debugPrint(uce);
                    }
                    client.getRuntime().send(new ErrorCommand(uce));
                }
            }
        });

    }

com.sun.btrace.agent.Client#registerTransformer方法中会调用java.lang.instrument.Instrumentation#addTransformer

        void registerTransformer() {
            inst.addTransformer(clInitTransformer, false);
            inst.addTransformer(this, true);
        }

通过java.lang.instrument.Instrumentation#addTransformer添加了ClassFileTransformer,当调用java.lang.instrument.Instrumentation#retransformClasses时,上面所添加的ClassFileTransformertransform方法就会被调用,这里也就是com.sun.btrace.agent.Client#transformer了,该方法最后是调用了com.sun.btrace.agent.Client#instrument来完成真正的字节码修改工作,

        private byte[] instrument(Class clazz, String cname, byte[] target) {
            byte[] instrumentedCode;
            try {
                ClassWriter writer = InstrumentUtils.newClassWriter(target);
                ClassReader reader = new ClassReader(target);
                Instrumentor i = new Instrumentor(clazz, className,  btraceCode, onMethods, writer);
                InstrumentUtils.accept(reader, i);
                if (Main.isDebug() && !i.hasMatch()) {
                    Main.debugPrint("*WARNING* No method was matched for class " + cname); // NOI18N
                }
                instrumentedCode = writer.toByteArray();
            } catch (Throwable th) {
                Main.debugPrint(th);
                return null;
            }
            Main.dumpClass(className, cname, instrumentedCode);
            return instrumentedCode;
        }

上面使用的很多类的包名都是com.sun.btrace.org.objectweb.asm,BTrace使用了ASM来完成字节码的修改工作,具体细节暂时也不深究了。

简单总结一下,

  1. BTrace脚本编译;
  2. BTrace客户端使用Attach API attach到目标VM,并加载agent包;
  3. agent打开socket来与客户端进行通信;
  4. 客户端给agent发送InstrumentCommand,其中包含BTrace脚本编译后的字节码;
  5. agent通过ASM和Instrumentation API来完成满足BTrace脚本的字节码修改工作;

Easter egg

通过BTrace的实现我们可以看到HotSpot Serviceability的几个技术点:Dynamic Attach,Instrumentation API以及JVM Tool Interface。下面来一一讨论下。

Dynamic Attach

Attach API底层使用的就是Dynamic Attach机制。我们平时jstack经常看到一个Attach Listener线程就与这个机制的实现有关。具体可参考这篇博文,这里就不展开了。

Instrumentation及JVM TI

上面我们看到了通过VirtualMachine#loadAgent来加载java.lang.instrument指定规范的agent。我们来看下在JDK8里面这个方法的代码

   /**
     * Loads an agent.
     *
     * <p> The agent provided to this method is a path name to a JAR file on the file
     * system of the target virtual machine. This path is passed to the target virtual
     * machine where it is interpreted. The target virtual machine attempts to start
     * the agent as specified by the {@link java.lang.instrument} specification.
     * That is, the specified JAR file is added to the system class path (of the target
     * virtual machine), and the <code>agentmain</code> method of the agent class, specified
     * by the <code>Agent-Class</code> attribute in the JAR manifest, is invoked. This
     * method completes when the <code>agentmain</code> method completes.
     *
     * @param   agent
     *          Path to the JAR file containing the agent.
     *
     * @param   options
     *          The options to provide to the agent's <code>agentmain</code>
     *          method (can be <code>null</code>).
     *
     * @throws  AgentLoadException
     *          If the agent does not exist, or cannot be started in the manner
     *          specified in the {@link java.lang.instrument} specification.
     *
     * @throws  AgentInitializationException
     *          If the <code>agentmain</code> throws an exception
     *
     * @throws  IOException
     *          If an I/O error occurs
     *
     * @throws  NullPointerException
     *          If <code>agent</code> is <code>null</code>.
     */
    public abstract void loadAgent(String agent, String options)
        throws AgentLoadException, AgentInitializationException, IOException;

HotSpot的VirtualMachine实现是HotSpotVirtualMachine,来看下HotSpotVirtualMachine#loadAgent

    /*
     * Load JPLIS agent which will load the agent JAR file and invoke
     * the agentmain method.
     */
    public void loadAgent(String agent, String options)
        throws AgentLoadException, AgentInitializationException, IOException
    {
        String args = agent;
        if (options != null) {
            args = args + "=" + options;
        }
        try {
            //
            // 加载 libinstrument.so/instrument.dll
            /
            loadAgentLibrary("instrument", args);
        } catch (AgentLoadException x) {
            throw new InternalError("instrument library is missing in target VM", x);
        } catch (AgentInitializationException x) {
            /*
             * Translate interesting errors into the right exception and
             * message (FIXME: create a better interface to the instrument
             * implementation so this isn't necessary)
             */
            int rc = x.returnValue();
            switch (rc) {
                case JNI_ENOMEM:
                    throw new AgentLoadException("Insuffient memory");
                case ATTACH_ERROR_BADJAR:
                    throw new AgentLoadException("Agent JAR not found or no Agent-Class attribute");
                case ATTACH_ERROR_NOTONCP:
                    throw new AgentLoadException("Unable to add JAR file to system class path");
                case ATTACH_ERROR_STARTFAIL:
                    throw new AgentInitializationException("Agent JAR loaded but agent failed to initialize");
                default :
                    throw new AgentLoadException("Failed to load agent - unknown reason: " + rc);
            }
        }
    }
    /*
     * Load agent library - library name will be expanded in target VM
     */
    public void loadAgentLibrary(String agentLibrary, String options)
        throws AgentLoadException, AgentInitializationException, IOException
    {
        loadAgentLibrary(agentLibrary, false, options);
    }
    /*
     * Load agent library
     * If isAbsolute is true then the agent library is the absolute path
     * to the library and thus will not be expanded in the target VM.
     * if isAbsolute is false then the agent library is just a library
     * name and it will be expended in the target VM.
     */
    private void loadAgentLibrary(String agentLibrary, boolean isAbsolute, String options)
        throws AgentLoadException, AgentInitializationException, IOException
    {
        //
        // 向Attach Listener发送load命令
        //
        InputStream in = execute("load",
                                 agentLibrary,
                                 isAbsolute ? "true" : "false",
                                 options);
        try {
            int result = readInt(in);
            if (result != 0) {
                throw new AgentInitializationException("Agent_OnAttach failed", result);
            }
        } finally {
            in.close();

        }
    }

最后使用execute方法与Attach Listener进行通信,

    /*
     * Execute the given command in the target VM - specific platform
     * implementation must implement this.
     */
    abstract InputStream execute(String cmd, Object ... args)
        throws AgentLoadException, IOException;

Linux平台上的实现,LinuxVirtualMachine,通过LinuxVirtualMachine#execute可以看到最终是通过UNIX socket与目标VM进行通信。
下面来看下Attach Listener收到load命令后是怎么处理的,Attach Listener的代码在attachListener_linux.cpp,接收到的命令所对应的操作在attachListener.cpp

// names must be of length <= AttachOperation::name_length_max
static AttachOperationFunctionInfo funcs[] = {
  { "agentProperties",  get_agent_properties },
  { "datadump",         data_dump },
  { "dumpheap",         dump_heap },
  { "load",             JvmtiExport::load_agent_library },
  { "properties",       get_system_properties },
  { "threaddump",       thread_dump },
  { "inspectheap",      heap_inspection },
  { "setflag",          set_flag },
  { "printflag",        print_flag },
  { "jcmd",             jcmd },
  { NULL,               NULL }
};

load所对应的JvmtiExport::load_agent_library方法在jvmtiExport.cpp。到这里其实就可以看到是使用了JVM TI。该方法会加载agent的库文件,并且会执行该库的Agent_OnAttach方法。在HotSpotVirtualMachine#loadAgent已经看到agent库名称传入的是instrument,因此在Linux平台上将会加载libinstrument.so。也就是说,libinstrument.soJPDA中的libjdwp.so一样,都是JVM TI的agent,我们也可以用-agentlib:instrument这样的方式来执行instrument。
libinstrument.soAgent_OnAttach实现在InvocationAdapter.c

/*
 *  This will be called once for every -javaagent on the command line.
 *  Each call to Agent_OnLoad will create its own agent and agent data.
 *
 *  The argument tail string provided to Agent_OnLoad will be of form
 *  <jarfile>[=<options>]. The tail string is split into the jarfile and
 *  options components. The jarfile manifest is parsed and the value of the
 *  Premain-Class attribute will become the agent's premain class. The jar
 *  file is then added to the system class path, and if the Boot-Class-Path
 *  attribute is present then all relative URLs in the value are processed
 *  to create boot class path segments to append to the boot class path.
 */
JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *vm, char *tail, void * reserved) { ... }

其中会创建JPLIS Agent,具体实现这里就不深究了,感兴趣的同学可以继续读代码。


最后的最后,总结下Dynamic Attach,Instrumentation以及JVMTI三者的关系。我们通过Attach API拿到VirtualMachine后,有以下两种方式加载agent,

   /**
     * Loads an agent.
     *
     * <p> The agent provided to this method is a path name to a JAR file on the file
     * system of the target virtual machine. This path is passed to the target virtual
     * machine where it is interpreted. The target virtual machine attempts to start
     * the agent as specified by the {@link java.lang.instrument} specification.
     * That is, the specified JAR file is added to the system class path (of the target
     * virtual machine), and the <code>agentmain</code> method of the agent class, specified
     * by the <code>Agent-Class</code> attribute in the JAR manifest, is invoked. This
     * method completes when the <code>agentmain</code> method completes.
     *
     * @param   agent
     *          Path to the JAR file containing the agent.
     *
     * @param   options
     *          The options to provide to the agent's <code>agentmain</code>
     *          method (can be <code>null</code>).
     *
     * @throws  AgentLoadException
     *          If the agent does not exist, or cannot be started in the manner
     *          specified in the {@link java.lang.instrument} specification.
     *
     * @throws  AgentInitializationException
     *          If the <code>agentmain</code> throws an exception
     *
     * @throws  IOException
     *          If an I/O error occurs
     *
     * @throws  NullPointerException
     *          If <code>agent</code> is <code>null</code>.
     */
    public abstract void loadAgent(String agent, String options)
        throws AgentLoadException, AgentInitializationException, IOException;
    /**
     * Loads an agent library.
     *
     * <p> A <a href="../../../../../../../../technotes/guides/jvmti/index.html">JVM
     * TI</a> client is called an <i>agent</i>. It is developed in a native language.
     * A JVM TI agent is deployed in a platform specific manner but it is typically the
     * platform equivalent of a dynamic library. Alternatively, it may be statically linked into the VM.
     * This method causes the given agent library to be loaded into the target
     * VM (if not already loaded or if not statically linked into the VM).
     * It then causes the target VM to invoke the <code>Agent_OnAttach</code> function
     * or, for a statically linked agent named 'L', the <code>Agent_OnAttach_L</code> function
     * as specified in the
     * <a href="../../../../../../../../technotes/guides/jvmti/index.html"> JVM Tools
     * Interface</a> specification. Note that the <code>Agent_OnAttach[_L]</code>
     * function is invoked even if the agent library was loaded prior to invoking
     * this method.
     *
     * <p> The agent library provided is the name of the agent library. It is interpreted
     * in the target virtual machine in an implementation-dependent manner. Typically an
     * implementation will expand the library name into an operating system specific file
     * name. For example, on UNIX systems, the name <tt>L</tt> might be expanded to
     * <tt>libL.so</tt>, and located using the search path specified by the
     * <tt>LD_LIBRARY_PATH</tt> environment variable. If the agent named 'L' is
     * statically linked into the VM then the VM must export a function named
     * <code>Agent_OnAttach_L</code>.</p>
     *
     * <p> If the <code>Agent_OnAttach[_L]</code> function in the agent library returns
     * an error then an {@link com.sun.tools.attach.AgentInitializationException} is
     * thrown. The return value from the <code>Agent_OnAttach[_L]</code> can then be
     * obtained by invoking the {@link
     * com.sun.tools.attach.AgentInitializationException#returnValue() returnValue}
     * method on the exception. </p>
     *
     * @param   agentLibrary
     *          The name of the agent library.
     *
     * @param   options
     *          The options to provide to the <code>Agent_OnAttach[_L]</code>
     *          function (can be <code>null</code>).
     *
     * @throws  AgentLoadException
     *          If the agent library does not exist, the agent library is not
     *          statically linked with the VM, or the agent library cannot be
     *          loaded for another reason.
     *
     * @throws  AgentInitializationException
     *          If the <code>Agent_OnAttach[_L]</code> function returns an error.
     *
     * @throws  IOException
     *          If an I/O error occurs
     *
     * @throws  NullPointerException
     *          If <code>agentLibrary</code> is <code>null</code>.
     *
     * @see     com.sun.tools.attach.AgentInitializationException#returnValue()
     */
    public abstract void loadAgentLibrary(String agentLibrary, String options)
        throws AgentLoadException, AgentInitializationException, IOException;

分别是Instrumentation的agent以及JVMTI的agent,而从上面也看到了,HotSpot的实现,HotSpotVirtualMachineloadAgent最终是调用了loadAgentLibrary("instrument", args),也就是说,Instrumentation本身就是JVMTI的agent(libinstrument.so),而BTrace的btrace-agent.jar则是Instrumentation的agent,有点绕哈:)


嗯,今天就到这吧,have fun ^_^

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
1.btrace扩展是在btrace已由功能上进行的扩展,原有功能和使用方式依然没变。目前版本扩展了两个功能:接口时间监控和接口时间调用树监控。扩展之后的btrace功能使用时都不需要写btrace脚本。 2.使用接口时间监控功能,命令格式为btrace -E mCall pid clazz method,其中clazz 为需要监控的方法所在的类,method为需要监控的方法名称。例如btrace -E mCall 5100 cn.com.icegarden.test.BtraceMain spendTime 3.使用接口时间调用树功能,命令为btrace -E mCallTree clazz method innerClazzes innerMethods 其中clazz为要监控的入口方法所在的类,method为要监控的入口方法名称。 innerClazzes和innerMethods为要监控的入口方法内部调用的方法和所在的类。举例来讲:cn.icegarden.com.A类的a方法中调用了cn.icegarden.com.B类的b方法、cn.icegarden.com.C类的c方法。cn.icegarden.com.C类的c方 法中又调用了cn.icegarden.com.D的d方法。如果要监控A类的a方法在调用这些方法上的时间,使用btrace扩展的调用树功能可以使用命令: btrace -E mCallTree 4432 cn.icegarden.com.A a cn.icegarden.com.B,cn.icegarden.com.C,cn.icegarden.com.D b,c,d 输出结果如下: 0 [9,999ms (1,000ms), 100%] - a +---0 [1,999ms, 20%, 20%] - b `---1,999 [7,000ms (3,000ms), 70%, 70%] - c `---1,999 [4,000ms, 57%, 40%] - d 每一个节点都会输出如下格式的内容: a [b ms,c%,d%] - e 其中a 是打点开始到当前节点开始执行期间的时间间隔毫秒值。 b 是当前节点的方法总共执行的时间。 c 是当前节点执行时间占上一个节点总执行时间的百分比。 d 是当前节点执行时间占入口节点总执行时间的百分比。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值