Есть ли в java какой-либо механизм, позволяющий виртуальной машине отслеживать вызовы методов без использования javaagent и т. Д.?
Я хочу создавать графики вызовов на лету, начиная с произвольного вызова метода или с нового потока, что всегда проще, изнутри самой исполняемой JVM. (эта часть программного обеспечения будет испытательным приспособлением для нагрузочного тестирования другой части программного обеспечения, которая использует графы вызовов)
Я понимаю, что есть некоторые интерфейсы SPI, но, похоже, вам нужно запустить флаг -javaagent с ними. Я хочу получить доступ к этому непосредственно в самой виртуальной машине.
В идеале я хотел бы получить обратный вызов для входа и выхода каждого вызова метода, параметров этого вызова метода и времени в этом методе. Внутри одной темы, очевидно.
Я знаю, что AOP, вероятно, мог бы сделать это, но мне просто интересно, есть ли в JDK инструменты, которые позволили бы мне это зафиксировать.
2 ответа
В JVM нет такого API, даже для агентов, запущенных с -javaagent
, TI JVM - это собственный интерфейс, предоставленный для собственных агентов, запущенных с -agent
вариант или для отладчиков. Агенты Java могут использовать 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();
}
}
}
Вы можете изменить байт-код каждого метода, добавив подпрограмму в журнал событий входа / выхода метода. 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