Performance monitoring based on JVMTI

Posted by dch27 on Tue, 18 Jan 2022 22:32:34 +0100

What is JVMTI

JVMTI full JVM Tool Interface is a programming interface defined by Java virtual machine for developing and monitoring JVMs. Through this interface, you can explore some running states inside the JVM and even control the execution of JVM applications.
Note that not all JVM implementations support JVMTI.

JVMTI is a two-way interface. JVMTI's client, or agent, can listen to events of interest through registration. In addition, JVMTI provides many operation functions that can be directly used to control applications.

The JVMTI agent runs in the same process as the target JVM and communicates through the JVMTI to maximize the control capability and minimize the communication cost.

Monitoring and control capability provided by JVMTI interface

All the capabilities provided by the JVMTI specification can be found in https://docs.oracle.com/javase/8/docs/platform/jvmti/jvmti.html#architecture See the document. Here is a brief list of some functions to give us a general understanding of their capabilities.

Function (provided control function)

Memory (VM Heap Memory)

  • Allocate Memory
  • Deallocate memory

thread

  • Get thread state (GetThreadState)
  • Get current thread (GetCurrentThread)
  • Get all threads (GetAllThreads)
  • Suspend thread
  • ...

Stack Frame

  • Get the current call stack of a thread (GetStackTrace)
  • Get call stacks of all threads (GetAllStackTraces)
  • Pop up stack frame

Force Early Return

  • Early return function

Class,Object,Method,Field

  • Obtain relevant information of classes, objects, functions and fields according to jclass, jobobject, jfield ID, jmethodids, etc

event listeners

The following are the complete types of event listening support

typedef struct {
                              /*   50 : VM Initialization Event */
    jvmtiEventVMInit VMInit;
                              /*   51 : VM Death Event */
    jvmtiEventVMDeath VMDeath;
                              /*   52 : Thread Start */
    jvmtiEventThreadStart ThreadStart;
                              /*   53 : Thread End */
    jvmtiEventThreadEnd ThreadEnd;
                              /*   54 : Class File Load Hook */
    jvmtiEventClassFileLoadHook ClassFileLoadHook;
                              /*   55 : Class Load */
    jvmtiEventClassLoad ClassLoad;
                              /*   56 : Class Prepare */
    jvmtiEventClassPrepare ClassPrepare;
                              /*   57 : VM Start Event */
    jvmtiEventVMStart VMStart;
                              /*   58 : Exception */
    jvmtiEventException Exception;
                              /*   59 : Exception Catch */
    jvmtiEventExceptionCatch ExceptionCatch;
                              /*   60 : Single Step */
    jvmtiEventSingleStep SingleStep;
                              /*   61 : Frame Pop */
    jvmtiEventFramePop FramePop;
                              /*   62 : Breakpoint */
    jvmtiEventBreakpoint Breakpoint;
                              /*   63 : Field Access */
    jvmtiEventFieldAccess FieldAccess;
                              /*   64 : Field Modification */
    jvmtiEventFieldModification FieldModification;
                              /*   65 : Method Entry */
    jvmtiEventMethodEntry MethodEntry;
                              /*   66 : Method Exit */
    jvmtiEventMethodExit MethodExit;
                              /*   67 : Native Method Bind */
    jvmtiEventNativeMethodBind NativeMethodBind;
                              /*   68 : Compiled Method Load */
    jvmtiEventCompiledMethodLoad CompiledMethodLoad;
                              /*   69 : Compiled Method Unload */
    jvmtiEventCompiledMethodUnload CompiledMethodUnload;
                              /*   70 : Dynamic Code Generated */
    jvmtiEventDynamicCodeGenerated DynamicCodeGenerated;
                              /*   71 : Data Dump Request */
    jvmtiEventDataDumpRequest DataDumpRequest;
                              /*   72 */
    jvmtiEventReserved reserved72;
                              /*   73 : Monitor Wait */
    jvmtiEventMonitorWait MonitorWait;
                              /*   74 : Monitor Waited */
    jvmtiEventMonitorWaited MonitorWaited;
                              /*   75 : Monitor Contended Enter */
    jvmtiEventMonitorContendedEnter MonitorContendedEnter;
                              /*   76 : Monitor Contended Entered */
    jvmtiEventMonitorContendedEntered MonitorContendedEntered;
                              /*   77 */
    jvmtiEventReserved reserved77;
                              /*   78 */
    jvmtiEventReserved reserved78;
                              /*   79 */
    jvmtiEventReserved reserved79;
                              /*   80 : Resource Exhausted */
    jvmtiEventResourceExhausted ResourceExhausted;
                              /*   81 : Garbage Collection Start */
    jvmtiEventGarbageCollectionStart GarbageCollectionStart;
                              /*   82 : Garbage Collection Finish */
    jvmtiEventGarbageCollectionFinish GarbageCollectionFinish;
                              /*   83 : Object Free */
    jvmtiEventObjectFree ObjectFree;
                              /*   84 : VM Object Allocation */
    jvmtiEventVMObjectAlloc VMObjectAlloc;
} jvmtiEventCallbacks;

Here are some common functions

  • Thread related events
    • Thread start
    • Thread end
    • ...
  • Class load event
    • Class file loading (ClassFileLoadHok)
    • Class is loaded into the virtual machine (ClassLoad)
    • Class preparation phase completed (ClassPrepare)
  • Abnormal event
  • Method execution
    • Method start execution (MethodEntry)
    • Method execution end (MethodExit)
    • ...
  • GC events
    • GC startup (garbage collection start)
    • GC end (garbage collection finish)
  • Object event
    • Allocate memory for an object (VMObjectAlloc)
    • Freeing memory occupied by an object (ObjectFree)

JVMTI Header

The complete JVMTI API is available through JVMTI H, which can be obtained in Android through This link see.

Implementation of JvmTi in Android

Android virtual machine did not realize JVMTI 1.2 until the beginning of 8.0 system. According to the current system version release, 8.0 can cover more than 65% of user devices. In Android, JvmTi is also known as ART Ti (after all, it is an ART virtual machine), which adds some restrictions, such as (excerpted from the description on the official website):

  1. First, the code that provides the proxy interface JVMTI is implemented as a runtime plug-in (rather than the core component of the runtime). Plug in loading may be restricted, which prevents the agent from finding any interface points
  • Second, ActivityManager classes and runtime processes only allow agents to connect to debuggable applications. Debuggable applications are signed off by their developers for analysis and instrumentation, and will not be distributed to end users. The Google Play store does not allow the release of debuggable apps. This ensures that common applications, including core components, cannot be detected or manipulated.

In Android, the architecture of JVMTI and Agent is shown in the figure below

There are two ways to use JVMTI in Android

  1. Connection agent on virtual machine startup
  2. Runtime. Load agent into current process

The specific use processes of the two methods can also be referred to Official website Introduction, not detailed here. The specific implementation of its jvmti interface can be found in here Found. It was eventually compiled into libopenjdkjvmti.so . Take my 64 bit mobile phone as an example, in the directory / lib64 / system / libopenjdkjvmti So can find the dynamic library

Some application examples of JVMTI

Here is a brief introduction to some applications of writing JVMTI.
Take Android official as an example

  • Implement the Apply Changes function based on JVMTI (redefine class function)
    • This part of agent implementation can be implemented in here Find out, from here you can understand the core logic of hot loading based on JVM ti
  • Some features of Android Studio Profiler
  • JVMTI is the back-end implementation of JDI system. We can realize the program debugging capability based on JVMTI. For example, meituan has an article introducing the function of online debugging based on JDWP, which can be referred to for JDPA system oracle document , the following is a relationship structure diagram of JVMTI, JDWP and JDI


Here are some other ideas. If you can break through the limitation of art virtual machine that debugger = false cannot use jvmti, can you realize more powerful performance monitoring online, or can you separate the offline environment from Android Studio to realize performance collection tools?
First of all, based on the powerful capabilities of JVMTI, we can at least realize the following functions offline

  • Based on the ability to obtain all thread stacks, the function call flame graph is generated by sampling
  • Memory monitoring based on object memory allocation and release function
  • Lock waiting monitoring based on monitorcontented
  • GC duration monitoring based on garbage collection
  • ...

Use JVMTI in the release package (debugger =true)

As mentioned earlier, due to the limitations of Android virtual machine, JVMTI Agent can only be dynamically loaded in non debug packages by default. So let's first understand how Android limits

JVMTI usage environment restrictions

    public static void attachJvmtiAgent(@NonNull String library, @Nullable String options,
            @Nullable ClassLoader classLoader) throws IOException {
        Preconditions.checkNotNull(library);
        Preconditions.checkArgument(!library.contains("="));

        if (options == null) {
            VMDebug.attachAgent(library, classLoader);
        } else {
            VMDebug.attachAgent(library + "=" + options, classLoader);
        }
    }

The attatchJvmtiAgent of the Debug class finally called vmdebug Attatchagent function, which finally calls the native layer Art / Runtime / native / Dalvik_ system_ VMDebug. Nativeatatchagent function of CC

static void VMDebug_nativeAttachAgent(JNIEnv* env, jclass, jstring agent, jobject classloader) {
  if (agent == nullptr) {
    ScopedObjectAccess soa(env);
    ThrowNullPointerException("agent is null");
    return;
  }
	
  // Judge whether to allow Jdwp
  if (!Dbg::IsJdwpAllowed()) {
    ScopedObjectAccess soa(env);
    ThrowSecurityException("Can't attach agent, process is not debuggable.");
    return;
  }

  std::string filename;
  {
    ScopedUtfChars chars(env, agent);
    if (env->ExceptionCheck()) {
      return;
    }
    filename = chars.c_str();
  }

  Runtime::Current()->AttachAgent(env, filename, classloader);
}



//art/runtime/debugger.cc

// JDWP is allowed unless the Zygote forbids it.
static bool gJdwpAllowed = true;

// Set Jdwp available
void Dbg::SetJdwpAllowed(bool allowed) {
  gJdwpAllowed = allowed;
}

//Setting Jdwp is not available
bool Dbg::IsJdwpAllowed() {
  return gJdwpAllowed;
}

VMDebug_ The nativeattachagent function is through debugger CC China's gJdwpAllowed variable. From the definition, it can be seen that this variable defaults to true. We continue to track where the variable was modified, and finally track that the variable was configured when the zygote process fork ed

static void ZygoteHooks_nativePostForkChild(JNIEnv* env,
                                            jclass,
                                            jlong token,
                                            jint runtime_flags,
                                            jboolean is_system_server,
                                            jboolean is_zygote,
                                            jstring instruction_set) {
  DCHECK(!(is_system_server && is_zygote));
  // Set the runtime state as the first thing, in case JIT and other services
  // start querying it.
  Runtime::Current()->SetAsZygoteChild(is_system_server, is_zygote);

  Thread* thread = reinterpret_cast<Thread*>(token);
  // Our system thread ID, etc, has changed so reset Thread state.
  thread->InitAfterFork();
  // Configure some features of the Debug program
  runtime_flags = EnableDebugFeatures(runtime_flags);
  
}


static uint32_t EnableDebugFeatures(uint32_t runtime_flags) {
  Runtime* const runtime = Runtime::Current();
  if ((runtime_flags & DEBUG_ENABLE_CHECKJNI) != 0) {
    JavaVMExt* vm = runtime->GetJavaVM();
    if (!vm->IsCheckJniEnabled()) {
      LOG(INFO) << "Late-enabling -Xcheck:jni";
      vm->SetCheckJniEnabled(true);
      // There's only one thread running at this point, so only one JNIEnv to fix up.
      Thread::Current()->GetJniEnv()->SetCheckJniEnabled(true);
    } else {
      LOG(INFO) << "Not late-enabling -Xcheck:jni (already on)";
    }
    runtime_flags &= ~DEBUG_ENABLE_CHECKJNI;
  }

  if ((runtime_flags & DEBUG_ENABLE_JNI_LOGGING) != 0) {
    gLogVerbosity.third_party_jni = true;
    runtime_flags &= ~DEBUG_ENABLE_JNI_LOGGING;
  }
	
  
  //Configure debugger gJdwpAllowed variable for CC
  Dbg::SetJdwpAllowed((runtime_flags & DEBUG_ENABLE_JDWP) != 0);
  runtime_flags &= ~DEBUG_ENABLE_JDWP;

  const bool safe_mode = (runtime_flags & DEBUG_ENABLE_SAFEMODE) != 0;
  if (safe_mode) {
    // Only quicken oat files.
    runtime->AddCompilerOption("--compiler-filter=quicken");
    runtime->SetSafeMode(true);
    runtime_flags &= ~DEBUG_ENABLE_SAFEMODE;
  }
}

To summarize: when starting our application, zygote performs forkProcess according to the runtime_ The flag bit in FLGS calls the Dbg::SetJdwpAllowed function. When it is a non debug package, the gJdwpAllowed variable is set to false, so debug is executed at runtime The attachjvmtiagent function throws an exception.

Break through the limit

In the previous section, we briefly analyzed the limitations of JDWP under non debug packages. We can easily think of changing the variable value of gJdwpAllowed to true at runtime, and then calling the attachJvmtiAgent function.
Here, I use the SandHook library to realize this function. The corresponding function symbol of Dbg::setJdwoAllowed is_ Zn3art3dbg14setjdwpallowedeb (under 64 bits),

The SandHook function mainly implements the InlineHook function. In this article, InlineHook is not required, but InlineHook is used in my project to realize other functions.
In fact, you only need to use functions that can crack the Android} dlopen limit, such as dlfunciotns

    if (sizeof(size_t) == 8) {
        if (fileExits("/apex/com.android.runtime/lib64/libart.so")) {
            ArtLibPath = "/apex/com.android.runtime/lib64/libart.so";
        } else {
            ArtLibPath = "/system/lib64/libart.so";
        }
        auto (*SetJdwpAllowed)(bool) = reinterpret_cast<void (*)(bool)>(SandGetSym(ArtLibPath,
                                                                                   "_ZN3art3Dbg14SetJdwpAllowedEb"));

        if (SetJdwpAllowed != nullptr) {
            SetJdwpAllowed(true);
        }
          
        auto
        (*setJavaDebuggable)(void *, bool) = reinterpret_cast<void (*)(void *, bool)>(SandGetSym(
                ArtLibPath,
                "_ZN3art7Runtime17SetJavaDebuggableEb"));
        if (setJavaDebuggable != nullptr) {
            setJavaDebuggable(ArtHelper::getRuntimeInstance(), true);
            ALOGE("zxw %s", "setJavaDebuggable true");
        }


    }

After modifying the variable, call the attatchJvmtiAgent function. At this time, the program will not crash. However, the Jdwp Agent failed to load. You can see the log output from the console from the log

Openjdkjvmti plugin was loaded on a non-debuggable Runtime. Plugin was loaded too late to change runtime state to DEBUGGABLE. Only kArtTiVersion (0x70010200) environments are available. Some functionality might not work properly.

Search the above logs in the source code, and we find the corresponding code

The output execution timing point of the above log is output when the Agent is attached to the process_ Failed to get JvmTiEnv in onattach callback function

jint SetupJvmtiEnv(JavaVM *vm,jvmtiEnv** jvmti) {
    jint res =  vm->GetEnv(reinterpret_cast<void **>(jvmti), JVMTI_VERSION_1_2);
    if (res != JNI_OK || jvmti == nullptr) {
        ALOGE("==========Agent vm->GetEnv VERSION_1_2 failed");
    }

    return res;
}

Therefore, we continue to go deep into the code and find some code that returns the JvmTiEnv pointer from the Jvmti source code. This part is implemented in openjdkjvmti CC medium

Continue to follow up the implementation of IsFullJvmtiAvailable

It turns out that when obtaining the Jvmti pointer in the source code, it is judged according to the IsJavaDebuggable() function of the Runtime class and does not directly depend on the Dbg class. Although we have modified the gJdwpAllowed variable of the Dbg class, the is of the Runtime_ java_ debuggable_ The variable has been modified in the Process Fork stage. Our lag modification of Dbg::gJdwpAllowed cannot affect places that do not directly depend on the Dbg::gJdwpAllowed variable. Therefore, we also need to manually modify the variable of the Runtime object. The modification method is the same as that of Dbg variable. The code is as follows

auto
(*setJavaDebuggable)(void *, bool) = reinterpret_cast<void (*)(void *, bool)>(SandGetSym(
ArtLibPath,"_ZN3art7Runtime17SetJavaDebuggableEb"));

if (setJavaDebuggable != nullptr) {
  setJavaDebuggable(ArtHelper::getRuntimeInstance(), true);
}

After modification, it is tested in the non debug package, and the basic functions are consistent with the debug package

Here, I only monitor GC events, thread creation and destruction and object allocation. Whether other events can be used normally has not been fully tested. In principle, the implementation of JVMTI is similar to the monitoring implementation mechanism provided by most systems and virtual machines, which is realized by writing relevant monitoring code in the source code in advance, As long as the code of the monitoring point depends on the two variables Dbg and Runtime, it should work normally.

Evaluation point for online performance monitoring using JVMTI

In the previous section, we realized the ability to use JVMTI in the production environment. However, there are still many points to be considered before JVMTI is really used in the online production environment, such as:

  • compatibility
    • It can only be used in systems 8.0 and above
    • Relying on dlsym to call system functions, it is not necessary to check whether Android officials or third-party manufacturers have modified these functions and need to test compatibility
  • performance
    • After JVMTI is enabled, some features of ART virtual machine will be disabled (for example, only interpretation and execution can be performed). It is necessary to evaluate the impact points on relevant performance

reference