JvmSandbox原理分析06-ThreadLocal+Unsafe解决高并发场景下对象频繁创建和销毁问题

400 阅读6分钟

由于作者只是储备有限,以下内容为作者思考所得,如有错误,可在评论区指出哈。

最近阅读Jvm-Sandbox代码发现了一个小tricks,跟大家一起分享下。这个tricks我用一句话总结下就是:利用ThreadLocal+Unsafe解决高并发场景下对象频繁创建和销毁问题。

1、背景

首先介绍下背景,Jvm-Sandbox是一款字节码增强平台,开发人员能够在该平台上自由创作,开发各种模块来实现不同的字节码增强功能,进而实现各自的业务需求。例如,我们完全可以在Jvm-Sandbox上开发出类似Arthas各种命令工具。

Jvm-Sandbox在启动时会加载一系列系统模块和用户模块,这些模块在加载时可以指定增强的类和方法,以及针对该类和方法增强的处理逻辑,这些处理逻辑在Jvm-Sandbox中叫做EventListener,它就类似于一种callback handler,每当增强方法中的事件被触发时,将会通过增强的字节码将触发的事件传播出来,交给对应的EventListener进行处理。传播的事件可能包含方法签名、方法入参和方法返回值,即存在基本变量,又存在引用变量,因此Jvm-Sandbox在事件传播前需要对事件进行一次深拷贝,防止后续的EventListener事件处理污染了原方法中的引用变量。

image-20220608210947640.png

2、问题

背景介绍完了,我们再来说下这种背景下可能会产生什么问题。Jvm-Sandbox的增强维度为方法维度,意味着每个指定的方法中都会插入一段增强的字节码。在并发场景下,假设每个方法调用的QPS为n,一共增强了m个方法,那么这种假设下的极限场景为n * m同时触发增强逻辑,这样就会存在n * m次深拷贝,如果方法的入参和返回值很大(比如几百k),那么对业务方的JVM来说肯定存在较大的性能损耗。

3、解决方案

那么Jvm-Sandbox是如何解决的呢?这里我先给出结论,然后再针对源码进行详细分析。

我们知道,n的QPS并不意味着JVM中就存在n个线程,我们都会利用线程池的线程复用机制防止大量线程的创建和销毁。于是Jvm-Sandbox在每个线程中添加一个ThreadLocal,该ThreadLocal引用了一个SingleEventFactory对象,该工厂负责每次触发后Event事件的创建,但是这种创建并没有使用传统的序列化和反序列这种深拷贝方式,而是使用Unsafe类进行直接写入内存。这样便能够保证相同线程每次触发增强逻辑时,不会再次创建、销毁Event对象,而是直接写内存。这样即利用了线程池线程复用能力,又减轻了深拷贝的性能损耗。

image-20220608211751398.png

4、源码分析

首先,我们来看下将字节码增强到业务方指定方法中后的代码是啥样子的,下面为增强Apache Http中doExecute方法增强后,反编译的部分代码:

 protected CloseableHttpResponse doExecute(HttpHost target, HttpRequest request, HttpContext context) throws IOException, ClientProtocolException {
         boolean var10000 = true;
         int var16;
         boolean var10001;
         try {
             Object[] var15 = new Object[]{target, request, context};
             Ret var10002 = Spy.spyMethodOnBefore(var15, "default", 1003, 1004, "org.apache.http.impl.client.InternalHttpClient", "doExecute", "(Lorg/apache/http/HttpHost;Lorg/apache/http/HttpRequest;Lorg/apache/http/protocol/HttpContext;)Lorg/apache/http/client/methods/CloseableHttpResponse;", this);
             // ...
         }
 }

可以看到,在进入方法时,会触发Spy#spyMethodOnBefore方法,并将doExecute方法入参、方法签名等参数组装后通过Spy#spyMethodOnBefore方法传播出去。

我们接下来看下Jvm-Sandbox中Spy#spyMethodOnBefore的增强逻辑:

 // Spy.java
 public static Ret spyMethodOnBefore(final Object[] argumentArray, final String namespace, final int listenerId,
                                     final int targetClassLoaderObjectID, final String javaClassName,
                                     final String javaMethodName, final String javaMethodDesc, final Object target) throws Throwable {
     // ...
     try {
         // [1] 委托给spyHandler进行处理
         final SpyHandler spyHandler = namespaceSpyHandlerMap.get(namespace);
         return spyHandler.handleOnBefore(listenerId, targetClassLoaderObjectID, argumentArray,
                 javaClassName, javaMethodName, javaMethodDesc, target);
     } catch (Throwable cause) {// ...} finally {// ...}
 }
  • [1] 代码省略了部分无关逻辑,方法首先会从namespaceSpyHandlerMap映射中获取spyHandler,由于我们只使用了一个default的namespace,所以这里的spyHandler只有一个,且实现类为EventListenerHandler,增强逻辑就委托给了EventListenerHandler#handleOnBefore方法处理。

EventListenerHandler#handleOnBefore源码如下所示:

// EventListenerHandler.java
 @Override
 public Spy.Ret handleOnBefore(int listenerId, int targetClassLoaderObjectID, Object[] argumentArray, String javaClassName, String javaMethodName, String javaMethodDesc, Object target) throws Throwable {
     // ...
     // [1] 获取事件处理器
     final EventProcessor processor = mappingOfEventProcessor.get(listenerId);
     // [2] 获取处理单元
     final EventProcessor.Process process = processor.processRef.get();
     // [3] 通过处理单元中的EventFactory构造具体时间
     final BeforeEvent event = process.getEventFactory().makeBeforeEvent(processId, invokeId, javaClassLoader, javaClassName,
             javaMethodName, javaMethodDesc, target, argumentArray);
     try {
         // [4] 分发事件
         return handleEvent(listenerId, processId, invokeId, event, processor);
     } finally {
         process.getEventFactory().returnEvent(event);
     }
 }
  • [1] 这里的listenerId与module中的EventListener一一对应,这也说明,每个被增强的方法都有唯一对应的EventListener以及唯一对应的EventProcessor。因此,假设我们增强了10个方法,将会存在10个EventProcessor。

  • [2] 这里的processRef为EventProcessor中的ThreadLocal对象,该ThreadLocal引用一个Process处理单元对象,Process是EventProcessor的内部类,其持有EventFactory工厂,用于构造具体的事件对象。他们的源码如下所示:

    •  // EventProcessor.java
       class EventProcessor {
           // ...
           final ThreadLocal<Process> processRef = new ThreadLocal<Process>() {
               @Override
               protected Process initialValue() {
                   return new Process();
               }
           };
           class Process {
               // ...
               private final SingleEventFactory eventFactory = new SingleEventFactory();
           }
       }
      
  • [3] 这一步就是构造事件的具体方法了,可以看到构造的逻辑在EventFactory中,makeBeforeEvent代码如下所示。该工厂会在类加载时通过反射获取Unsafe类,并计算各个字段的偏移量,并在每次构造Event时,利用Unsafe对象直接写入内存,避免了深拷贝的内存损耗。

    •  // SingleEventFactory.java
       class SingleEventFactory {
           static {
               try {
                   unsafe = UnsafeUtils.getUnsafe();
                   processIdFieldInInvokeEventOffset = unsafe.objectFieldOffset(InvokeEvent.class.getDeclaredField("processId"));
                   invokeIdFieldInInvokeEventOffset = unsafe.objectFieldOffset(InvokeEvent.class.getDeclaredField("invokeId"));
                   javaClassLoaderFieldInBeforeEventOffset = unsafe.objectFieldOffset(BeforeEvent.class.getDeclaredField("javaClassLoader"));
                   javaClassNameFieldInBeforeEventOffset = unsafe.objectFieldOffset(BeforeEvent.class.getDeclaredField("javaClassName"));
                   javaMethodNameFieldInBeforeEventOffset = unsafe.objectFieldOffset(BeforeEvent.class.getDeclaredField("javaMethodName"));
                   javaMethodDescFieldInBeforeEventOffset = unsafe.objectFieldOffset(BeforeEvent.class.getDeclaredField("javaMethodDesc"));
                   targetFieldInBeforeEventOffset = unsafe.objectFieldOffset(BeforeEvent.class.getDeclaredField("target"));
                   argumentArrayFieldInBeforeEventOffset = unsafe.objectFieldOffset(BeforeEvent.class.getDeclaredField("argumentArray"));
                   objectFieldInReturnEventOffset = unsafe.objectFieldOffset(ReturnEvent.class.getDeclaredField("object"));
                   throwableFieldInThrowsEventOffset = unsafe.objectFieldOffset(ThrowsEvent.class.getDeclaredField("throwable"));
                   lineNumberFieldInLineEventOffset = unsafe.objectFieldOffset(LineEvent.class.getDeclaredField("lineNumber"));
       ​
                   lineNumberFieldInCallBeforeEventOffset = unsafe.objectFieldOffset(CallBeforeEvent.class.getDeclaredField("lineNumber"));
                   ownerFieldInCallBeforeEventOffset = unsafe.objectFieldOffset(CallBeforeEvent.class.getDeclaredField("owner"));
                   nameFieldInCallBeforeEventOffset = unsafe.objectFieldOffset(CallBeforeEvent.class.getDeclaredField("name"));
                   descFieldInCallBeforeEventOffset = unsafe.objectFieldOffset(CallBeforeEvent.class.getDeclaredField("desc"));
                   throwExceptionFieldInCallThrowsEventOffset = unsafe.objectFieldOffset(CallThrowsEvent.class.getDeclaredField("throwException"));
               } catch (Exception e) {
                   throw new Error(e);
               }
           }
           
           public BeforeEvent makeBeforeEvent(final int processId, final int invokeId, final ClassLoader javaClassLoader, final String javaClassName,
                                              final String javaMethodName, final String javaMethodDesc, final Object target, final Object[] argumentArray) {
               if (null == beforeEvent) {
                   beforeEvent = new BeforeEvent(ILLEGAL_PROCESS_ID, ILLEGAL_INVOKE_ID, null, null, null, null, null, null);
               }
               unsafe.putInt(beforeEvent, processIdFieldInInvokeEventOffset, processId);
               unsafe.putInt(beforeEvent, invokeIdFieldInInvokeEventOffset, invokeId);
               unsafe.putObject(beforeEvent, javaClassLoaderFieldInBeforeEventOffset, javaClassLoader);
               unsafe.putObject(beforeEvent, javaClassNameFieldInBeforeEventOffset, javaClassName);
               unsafe.putObject(beforeEvent, javaMethodNameFieldInBeforeEventOffset, javaMethodName);
               unsafe.putObject(beforeEvent, javaMethodDescFieldInBeforeEventOffset, javaMethodDesc);
               unsafe.putObject(beforeEvent, targetFieldInBeforeEventOffset, target);
               unsafe.putObject(beforeEvent, argumentArrayFieldInBeforeEventOffset, argumentArray);
               return beforeEvent;
           }
       }
      
  • [4] 将构造后的Event对象分发给module对应的EventListener。

5、总结

本篇文章介绍了Jvm-Sandbox是如何通过ThreadLocl+Unsafe类来解决并发场景下对象频繁创建和删除的方法。使用ThreadLocal创建对象,能够在线程被线程池回收时仍然持有对象,避免线程被销毁,在下一次需要创建对象时,使用Unsafe对原对象直接写内存,也能够防止对象二次创建。