JVM 源码分析之 javaagent 原理完全解读
转载:https://infoq.cn/article/javaagent-illustrated
本文重点讲述 javaagent 的具体实现,因为它面向的是我们 Java 程序员,而且 agent 都是用 Java 编写的,不需要太多的 C/C++ 编程基础,不过这篇文章里也会讲到 JVMTIAgent(C 实现的),因为 javaagent 的运行还是依赖于一个特殊的 JVMTIAgent。
对于 javaagent,或许大家都听过,甚至使用过,常见的用法大致如下:
java -javaagent:myagent.jar=mode=test Test
我们通过 -javaagent 来指定我们编写的 agent 的 jar 路径(./myagent.jar),以及要传给 agent 的参数(mode=test),在启动的时候这个 agent 就可以做一些我们希望的事了。
javaagent 的主要功能如下:
- 可以在加载 class 文件之前做拦截,对字节码做修改
 - 可以在运行期对已加载类的字节码做变更,但是这种情况下会有很多的限制,后面会详细说
 - 还有其他一些小众的功能
- 获取所有已经加载过的类
 - 获取所有已经初始化过的类(执行过 clinit 方法,是上面的一个子集)
 - 获取某个对象的大小
 - 将某个 jar 加入到 bootstrap classpath 里作为高优先级被 bootstrapClassloader 加载
 - 将某个 jar 加入到 classpath 里供 AppClassloard 去加载
 - 设置某些 native 方法的前缀,主要在查找 native 方法的时候做规则匹配
 
 
想象一下可以让程序按照我们预期的逻辑去执行,听起来是不是挺酷的。
JVMTI全称 JVM Tool Interface,是 JVM 暴露出来的一些供用户扩展的接口集合。JVMTI 是基于事件驱动的,JVM 每执行到一定的逻辑就会调用一些事件的回调接口(如果有的话),这些接口可以供开发者扩展自己的逻辑。
比如最常见的,我们想在某个类的字节码文件读取之后、类定义之前修改相关的字节码,从而使创建的 class 对象是我们修改之后的字节码内容,那就可以实现一个回调函数赋给 jvmtiEnv(JVMTI 的运行时,通常一个 JVMTIAgent 对应一个 jvmtiEnv,但是也可以对应多个)的回调方法集合里的 ClassFileLoadHook,这样在接下来的类文件加载过程中都会调用到这个函数中,大致实现如下:,
    jvmtiEventCallbacks callbacks;
    jvmtiEnv *          jvmtienv = jvmti(agent);
    jvmtiError          jvmtierror;
    memset(&callbacks, 0, sizeof(callbacks));
    callbacks.ClassFileLoadHook = &eventHandlerClassFileLoadHook;
    jvmtierror = (*jvmtienv)->SetEventCallbacks( jvmtienv,
                                                 &callbacks,
                                                 sizeof(callbacks));
JVMTIAgent 其实就是一个动态库,利用 JVMTI 暴露出来的一些接口来干一些我们想做、但是正常情况下又做不到的事情,不过为了和普通的动态库进行区分,它一般会实现如下的一个或者多个函数:
JNIEXPORT jint JNICALL
Agent_OnLoad(JavaVM *vm, char *options, void *reserved);
JNIEXPORT jint JNICALL
Agent_OnAttach(JavaVM* vm, char* options, void* reserved);
JNIEXPORT void JNICALL
Agent_OnUnload(JavaVM *vm);
- Agent_OnLoad函数,如果 agent 是在启动时加载的,也就是在 vm 参数里通过 -agentlib 来指定的,那在启动过程中就会去执行这个 agent 里的Agent_OnLoad函数。
 - Agent_OnAttach函数,如果 agent 不是在启动时加载的,而是我们先 attach 到目标进程上,然后给对应的目标进程发送 load 命令来加载,则在加载过程中会调用Agent_OnAttach函数。
 - Agent_OnUnload函数,在 agent 卸载时调用,不过貌似基本上很少实现它。
 
其实我们每天都在和 JVMTIAgent 打交道,只是你可能没有意识到而已,比如我们经常使用 Eclipse 等工具调试 Java 代码,其实就是利用 JRE 自带的 jdwp agent 实现的,只是 Eclipse 等工具在没让你察觉的情况下将相关参数 (类似-agentlib:jdwp=transport=dt_socket,suspend=y,address=localhost:61349) 自动加到程序启动参数列表里了,其中 agentlib 参数就用来跟要加载的 agent 的名字,比如这里的 jdwp(不过这不是动态库的名字,JVM 会做一些名称上的扩展,比如在 Linux 下会去找libjdwp.so的动态库进行加载,也就是在名字的基础上加前缀lib,再加后缀.so),接下来会跟一堆相关的参数,将这些参数传给Agent_OnLoad或者Agent_OnAttach函数里对应的options。
说到 javaagent,必须要讲的是一个叫做 instrument 的 JVMTIAgent(Linux 下对应的动态库是 libinstrument.so),因为 javaagent 功能就是它来实现的,另外 instrument agent 还有个别名叫 JPLISAgent(Java Programming Language Instrumentation Services Agent),这个名字也完全体现了其最本质的功能:就是专门为 Java 语言编写的插桩服务提供支持的。
instrument agent
instrument agent 实现了Agent_OnLoad和Agent_OnAttach两方法,也就是说在使用时,agent既可以在启动时加载,也可以在运行时动态加载。其中启动时加载还可以通过类似-javaagent:myagent.jar的方式来间接加载 instrument agent,运行时动态加载依赖的是 JVM 的 attach 机制(JVM Attach 机制实现),通过发送 load 命令来加载 agent。
instrument agent 的核心数据结构如下:
struct _JPLISAgent {
    JavaVM *                mJVM;                   /* handle to the JVM */
    JPLISEnvironment        mNormalEnvironment;     /* for every thing but retransform stuff */
    JPLISEnvironment        mRetransformEnvironment;/* for retransform stuff only */
    jobject                 mInstrumentationImpl;   /* handle to the Instrumentation instance */
    jmethodID               mPremainCaller;         /* method on the InstrumentationImpl that does the premain stuff (cached to save lots of lookups) */
    jmethodID               mAgentmainCaller;       /* method on the InstrumentationImpl for agents loaded via attach mechanism */
    jmethodID               mTransform;             /* method on the InstrumentationImpl that does the class file transform */
    jboolean                mRedefineAvailable;     /* cached answer to "does this agent support redefine" */
    jboolean                mRedefineAdded;         /* indicates if can_redefine_classes capability has been added */
    jboolean                mNativeMethodPrefixAvailable; /* cached answer to "does this agent support prefixing" */
    jboolean                mNativeMethodPrefixAdded;     /* indicates if can_set_native_method_prefix capability has been added */
    char const *            mAgentClassName;        /* agent class name */
    char const *            mOptionsString;         /* -javaagent options string */
};
struct _JPLISEnvironment {
    jvmtiEnv *              mJVMTIEnv;              /* the JVM TI environment */
    JPLISAgent *            mAgent;                 /* corresponding agent */
    jboolean                mIsRetransformer;       /* indicates if special environment */
};
这里解释一下几个重要项:
- mNormalEnvironment:主要提供正常的类 transform 及 redefine 功能。
 - mRetransformEnvironment:主要提供类 retransform 功能。
 - mInstrumentationImpl:这个对象非常重要,也是我们 Java agent 和 JVM 进行交互的入口,或许写过 javaagent 的人在写`premain`以及`agentmain`方法的时候注意到了有个 Instrumentation 参数,该参数其实就是这里的对象。
 - mPremainCaller:指向`sun.instrument.InstrumentationImpl.loadClassAndCallPremain`方法,如果 agent 是在启动时加载的,则该方法会被调用。
 - mAgentmainCaller:指向`sun.instrument.InstrumentationImpl.loadClassAndCallAgentmain`方法,该方法在通过 attach 的方式动态加载 agent 的时候调用。
 - mTransform:指向`sun.instrument.InstrumentationImpl.transform`方法。
 - mAgentClassName:在我们 javaagent 的 MANIFEST.MF 里指定的`Agent-Class`。
 - mOptionsString:传给 agent 的一些参数。
 - mRedefineAvailable:是否开启了 redefine 功能,在 javaagent 的 MANIFEST.MF 里设置`Can-Redefine-Classes:true`。
 - mNativeMethodPrefixAvailable:是否支持 native 方法前缀设置,同样在 javaagent 的 MANIFEST.MF 里设置`Can-Set-Native-Method-Prefix:true`。
 - mIsRetransformer:如果在 javaagent 的 MANIFEST.MF 文件里定义了`Can-Retransform-Classes:true`,将会设置 mRetransformEnvironment 的 mIsRetransformer 为 true。
 
在启动时加载 instrument agent
正如前面“概述”里提到的方式,就是启动时加载 instrument agent,具体过程都在`InvocationAdapter.c`的`Agent_OnLoad`方法里,这里简单描述下过程:
- 创建并初始化 JPLISAgent
 - 监听 VMInit 事件,在 vm 初始化完成之后做下面的事情:
- 创建 InstrumentationImpl 对象
 - 监听 ClassFileLoadHook 事件
 - 调用 InstrumentationImpl 的`loadClassAndCallPremain`方法,在这个方法里会调用 javaagent 里 MANIFEST.MF 里指定的`Premain-Class`类的 premain 方法
 
 - 解析 javaagent 里 MANIFEST.MF 里的参数,并根据这些参数来设置 JPLISAgent 里的一些内容
 
在运行时加载 instrument agent
在运行时加载的方式,大致按照下面的方式来操作:
VirtualMachine vm = VirtualMachine.attach(pid);
vm.loadAgent(agentPath, agentArgs);
上面会通过 JVM 的 attach 机制来请求目标 JVM 加载对应的 agent,过程大致如下:
- 创建并初始化 JPLISAgent
 - 解析 javaagent 里 MANIFEST.MF 里的参数
 - 创建 InstrumentationImpl 对象
 - 监听 ClassFileLoadHook 事件
 - 调用 InstrumentationImpl 的loadClassAndCallAgentmain方法,在这个方法里会调用 javaagent 里 MANIFEST.MF 里指定的Agent-Class类的agentmain方法
 
instrument agent 的 ClassFileLoadHook 回调实现
不管是启动时还是运行时加载的 instrument agent,都关注着同一个 jvmti 事件——ClassFileLoadHook,这个事件是在读取字节码文件之后回调时用的,这样可以对原来的字节码做修改,那这里面究竟是怎样实现的呢?
void JNICALL
eventHandlerClassFileLoadHook(  jvmtiEnv *              jvmtienv,
                                JNIEnv *                jnienv,
                                jclass                  class_being_redefined,
                                jobject                 loader,
                                const char*             name,
                                jobject                 protectionDomain,
                                jint                    class_data_len,
                                const unsigned char*    class_data,
                                jint*                   new_class_data_len,
                                unsigned char**         new_class_data) {
    JPLISEnvironment * environment  = NULL;
    environment = getJPLISEnvironment(jvmtienv);
    /* if something is internally inconsistent (no agent), just silently return without touching the buffer */
    if ( environment != NULL ) {
        jthrowable outstandingException = preserveThrowable(jnienv);
        transformClassFile( environment->mAgent,
                            jnienv,
                            loader,
                            name,
                            class_being_redefined,
                            protectionDomain,
                            class_data_len,
                            class_data,
                            new_class_data_len,
                            new_class_data,
                            environment->mIsRetransformer);
        restoreThrowable(jnienv, outstandingException);
    }
}
先根据 jvmtiEnv 取得对应的 JPLISEnvironment,因为上面我已经说到其实有两个 JPLISEnvironment(并且有两个 jvmtiEnv),其中一个是专门做 retransform 的,而另外一个用来做其他事情,根据不同的用途,在注册具体的 ClassFileTransformer 时也是分开的,对于作为 retransform 用的 ClassFileTransformer,我们会注册到一个单独的 TransformerManager 里。
接着调用 transformClassFile 方法,由于函数实现比较长,这里就不贴代码了,大致意思就是调用 InstrumentationImpl 对象的 transform 方法,根据最后那个参数来决定选哪个 TransformerManager 里的 ClassFileTransformer 对象们做 transform 操作。
private byte[]
    transform(  ClassLoader         loader,
                String              classname,
                Class               classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer,
                boolean             isRetransformer) {
        TransformerManager mgr = isRetransformer?
                                        mRetransfomableTransformerManager :
                                        mTransformerManager;
        if (mgr == null) {
            return null; // no manager, no transform
        } else {
            return mgr.transform(   loader,
                                    classname,
                                    classBeingRedefined,
                                    protectionDomain,
                                    classfileBuffer);
        }
    }
  public byte[]
    transform(  ClassLoader         loader,
                String              classname,
                Class               classBeingRedefined,
                ProtectionDomain    protectionDomain,
                byte[]              classfileBuffer) {
        boolean someoneTouchedTheBytecode = false;
        TransformerInfo[]  transformerList = getSnapshotTransformerList();
        byte[]  bufferToUse = classfileBuffer;
        // order matters, gotta run 'em in the order they were added
        for ( int x = 0; x < transformerList.length; x++ ) {
            TransformerInfo         transformerInfo = transformerList[x];
            ClassFileTransformer    transformer = transformerInfo.transformer();
            byte[]                  transformedBytes = null;
            try {
                transformedBytes = transformer.transform(   loader,
                                                            classname,
                                                            classBeingRedefined,
                                                            protectionDomain,
                                                            bufferToUse);
            }
            catch (Throwable t) {
                // don't let any one transformer mess it up for the others.
                // This is where we need to put some logging. What should go here? FIXME
            }
            if ( transformedBytes != null ) {
                someoneTouchedTheBytecode = true;
                bufferToUse = transformedBytes;
            }
        }
        // if someone modified it, return the modified buffer.
        // otherwise return null to mean "no transforms occurred"
        byte [] result;
        if ( someoneTouchedTheBytecode ) {
            result = bufferToUse;
        }
        else {
            result = null;
        }
        return result;
    }   
以上是最终调到的 java 代码,可以看到已经调用到我们自己编写的 javaagent 代码里了,我们一般是实现一个 ClassFileTransformer 类,然后创建一个对象注册到对应的 TransformerManager 里。
这里说的 class transform 其实是狭义的,主要是针对第一次类文件加载时就要求被 transform 的场景,在加载类文件的时候发出 ClassFileLoad 事件,然后交给 instrumenat agent 来调用 javaagent 里注册的 ClassFileTransformer 实现字节码的修改。
类重新定义,这是 Instrumentation 提供的基础功能之一,主要用在已经被加载过的类上,想对其进行修改,要做这件事,我们必须要知道两个东西,一个是要修改哪个类,另外一个是想将那个类修改成怎样的结构,有了这两个信息之后就可以通过 InstrumentationImpl 下面的 redefineClasses 方法操作了:
public void redefineClasses(ClassDefinition[]   definitions) throws  ClassNotFoundException {
        if (!isRedefineClassesSupported()) {
            throw new UnsupportedOperationException("redefineClasses is not supported in this environment");
        }
        if (definitions == null) {
            throw new NullPointerException("null passed as 'definitions' in redefineClasses");
        }
        for (int i = 0; i < definitions.length; ++i) {
            if (definitions[i] == null) {
                throw new NullPointerException("element of 'definitions' is null in redefineClasses");
            }
        }
        if (definitions.length == 0) {
            return; // short-circuit if there are no changes requested
        }
        redefineClasses0(mNativeAgent, definitions);
    }
在 JVM 里对应的实现是创建一个VM_RedefineClasses的VM_Operation,注意执行它的时候会 stop-the-world:
jvmtiError
JvmtiEnv::RedefineClasses(jint class_count, const jvmtiClassDefinition* class_definitions) {
//TODO: add locking
  VM_RedefineClasses op(class_count, class_definitions, jvmti_class_load_kind_redefine);
  VMThread::execute(&op);
  return (op.check_error());
} /* end RedefineClasses */
这个过程我尽量用语言来描述清楚,不详细贴代码了,因为代码量实在有点大:
- 挨个遍历要批量重定义的 jvmtiClassDefinition
 - 然后读取新的字节码,如果有关注 ClassFileLoadHook 事件的,还会走对应的 transform 来对新的字节码再做修改
 - 字节码解析好,创建一个 klassOop 对象
 - 对比新老类,并要求如下:
- 父类是同一个
 - 实现的接口数也要相同,并且是相同的接口
 - 类访问符必须一致
 - 字段数和字段名要一致
 - 新增的方法必须是 private static/final 的
 - 可以删除修改方法
 
 - 对新类做字节码校验
 - 合并新老类的常量池
 - 如果老类上有断点,那都清除掉
 - 对老类做 JIT 去优化
 - 对新老方法匹配的方法的 jmethodId 做更新,将老的 jmethodId 更新到新的 method 上
 - 新类的常量池的 holer 指向老的类
 - 将新类和老类的一些属性做交换,比如常量池,methods,内部类
 - 初始化新的 vtable 和 itable
 - 交换 annotation 的 method、field、paramenter
 - 遍历所有当前类的子类,修改他们的 vtable 及 itable
 
上面是基本的过程,总的来说就是只更新了类里的内容,相当于只更新了指针指向的内容,并没有更新指针,避免了遍历大量已有类对象对它们进行更新所带来的开销。
retransform class 可以简单理解为回滚操作,具体回滚到哪个版本,这个需要看情况而定,下面不管那种情况都有一个前提,那就是 javaagent 已经要求要有 retransform 的能力了:
- 如果类是在第一次加载的的时候就做了 transform,那么做 retransform 的时候会将代码回滚到 transform 之后的代码
 - 如果类是在第一次加载的的时候没有任何变化,那么做 retransform 的时候会将代码回滚到最原始的类文件里的字节码
 - 如果类已经加载了,期间类可能做过多次 redefine(比如被另外一个 agent 做过),但是接下来加载一个新的 agent 要求有 retransform 的能力了,然后对类做 redefine 的动作,那么 retransform 的时候会将代码回滚到上一个 agent 最后一次做 redefine 后的字节码
 
我们从 InstrumentationImpl 的retransformClasses方法参数看猜到应该是做回滚操作,因为我们只指定了 class:
    public void retransformClasses(Class<?>[] classes) {
        if (!isRetransformClassesSupported()) {
            throw new UnsupportedOperationException( "retransformClasses is not supported in this environment");
        }
        retransformClasses0(mNativeAgent, classes);
    }
不过 retransform 的实现其实也是通过 redefine 的功能来实现,在类加载的时候有比较小的差别,主要体现在究竟会走哪些 transform 上,如果当前是做 retransform 的话,那将忽略那些注册到正常的 TransformerManager 里的 ClassFileTransformer,而只会走专门为 retransform 而准备的 TransformerManager 的 ClassFileTransformer,不然想象一下字节码又被无声无息改成某个中间态了。
private:
  void post_all_envs() {
    if (_load_kind != jvmti_class_load_kind_retransform) {
      // for class load and redefine,
      // call the non-retransformable agents
      JvmtiEnvIterator it;
      for (JvmtiEnv* env = it.first(); env != NULL; env = it.next(env)) {
        if (!env->is_retransformable() && env->is_enabled(JVMTI_EVENT_CLASS_FILE_LOAD_HOOK)) {
          // non-retransformable agents cannot retransform back,
          // so no need to cache the original class file bytes
          post_to_env(env, false);
        }
      }
    }
    JvmtiEnvIterator it;
    for (JvmtiEnv* env = it.first(); env != NULL; env = it.next(env)) {
      // retransformable agents get all events
      if (env->is_retransformable() && env->is_enabled(JVMTI_EVENT_CLASS_FILE_LOAD_HOOK)) {
        // retransformable agents need to cache the original class file
        // bytes if changes are made via the ClassFileLoadHook
        post_to_env(env, true);
      }
    }
  }
javaagent 除了做字节码上面的修改之外,其实还有一些小功能,有时候还是挺有用的
- 获取所有已经被加载的类:Class[] getAllLoadedClasses();
 - 获取所有已经初始化了的类: Class[] getInitiatedClasses(ClassLoader loader);
 - 获取某个对象的大小: long getObjectSize(Object objectToSize);
 - 将某个 jar 加入到 bootstrap classpath 里优先其他 jar 被加载: void appendToBootstrapClassLoaderSearch(JarFile jarfile);
 - 将某个 jar 加入到 classpath 里供 appclassloard 去加载:void appendToSystemClassLoaderSearch(JarFile jarfile);
 - 设置某些 native 方法的前缀,主要在找 native 方法的时候做规则匹配: void setNativeMethodPrefix(ClassFileTransformer transformer, String prefix)。
 
李嘉鹏,花名寒泉子,使用“你假笨”的 ID 混迹网络,蚂蚁金服码农一枚。本科毕业四年多,一直待在支付宝,先后从事过监控系统、框架容器以及性能分析系统等研发工作,其中从事框架容器三年多,主要负责开发支付宝的统一编程框架 sofa,2014 年下半年开始重点从事性能分析系统的研发工作并于年底加入 JVM 团队。
感谢臧秀涛对本文的审校。
JVM 源码分析之 javaagent 原理完全解读的更多相关文章
- JVM源码分析之javaagent原理完全解读
		
概述 本文重点讲述javaagent的具体实现,因为它面向的是我们Java程序员,而且agent都是用Java编写的,不需要太多的C/C++编程基础,不过这篇文章里也会讲到JVMTIAgent(C实现 ...
 - JVM源码分析之javaagent原理完全解读--转
		
原文地址:http://www.infoq.com/cn/articles/javaagent-illustrated 概述 本文重点讲述javaagent的具体实现,因为它面向的是我们Java程序员 ...
 - Guava 源码分析(Cache 原理 对象引用、事件回调)
		
前言 在上文「Guava 源码分析(Cache 原理)」中分析了 Guava Cache 的相关原理. 文末提到了回收机制.移除时间通知等内容,许多朋友也挺感兴趣,这次就这两个内容再来分析分析. 在开 ...
 - JVM源码分析之SystemGC完全解读
		
JVM源码分析之SystemGC完全解读 概述 JVM的GC一般情况下是JVM本身根据一定的条件触发的,不过我们还是可以做一些人为的触发,比如通过jvmti做强制GC,通过System.gc触发,还可 ...
 - 深入源码分析SpringMVC底层原理(二)
		
原文链接:深入源码分析SpringMVC底层原理(二) 文章目录 深入分析SpringMVC请求处理过程 1. DispatcherServlet处理请求 1.1 寻找Handler 1.2 没有找到 ...
 - JVM源码分析之一个Java进程究竟能创建多少线程
		
JVM源码分析之一个Java进程究竟能创建多少线程 原创: 寒泉子 你假笨 2016-12-06 概述 虽然这篇文章的标题打着JVM源码分析的旗号,不过本文不仅仅从JVM源码角度来分析,更多的来自于L ...
 - 【转】MaBatis学习---源码分析MyBatis缓存原理
		
[原文]https://www.toutiao.com/i6594029178964673027/ 源码分析MyBatis缓存原理 1.简介 在 Web 应用中,缓存是必不可少的组件.通常我们都会用 ...
 - JVM源码分析之堆外内存完全解读
		
JVM源码分析之堆外内存完全解读 寒泉子 2016-01-15 17:26:16 浏览6837 评论0 阿里技术协会 摘要: 概述 广义的堆外内存 说到堆外内存,那大家肯定想到堆内内存,这也是我们 ...
 - JVM源码分析之Metaspace解密
		
概述 metaspace,顾名思义,元数据空间,专门用来存元数据的,它是jdk8里特有的数据结构用来替代perm,这块空间很有自己的特点,前段时间公司这块的问题太多了,主要是因为升级了中间件所 ...
 
随机推荐
- JAVA Web学习笔记
			
JAVA Web学习笔记 1.JSP (java服务器页面) 锁定 本词条由“科普中国”百科科学词条编写与应用工作项目 审核 . JSP全名为Java Server Pages,中文名叫java服务器 ...
 - mybatis原理与设计模式-日志模块- 适配器模式
			
在讲设计模式之前,得先知道java程序设计中得六大原则,才能更好得理解我们得系统为什么需要设计模式 1 单一职责原则 一个类只负责一种职责,只有这种职责的改变会导致这个类的变更.绕口一点的正统说法:不 ...
 - Mysql create constraint foreign key faild.trouble shooting method share
			
mysql> create table tb_test (id int(10) not null auto_increment primary key,action_id int(10) not ...
 - EOJ 1058. 挤模具 (多边形面积)
			
题目链接:1058. 挤模具 题意 给出模具的底和体积,求模具的高. 思路 模具的底为多边形,因此求出多边形面积,用体积除以底的面积就是答案. 多边形的面积求解见 EOJ 1127. 多边形面积(计算 ...
 - mysql与python连接学习
			
1 问题: pip install MySQLClient 遇到 error: Microsoft Visual C++ 14.0 is required. Get it with "Mi ...
 - Rust <8>:lifetime 高级语法与 trait 关联绑定
			
一.生命周期关联:如下声明表示,'s >= 'c struct Parser<'c, 's: 'c> { context: &'c Context<'s>, } ...
 - Linux(一)—— Linux环境搭建
			
Linux环境搭建 一.虚拟机安装 1.下载地址 https://my.vmware.com/web/vmware/info/slug/desktop_end_user_computing/vmwar ...
 - git 上传你代码到码云
			
转载自:http://blog.csdn.net/u013776188/article/details/60867437
 - axios  interceptors 拦截 , 页面跳转, token 验证 Vue+axios实现登陆拦截,axios封装(报错,鉴权,跳转,拦截,提示)
			
Vue+axios实现登陆拦截,axios封装(报错,鉴权,跳转,拦截,提示) :https://blog.csdn.net/H1069495874/article/details/80057107 ...
 - [Java Performance] 线程及同步的性能之线程池/ThreadPoolExecutors/ForkJoinPool
			
线程池和ThreadPoolExecutors 虽然在程序中可以直接使用Thread类型来进行线程操作,但是更多的情况是使用线程池,尤其是在Java EE应用服务器中,一般会使用若干个线程池来处理 ...