Hi,大家好,我是编程小6,很荣幸遇见你,我把这些年在开发过程中遇到的问题或想法写出来,今天说一说fastjson2为什么这么快,希望能够帮助你!!!。
本文作者从以下三个方面讲述了fastjson2 使用了哪些核心技术来提升速度。
1、用「Lambda 生成函数映射」代替「高频的反射操作」
2、对 String 做零拷贝优化
3、常见类型解析优化
作者 | 严彬源(泰文)
来源 | 阿里开发者公众号
fastjson 是很多企业应用中处理 json 数据的基础工具,其凭借在易用性、处理速度上的优越性,支撑了很多数据处理场景。fastjson 的作者「高铁」已更新推出 2.0 版本的 fastjson,即 fastjosn2[1]。
据 “相关数据” 显示,fastjson2 各方面性能均有提升,常规数据序列化相比 1.0 系列提升达到 30%,那么,fastjson2 使用了哪些核心技术来提升速度的呢?笔者总结包含但不限于以下几个方面:
我们来看一段最简单的反射执行代码:
public class Bean {
int id;
public int getId() {
return id;
}
}
Method methodGetId = Bean.class.getMethod("getId");
Bean bean = createInstance();
int value = (Integer) methodGetId.invoke(bean);
上面的反射执行代码可以被改写成这样:
// 将getId()映射为function函数
java.util.function.ToIntFunction<Bean> function = Bean::getId;
int i = function.applyAsInt(bean);
fastjson2 中的具体实现的要复杂一点,但本质上跟上面一样,其本质也是生成了一个 function
//function
java.util.function.ToIntFunction<Bean> function = LambdaMetafactory.metafactory(
lookup,
"applyAsInt",
methodHanlder,
methodType(ToIntFunction.class),
lookup.findVirtual(int.class, "getId", methodType(int.class)),
methodType(int.class)
);
int i = function.applyAsInt(bean);
我们使用反射获取到的 Method 和 Lambda 函数分别执行 10000 次来看下处理速度差异:
Method invoke elapsed: 25ms
Bean::getId elapsed: 1ms
处理速度相差居然达到 25 倍,使用 Java8 Lambda 为什么能提升这多呢?
答案就是:Lambda 利用 LambdaMetafactory 生成了函数映射代替反射。
下面我们详细分析下 Java反射 与 Lambda 函数映射 的底层区别。
注:以下只是想表达出反射调用本身的繁杂性,大可不必深究这些代码细节
从代码角度,我们从 Java 方法反射 Method.invoke 的源码入口来深入:
public Object invoke(Object obj, Object... args) throws IllegalAccessException, IllegalArgumentException,InvocationTargetException{
if (!override) {
if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
Class<?> caller = Reflection.getCallerClass();
checkAccess(caller, clazz, obj, modifiers);
}
}
MethodAccessor ma = methodAccessor;// read volatile
if (ma == null) ma = acquireMethodAccessor();
return ma.invoke(obj, args);
}
可见,经过简单的检查后,调用的是MethodAccessor.invoke(),这部分的实际实现:
public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
this.parent.setDelegate(var3);
}
return invoke0(this.method, var1, var2);
}
private static native Object invoke0(Method var0, Object var1, Object[] var2);
可见,最终调用的是 native 本地方法(本地方法栈)的 invoke0(),这部分的实现:
public Object invoke(Object var1, Object[] var2) throws IllegalArgumentException, InvocationTargetException {
if (++this.numInvocations > ReflectionFactory.inflationThreshold() && !ReflectUtil.isVMAnonymousClass(this.method.getDeclaringClass())) {
MethodAccessorImpl var3 = (MethodAccessorImpl)(new MethodAccessorGenerator()).generateMethod(this.method.getDeclaringClass(), this.method.getName(), this.method.getParameterTypes(), this.method.getReturnType(), this.method.getExceptionTypes(), this.method.getModifiers());
this.parent.setDelegate(var3);
}
return invoke0(this.method, var1, var2);
}
private static native Object invoke0(Method var0, Object var1, Object[] var2);
可见,调用的是 jvm.h 模块的 JVM_InvokeMethod 方法,这部分的实现:
JVM_ENTRY(jobject, JVM_InvokeMethod(JNIEnv *env, jobject method, jobject obj, jobjectArray args0))
JVMWrapper("JVM_InvokeMethod");
Handle method_handle;
if (thread->stack_available((address) &method_handle) >= JVMInvokeMethodSlack) {
method_handle = Handle(THREAD, JNIHandles::resolve(method));
Handle receiver(THREAD, JNIHandles::resolve(obj));
objArrayHandle args(THREAD, objArrayOop(JNIHandles::resolve(args0)));
oop result = Reflection::invoke_method(method_handle(), receiver, args, CHECK_NULL);
jobject res = JNIHandles::make_local(env, result);
if (JvmtiExport::should_post_vm_object_alloc()) {
oop ret_type = java_lang_reflect_Method::return_type(method_handle());
assert(ret_type != NULL, "sanity check: ret_type oop must not be NULL!");
if (java_lang_Class::is_primitive(ret_type)) {
// Only for primitive type vm allocates memory for java object.
// See box() method.
JvmtiExport::post_vm_object_alloc(JavaThread::current(), result);
}
}
return res;
} else {
THROW_0(vmSymbols::java_lang_StackOverflowError());
}
JVM_END
更详细的细节:https://www.zhihu.com/question/464985077/answer/1940021614
具体来讲,Bean::getId 这种 Lambda 写法进过编译后,会通过 java.lang.invoke.LambdaMetafactory 调用到java.lang.invoke.InnerClassLambdaMetafactory#spinInnerClass,最终实现是调用 JDK 自带的字节码库 jdk.internal.org.objectweb.asm 动态生成一个内部类,上层 call 内部类的方法执行调用。
所以 Lambda 生成函数映射的方式,核心消耗就在于生成函数映射,那生成函数映射的效率究竟如何呢?
我们和反射获取 Method 做个对比,Benchmark 结论:
|
Benchmark |
Mode |
Cnt |
Score |
Error |
Units |
|
genMethod(反射获取方法) |
avgt(平均耗时) |
5 |
0.125 |
0.015 |
us/op |
|
genLambda(生成方法的函数映射) |
avgt |
5 |
51.880 |
40.040 |
us/op |
从数据来看,生成函数映射的耗时远高于反射获取 Method。那为我们不禁要问,既然生成函数映射的性能远低于反射获取方法,那为什么最终用生成函数的方式的执行速度比反射要快?
答案就在于——函数复用,将一个固定签名的函数缓存起来,下次调用就可以省去函数创建的过程。
比如 fastjson2 直接将常用函数的初始化缓存放在 static 代码块,这就将函数创建的消耗就被前置到类加载阶段,在数据处理阶段的耗时进一步降低。
从原理上来说,反射方式,在获取 Method 阶段消耗较少,但 invoke 阶段则是每次都用都调用本地方法执行,先是在 jvm 层面多出一些检查,而后转到 JNI 本地库,除了有额外的 jvm 堆栈与本地方法栈的 context 交换 ,还多出一系列 C 体系额外操作,在性能上自然是不如 Lambda 函数映射;
Lambda 生成函数映射的方式,在生成代理类的过程中有部分开销,这部分开销可以通过缓存机制大量减少,而后的调用则全部属于 Java 范畴内的堆栈调用(即拿到代理类后,调用效率和原生方法调用几乎一致)。
零拷贝是老生常谈的问题,Kafka 还是 Netty 等都用到了零拷贝的知识,这里简单介绍一下其概念以便生疏的读者理解上流畅。
零拷贝:是指计算机执行IO操作时,CPU不需要将数据从一个存储区域复制到另一个存储区域,进而减少上下文切换以及CPU的拷贝时间。它是一种IO操作优化技术。
JDK8 中的 String 是如何拷贝的?
为了实现字符串是不可变的特性,JDK 在构造 String 构造字符串的时候,会有拷贝的过程,比如上图是 JDK8 的 String 的一个构造函数的实现,其在堆内存中重新开辟了一块内存区域。
如果要提升构造字符串的开销,就要避免这样的拷贝,即零拷贝。
在 JDK8 中,String 有一个构造函数是不做拷贝的:
但这个方法不是 public,不能直接访问到,可以反射执行,也可以使用 LambdaMetafactory 创建函数映射来调用,前面有介绍这个技巧。
生成的函数映射可以缓存起来复用,而这个构造方法的签名是固定不变的,这意味着,只需要生成一次,后续所有需要初始化 String 的时候都可以复用。
将 LocalDate 格式化为 “yyyy-MM-dd” 的 String 源码(注:针对 JDK8 的实现,此处对源码精简整理以方便阅读):
static BiFunction<char[], Boolean, String> STRING_CREATOR_JDK8;
static {
//为上述String的0拷贝构造方法创建一个映射函数
CallSite callSite = LambdaMetafactory.metafactory(caller, "apply", methodType(BiFunction.class), methodType(Object.class, Object.class, Object.class), handle, methodType(String.class, char[].class, boolean.class));
STRING_CREATOR_JDK8 = (BiFunction<char[], Boolean, String>) callSite.getTarget().invokeExact();
}
static String formatYYYYMMDD(LocalDate date) {
int year = date.getYear();
int month = date.getMonthValue();
int dayOfMonth = date.getDayOfMonth();
int y0 = year / 1000 + '0';
int y1 = (year / 100) % 10 + '0';
int y2 = (year / 10) % 10 + '0';
int y3 = year % 10 + '0';
int m0 = month / 10 + '0';
int m1 = month % 10 + '0';
int d0 = dayOfMonth / 10 + '0';
int d1 = dayOfMonth % 10 + '0';
//char array
char[] chars = new char[10];
chars[0] = (char) y1;
chars[1] = (char) y2;
chars[2] = (char) y3;
chars[3] = (char) y4;
chars[4] = '-';
chars[5] = (char) m0;
chars[6] = (char) m1;
chars[7] = '-';
chars[8] = (char) d0;
chars[9] = (char) d1;
//执行「lambda函数映射」构造String
String str = STRING_CREATOR_JDK8.apply(chars, Boolean.TRUE);
return str;
}
在 JDK8 的实现中,先拼接好格式中每一个 char 字符,然后通过零拷贝的方式构造字符串对象,这样就实现了快速格式化 LocalDate 到 String,这样的实现远比使用 SimpleDateFormat 之类要快。这种实例化 String 的方式在fatsjson2 中的 JSONReader、JSONWritter 随处可见。
点击查看原文,获取更多福利!
https://developer.aliyun.com/article/1165414?utm_content=g_1000368764
版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。
今天的分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。
上一篇
已是最后文章
下一篇
已是最新文章