java是不是有任何机制让VM在不使用javaagent等的情况下跟踪自身的方法调用?
Posted
技术标签:
【中文标题】java是不是有任何机制让VM在不使用javaagent等的情况下跟踪自身的方法调用?【英文标题】:Does java have any mechanism for a VM to trace method calls on itself, without using javaagent, etc?java是否有任何机制让VM在不使用javaagent等的情况下跟踪自身的方法调用? 【发布时间】:2013-11-19 23:15:52 【问题描述】:我想在运行中构建调用图,从任意方法调用或新线程开始,这更容易,从正在运行的 JVM 本身开始。 (该软件将成为负载测试另一个使用调用图的软件的测试夹具)
我知道有一些 SPI 接口,但看起来您需要使用它们运行 -javaagent 标志。我想直接在 VM 本身中访问它。
理想情况下,我希望获取每个方法调用的进入和退出的回调、该方法调用的参数以及该方法中的时间。显然在一个线程内。
我知道 AOP 可能可以做到这一点,但我只是想知道 JDK 中是否有工具可以让我捕捉到这一点。
【问题讨论】:
你可以看看当前的 Java 调试器接口是什么,因为 VisualVM 必须能够以某种方式工作。 (似乎是JPDA 和它所包含的东西。)也就是说,我怀疑你会得到关于你想要做的具体任务的非常具体的指导。 (我也不能保证调试器接口甚至可以完成它。) @millimoose - 我以为我看到了一个 SPI 接口,用于在 JDK 的某个地方获取此信息,但我现在找不到它。也许我弄错了。 【参考方案1】:JVM 没有提供这样的 API — 即使对于以 -javaagent
开头的代理也是如此。 JVM TI 是为使用-agent
选项启动的本机代理或调试器提供的本机接口。 Java 代理可能使用Instrumentation API,它提供了类检测的低级功能,但没有直接的分析能力。
有两种类型的分析实现,通过采样和通过检测。
采样通过定期记录堆栈跟踪(样本)来工作。这不会跟踪每个方法调用,但仍会检测热点,因为它们在记录的堆栈跟踪中多次出现。优点是它不需要代理,也不需要特殊的 API,并且您可以控制探查器的开销。您可以通过ThreadMXBean 实现它,它允许您获取所有正在运行的线程的堆栈跟踪。事实上,即使Thread.getAllStackTraces()
也可以,但ThreadMXBean
提供了有关线程的更详细信息。
因此,主要任务是为堆栈跟踪中找到的方法实现有效的存储结构,即将相同方法的出现折叠成单个调用树项。
这是一个在自己的 JVM 上运行的非常简单的采样器示例:
import java.lang.Thread.State;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.util.*;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
public class Sampler
private static final ThreadMXBean TMX=ManagementFactory.getThreadMXBean();
private static String CLASS, METHOD;
private static CallTree ROOT;
private static ScheduledExecutorService EXECUTOR;
public static synchronized void startSampling(String className, String method)
if(EXECUTOR!=null) throw new IllegalStateException("sampling in progress");
System.out.println("sampling started");
CLASS=className;
METHOD=method;
EXECUTOR = Executors.newScheduledThreadPool(1);
// "fixed delay" reduces overhead, "fixed rate" raises precision
EXECUTOR.scheduleWithFixedDelay(new Runnable()
public void run()
newSample();
, 150, 75, TimeUnit.MILLISECONDS);
public static synchronized CallTree stopSampling() throws InterruptedException
if(EXECUTOR==null) throw new IllegalStateException("no sampling in progress");
EXECUTOR.shutdown();
EXECUTOR.awaitTermination(Long.MAX_VALUE, TimeUnit.DAYS);
EXECUTOR=null;
final CallTree root = ROOT;
ROOT=null;
return root;
public static void printCallTree(CallTree t)
if(t==null) System.out.println("method not seen");
else printCallTree(t, 0, 100);
private static void printCallTree(CallTree t, int ind, long percent)
long num=0;
for(CallTree ch:t.values()) num+=ch.count;
if(num==0) return;
for(Map.Entry<List<String>,CallTree> ch:t.entrySet())
CallTree cht=ch.getValue();
StringBuilder sb = new StringBuilder();
for(int p=0; p<ind; p++) sb.append(' ');
final long chPercent = cht.count*percent/num;
sb.append(chPercent).append("% (").append(cht.cpu*percent/num)
.append("% cpu) ").append(ch.getKey()).append(" ");
System.out.println(sb.toString());
printCallTree(cht, ind+2, chPercent);
static class CallTree extends HashMap<List<String>, CallTree>
long count=1, cpu;
CallTree(boolean cpu) if(cpu) this.cpu++;
CallTree getOrAdd(String cl, String m, boolean cpu)
List<String> key=Arrays.asList(cl, m);
CallTree t=get(key);
if(t!=null) t.count++; if(cpu) t.cpu++;
else put(key, t=new CallTree(cpu));
return t;
static void newSample()
for(ThreadInfo ti:TMX.dumpAllThreads(false, false))
final boolean cpu = ti.getThreadState()==State.RUNNABLE;
StackTraceElement[] stack=ti.getStackTrace();
for(int ix = stack.length-1; ix>=0; ix--)
StackTraceElement ste = stack[ix];
if(!ste.getClassName().equals(CLASS)||!ste.getMethodName().equals(METHOD))
continue;
CallTree t=ROOT;
if(t==null) ROOT=t=new CallTree(cpu);
for(ix--; ix>=0; ix--)
ste = stack[ix];
t=t.getOrAdd(ste.getClassName(), ste.getMethodName(), cpu);
探查器在不通过调试 API 的情况下寻找每个方法调用,使用工具将通知代码添加到他们感兴趣的每个方法中。优点是他们永远不会错过方法调用,但另一方面它们会增加大量开销在搜索热点时可能影响结果的执行。而且实现起来要复杂得多。我不能给你一个这样的字节码转换的代码示例。
Instrumentation API 仅提供给 Java 代理,但如果您想进入 Instrumentation 方向,这里有一个程序演示如何连接到它自己的 JVM 并将其自身加载为 Java 代理:
import java.io.*;
import java.lang.instrument.Instrumentation;
import java.lang.management.ManagementFactory;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.UUID;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
// this API comes from the tools.jar of your JDK
import com.sun.tools.attach.*;
public class SelfAttacher
public static Instrumentation BACK_LINK;
public static void main(String[] args) throws Exception
// create a special property to verify our JVM connection
String magic=UUID.randomUUID().toString()+'/'+System.nanoTime();
System.setProperty("magic", magic);
// the easiest way uses the non-standardized runtime name string
String name=ManagementFactory.getRuntimeMXBean().getName();
int ix=name.indexOf('@');
if(ix>=0) name=name.substring(0, ix);
VirtualMachine vm;
getVM:
try
vm = VirtualMachine.attach(name);
if(magic.equals(vm.getSystemProperties().getProperty("magic")))
break getVM;
catch(Exception ex)
// if the easy way failed, try iterating over all local JVMs
for(VirtualMachineDescriptor vd:VirtualMachine.list()) try
vm=VirtualMachine.attach(vd);
if(magic.equals(vm.getSystemProperties().getProperty("magic")))
break getVM;
vm.detach();
catch(Exception ex)
// could not find our own JVM or could not attach to it
return;
System.out.println("attached to: "+vm.id()+'/'+vm.provider().type());
vm.loadAgent(createJar().getAbsolutePath());
synchronized(SelfAttacher.class)
while(BACK_LINK==null) SelfAttacher.class.wait();
System.out.println("Now I have hands on instrumentation: "+BACK_LINK);
System.out.println(BACK_LINK.isModifiableClass(SelfAttacher.class));
vm.detach();
// create a JAR file for the agent; since our class is already in class path
// our jar consisting of a MANIFEST declaring our class as agent only
private static File createJar() throws IOException
File f=File.createTempFile("agent", ".jar");
f.deleteOnExit();
Charset cs=StandardCharsets.ISO_8859_1;
try(FileOutputStream fos=new FileOutputStream(f);
ZipOutputStream os=new ZipOutputStream(fos))
os.putNextEntry(new ZipEntry("META-INF/MANIFEST.MF"));
ByteBuffer bb = cs.encode("Agent-Class: "+SelfAttacher.class.getName());
os.write(bb.array(), bb.arrayOffset()+bb.position(), bb.remaining());
os.write(10);
os.closeEntry();
return f;
// invoked when the agent is loaded into the JVM, pass inst back to the caller
public static void agentmain(String agentArgs, Instrumentation inst)
synchronized(SelfAttacher.class)
BACK_LINK=inst;
SelfAttacher.class.notifyAll();
【讨论】:
【参考方案2】:您可以修改每个方法的字节码,添加例程来记录方法的进入/退出事件。 Javassist 会帮助你http://www.csg.ci.i.u-tokyo.ac.jp/~chiba/javassist/
还可以查看一个不错的教程:https://today.java.net/pub/a/today/2008/04/24/add-logging-at-class-load-time-with-instrumentation.html
【讨论】:
以上是关于java是不是有任何机制让VM在不使用javaagent等的情况下跟踪自身的方法调用?的主要内容,如果未能解决你的问题,请参考以下文章
如何在不使用java发送任何邮件的情况下检查域中是不是存在电子邮件ID