由于作者只是储备有限,以下内容为作者思考所得,如有错误,可在评论区指出哈。
最近阅读Jvm-Sandbox代码发现了一个小tricks,跟大家一起分享下。这个tricks我用一句话总结下就是:利用ThreadLocal+Unsafe解决高并发场景下对象频繁创建和销毁问题。
1、背景
首先介绍下背景,Jvm-Sandbox是一款字节码增强平台,开发人员能够在该平台上自由创作,开发各种模块来实现不同的字节码增强功能,进而实现各自的业务需求。例如,我们完全可以在Jvm-Sandbox上开发出类似Arthas各种命令工具。
Jvm-Sandbox在启动时会加载一系列系统模块和用户模块,这些模块在加载时可以指定增强的类和方法,以及针对该类和方法增强的处理逻辑,这些处理逻辑在Jvm-Sandbox中叫做EventListener,它就类似于一种callback handler,每当增强方法中的事件被触发时,将会通过增强的字节码将触发的事件传播出来,交给对应的EventListener进行处理。传播的事件可能包含方法签名、方法入参和方法返回值,即存在基本变量,又存在引用变量,因此Jvm-Sandbox在事件传播前需要对事件进行一次深拷贝,防止后续的EventListener事件处理污染了原方法中的引用变量。
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对象,而是直接写内存。这样即利用了线程池线程复用能力,又减轻了深拷贝的性能损耗。
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对原对象直接写内存,也能够防止对象二次创建。