fastjson2为什么这么快

(20) 2024-01-01 08:12

Hi,大家好,我是编程小6,很荣幸遇见你,我把这些年在开发过程中遇到的问题或想法写出来,今天说一说fastjson2为什么这么快,希望能够帮助你!!!。

本文作者从以下三个方面讲述了fastjson2 使用了哪些核心技术来提升速度。

1、用「Lambda 生成函数映射」代替「高频的反射操作」

2、对 String 做零拷贝优化

3、常见类型解析优化

作者 | 严彬源(泰文)

来源 | 阿里开发者公众号

fastjson 是很多企业应用中处理 json 数据的基础工具,其凭借在易用性、处理速度上的优越性,支撑了很多数据处理场景。fastjson 的作者「高铁」已更新推出 2.0 版本的 fastjson,即 fastjosn2[1]。

fastjson2为什么这么快_https://bianchenghao6.com/blog__第1张

据 “相关数据” 显示,fastjson2 各方面性能均有提升,常规数据序列化相比 1.0 系列提升达到 30%,那么,fastjson2 使用了哪些核心技术来提升速度的呢?笔者总结包含但不限于以下几个方面:

  • 用「Lambda 生成函数映射」代替「高频的反射操作」
  • 对 String 做零拷贝优化
  • 常见类型解析优化

1、用「 Lambda 生成函数映射」代替「高频的反射操作」

我们来看一段最简单的反射执行代码:

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 函数映射 的底层区别。

A、反射执行的底层原理

注:以下只是想表达出反射调用本身的繁杂性,大可不必深究这些代码细节

从代码角度,我们从 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

B、Lambda生成函数映射的底层原理

具体来讲,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 代码块,这就将函数创建的消耗就被前置到类加载阶段,在数据处理阶段的耗时进一步降低。

C、对比分析 & 结论

从原理上来说,反射方式,在获取 Method 阶段消耗较少,但 invoke 阶段则是每次都用都调用本地方法执行,先是在 jvm 层面多出一些检查,而后转到 JNI 本地库,除了有额外的 jvm 堆栈与本地方法栈的 context 交换 ,还多出一系列 C 体系额外操作,在性能上自然是不如 Lambda 函数映射;

Lambda 生成函数映射的方式,在生成代理类的过程中有部分开销,这部分开销可以通过缓存机制大量减少,而后的调用则全部属于 Java 范畴内的堆栈调用(即拿到代理类后,调用效率和原生方法调用几乎一致)。

2、对 String 做零拷贝优化

A、何为零拷贝

零拷贝是老生常谈的问题,Kafka 还是 Netty 等都用到了零拷贝的知识,这里简单介绍一下其概念以便生疏的读者理解上流畅。

零拷贝:是指计算机执行IO操作时,CPU不需要将数据从一个存储区域复制到另一个存储区域,进而减少上下文切换以及CPU的拷贝时间。它是一种IO操作优化技术。

JDK8 中的 String 是如何拷贝的?

fastjson2为什么这么快_https://bianchenghao6.com/blog__第2张

为了实现字符串是不可变的特性,JDK 在构造 String 构造字符串的时候,会有拷贝的过程,比如上图是 JDK8 的 String 的一个构造函数的实现,其在堆内存中重新开辟了一块内存区域。

如果要提升构造字符串的开销,就要避免这样的拷贝,即零拷贝。

B、fastjson2 中如何实现 0 拷贝

在 JDK8 中,String 有一个构造函数是不做拷贝的:

fastjson2为什么这么快_https://bianchenghao6.com/blog__第3张

但这个方法不是 public,不能直接访问到,可以反射执行,也可以使用 LambdaMetafactory 创建函数映射来调用,前面有介绍这个技巧。

生成的函数映射可以缓存起来复用,而这个构造方法的签名是固定不变的,这意味着,只需要生成一次,后续所有需要初始化 String 的时候都可以复用。

C、fastjson2 中的应用

将 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

版权声明:本文内容由阿里云实名注册用户自发贡献,版权归原作者所有,阿里云开发者社区不拥有其著作权,亦不承担相应法律责任。具体规则请查看《阿里云开发者社区用户服务协议》和《阿里云开发者社区知识产权保护指引》。如果您发现本社区中有涉嫌抄袭的内容,填写侵权投诉表单进行举报,一经查实,本社区将立刻删除涉嫌侵权内容。

今天的分享到此就结束了,感谢您的阅读,如果确实帮到您,您可以动动手指转发给其他人。

上一篇

已是最后文章

下一篇

已是最新文章

发表回复