java是否有任何机制让VM自己跟踪方法调用,而不使用javaagent等?

我想动态地构建调用图,从任意方法调用开始,或者从运行的JVM本身中使用新线程开始构建调用图。 (这个软件将成为负载测试另一个消耗调用图的软件的测试工具)

我知道有一些SPI接口,但看起来你需要用它们运行-javaagent标志。 我想直接在VM本身访问它。

理想情况下,我想获得每个方法调用的入口和出口的回调,该方法调用的参数以及该方法中的时间。 显然在单个线程内。

我知道AOP可能会这样做,但我只是想知道JDK中是否有工具可以让我捕获它。

即使对于以-javaagent开头的代理,JVM也没有提供这样的API。 JVM TI是为使用-agent选项启动的本机代理或调试器提供的本机接口。 Java代理可能使用Instrumentation API,它提供类检测的低级function,但没有直接的分析function。

有两种类型的分析实现,通过采样和通过检测。

采样通过定期记录堆栈跟踪(样本)来工作。 这不会跟踪每个方法调用,但仍会检测到在记录的堆栈跟踪中多次出现的热点。 优点是它不需要代理也不需要特殊的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,CallTree> ch:t.entrySet()) { CallTree cht=ch.getValue(); StringBuilder sb = new StringBuilder(); for(int p=0; p, CallTree> { long count=1, cpu; CallTree(boolean cpu) { if(cpu) this.cpu++; } CallTree getOrAdd(String cl, String m, boolean cpu) { List 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(); } } } 

您可以修改每个方法的字节码,添加例程以记录方法的进入/退出事件。 Javassist将帮助您http://www.csg.ci.iu-tokyo.ac.jp/~chiba/javassist/

另请查看一个很好的教程: https : //today.java.net/pub/a/today/2008/04/24/add-logging-at-class-load-time-with-instrumentation.html