Подтвердить что ты не робот

Есть ли у java какой-либо механизм для виртуальной машины для отслеживания вызовов методов на себе, без использования javaagent и т.д.?

Я хочу построить диаграммы вызовов "на лету", начиная с произвольного вызова метода или с помощью нового потока, который когда-либо проще, из самого запущенного JVM. (эта часть программного обеспечения будет тестовым прибором для тестирования нагрузки другой части программного обеспечения, которая потребляет графики вызовов)

Я понимаю, что есть некоторые интерфейсы SPI, но похоже, что вам нужно запустить флаг javaagent. Я хочу получить доступ к нему непосредственно в самой виртуальной машине.

В идеале я хотел бы получить обратный вызов для ввода и выхода каждого вызова метода, параметров этого вызова метода и времени в этом методе. Внутри одного потока очевидно.

Я знаю, что AOP, вероятно, может это сделать, но мне просто интересно, есть ли в JDK инструменты, которые позволят мне захватить это.

4b9b3361

Ответ 1

Нет такого API, предоставляемого JVM, даже для агентов, начинающихся с -javaagent. JVM TI - это собственный интерфейс, предоставляемый для собственных агентов, запущенных с помощью опции -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 отладки, используют инструментарий для добавления кода уведомления к каждому интересующему их методу. Преимущество заключается в том, что они никогда не пропускают вызов метода, но, с другой стороны, они добавляют значительные накладные расходы к исполнению, которое может повлиять на результат при поиске горячих точек. И его путь сложнее реализовать. Я не могу дать вам пример кода для такого преобразования байтового кода.

Инструментарий 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();
    }
  }
}