插桩?其实JVM做了很多

入口

对JVM中的字节码进行替换,这是JVM通过jvmti接口对外提供的扩展功能。如果要通过Java语言来实现(jvmti提供的是C接口),
可以通过javaagent的方式或者通过tools.jar提供的attach接口进行jar包的加载。直接使用jvmti接口,则可以参照jvmti文档编写一个agent让JVM加载。

javaagent实现

注意: 本文仅介绍在线插装方式

javaagent需要以jar包的形式加载,和普通的jar没什么区别,唯一需要在MANIFEST.MF文件中增加一些入口。
对于maven工程,可以使用maven-jar-plugin来生成该文件内容:

<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-jar-plugin</artifactId>
    <version>3.1.0</version>
    <configuration>
        <archive>
            <manifestEntries>
                <Agent-Class>
                    com.cainiao.isimu.instrument.test.inst.InstrumentMain
                </Agent-Class>
                <Can-Redefine-Classes>
                    true
                </Can-Redefine-Classes>
                <Can-Retransform-Classes>
                    true
                </Can-Retransform-Classes>
            </manifestEntries>
        </archive>
    </configuration>
</plugin>

这样会在最终生成的MANIFEST.MF中增加Agent-ClassCan-Redefine-ClassesCan-Retransform-Classes这三项,
注意修改Agent-Class项的值,设置成agent入口类。

入口类和标准Java入口不一样,需要在类中定义签名为public static void agentmain(String agentArgs, Instrumentation inst)的方法。JVM加载该类之后,会执行这个函数。

通过这个入口就可以获取到Instrumentation对象了,后面所有的类转换操作都需要通过该对象进行。对于插桩来说,主要是addTransformer方法来添加类文件转换器,retransformClasses来发起类转换。

具体让JVM动态加载该包的方式,详见JVMTI那些事——加载

直接使用jvmti接口

通过jvmti接口编写基于c/c++语言的agent可以参考:JVMTI那些事——c++编写的agent
该文没有尝试调用jvmti中的字节码转换函数。

JVM开始工作

通过jvmti接口提交了类转换请求之后,JVM就需要开始工作了。hotspot对jvmti的实现在hotspot/src/share/vm/prims/jvmtiEnv.cpp文件中,对应转换类的函数实现是jvmtiError
JvmtiEnv::RetransformClasses(jint class_count, const jclass* classes)

这个函数前面是各种检查和验证,我们唯一关心的是最后两行:

“`C++
VM_RedefineClasses op(class_count, class_definitions, jvmti_class_load_kind_retransform);
VMThread::execute(&op);


这里创建了一个名为`VM_RedefineClasses`的vmop,然后通知VMThread进行处理。 ### VM_Operation `VM_Operation`是虚拟机级别的操作,这些操作包含了所有JVM的内置操作,例如GC、获取线程栈等等。 这个类是所有这些操作的基类,该类定义在`hotspot/src/share/vm/runtime/vm_operations.hpp`我们来看下一些默认的操作。 首先,该类定义了`Mode`和`VMOp_Type`两个枚举。第一个表示该操作的模式,第二个表示该操作的类型。事实上所有的类型都在文件开头的宏定义中写明了,这里我们只关心`RedefineClasses`这个类型。`Mode`包括这四种: ```C++ enum Mode { _safepoint, // blocking, safepoint, vm_op C-heap allocated _no_safepoint, // blocking, no safepoint, vm_op C-Heap allocated _concurrent, // non-blocking, no safepoint, vm_op C-Heap allocated _async_safepoint // non-blocking, safepoint, vm_op C-Heap allocated }; </code></pre> 根据注释可以知道,<code>VM_Operation</code>的模式主要分为阻塞和非阻塞。 主要成员函数有: ```C++ // Called by VM thread - does in turn invoke doit(). Do not override this void evaluate(); // evaluate() is called by the VMThread and in turn calls doit(). // If the thread invoking VMThread::execute((VM_Operation) is a JavaThread, // doit_prologue() is called in that thread before transferring control to // the VMThread. // If doit_prologue() returns true the VM operation will proceed, and // doit_epilogue() will be called by the JavaThread once the VM operation // completes. If doit_prologue() returns false the VM operation is cancelled. virtual void doit() = 0; virtual bool doit_prologue() { return true; }; virtual void doit_epilogue() {}; // Note: Not called if mode is: _concurrent // Configuration. Override these appropriatly in subclasses. virtual VMOp_Type type() const = 0; virtual Mode evaluation_mode() const { return _safepoint; } virtual bool allow_nested_vm_operations() const { return false; } virtual bool is_cheap_allocated() const { return false; } virtual void oops_do(OopClosure f) { /* do nothing */ }; virtual bool evaluate_at_safepoint() const { return evaluation_mode() == _safepoint || evaluation_mode() == _async_safepoint; } virtual bool evaluate_concurrently() const { return evaluation_mode() == _concurrent || evaluation_mode() == _async_safepoint; }

注释里面说明了`evaluate`函数不能覆盖,实际业务逻辑必须在`doit`函数中实现。如果需要在实际操作之前有前置操作,可以在`doit_prologue`函数中执行。特别注意的是,`doit_prologue`函数是在Java线程中运行的,这个执行线程实际就是提交`VM_Operation`的线程。 `evaluation_mode`函数返回当前操作的模式,如果子类不覆盖,默认值是阻塞模式。 ### VM_RedefineClasses `VM_RedefineClasses`是`VM_Operation`的子类,实现了类转换的所有逻辑。该类定义和实现分别在`hotspot/src/share/vm/prims/jvmtiRedefineClasses.hpp`和`hotspot/src/share/vm/prims/jvmtiRedefineClasses.cpp`。 对于一个`VM_Operation`的子类,首先需要关心`evaluation_mode`函数。`VM_RedefineClasses`类中找不到该函数,因此它是一个需要在safepoint阻塞的操作。 然后就是核心操作,按照前文说的调用顺序,既`doit_prologue`、`doit`、`doit_epilogue`。 代码比较复杂,我们先从注释上了解每个步骤做了什么: ```C++ // 1) doit_prologue() is called by the JavaThread on the way to a // safepoint. It does parameter verification and loads scratch_class // which involves: // - parsing the incoming class definition using the_class' class // loader and security context // - linking scratch_class // - merging constant pools and rewriting bytecodes as needed // for the merged constant pool // - verifying the bytecodes in scratch_class // - setting up the constant pool cache and rewriting bytecodes // as needed to use the cache // - finally, scratch_class is compared to the_class to verify // that it is a valid replacement class // - if everything is good, then scratch_class is saved in an // instance field in the VM operation for the doit() call // // Note: A JavaThread must do the above work. </code></pre> 在<code>doit_prologue</code>阶段,整个操作都是在Java线程中进行的,因此不会阻塞VMThread,也不会被计入safepoint的耗时。 注意整个源码中<code>the_class</code>表示待替换的类,<code>scratch_class</code>表示新的类。该阶段主要做的就是准备需要的字节码, 如果业务代码中准备新的字节码时间比较长(前面提到的获取新字节码的回调也是在这里发生),这个阶段时间就会变长,但是不会阻塞JVM的核心线程。 然后是最核心的<code>doit</code>函数。 ```c++ // 2) doit() is called by the VMThread during a safepoint. It installs // the new class definition(s) which involves: // - retrieving the scratch_class from the instance field in the // VM operation // - house keeping (flushing breakpoints and caches, deoptimizing // dependent compiled code) // - replacing parts in the_class with parts from scratch_class // - adding weak reference(s) to track the obsolete but interesting // parts of the_class // - adjusting constant pool caches and vtables in other classes // that refer to methods in the_class. These adjustments use the // ClassLoaderDataGraph::classes_do() facility which only allows // a helper method to be specified. The interesting parameters // that we would like to pass to the helper method are saved in // static global fields in the VM operation. // - telling the SystemDictionary to notice our changes // // Note: the above work must be done by the VMThread to be safe.
该函数**必须**在`VMThread`中进行,且调用会从safepoint开始阻塞。该函数的操作主要氛围两个阶段:

* 第一阶段主要是准备工作,执行内容包括:
    - 移除所有断点信息(所以进行过转换的类,需要重新debug,否则是没法断点的)
    - 去优化(Deoptimize)
    - 交换方法和常量池
    - 交换内部类
    - 初始化虚函数表(vtable)和接口函数表(itable)
    - 复制源码相关信息
    - 替换类版本信息
* 第二阶段是依赖处理,主要内容有:
    - 调整所有依赖被替换方法的类的常量池缓存和vtable
    - [JSR-292](https://jcp.org/en/jsr/detail?id=292)支持(invokedynamic)
    - 刷新对象实例缓存,去除废弃的方法
    - 增加被修改类(及其内联类)的`classRedefinedCount`属性

最后的`doit_epilogue`函数比较简单,主要做清理工作和最终统计工作。

## VM_RedefineClasses中的计时器

在看`VM_RedefineClasses`类,我们可以发现有几个定时器相关的字段:

```C++
elapsedTimer  _timer_rsc_phase1;
elapsedTimer  _timer_rsc_phase2;
elapsedTimer  _timer_vm_op_prologue;

前两个用于对doit函数中的两个阶段分别计时,最后一个为前置操作计时(主要是加载和解析新类时间)。

整个类替换的计时和日志,定义在jvmtiRedefineClassesTrace.hpp文件中,里面通过宏来判断是否输出
该阶段的日志。如果需要打印对应的日志,可以在JVM启动的时候添加-XX:TraceRedefineClasses=XX来指定。
其中XX参考头文件中的注释,如果要打印所有日志,可以设置为33554431;如果只需要打印计时器时间,可以设置为4。

另外,如果还需要了解整个替换过程中对JVM的开销,还以通过在JVM启动时增加参数-XX:+PrintSafepointStatistics -XX:PrintSafepointStatisticsCount=1。这样JVM会在标准输出中打印所有vmop的时间消耗。注意这个时间仅仅是在VMThread中运行的时间,对于像VM_RedefineClasses这种前置有大量时间消耗的,都不会计入其中的vmop时间消耗。

发表评论

电子邮件地址不会被公开。 必填项已用*标注

This site uses Akismet to reduce spam. Learn how your comment data is processed.