服务粉丝

我们一直在努力
当前位置:首页 > 财经 >

fastjson2为什么这么快?

日期: 来源:阿里开发者收集编辑:严彬源(泰文)
阿里妹导读本文作者从以下三个方面讲述了fastjson2 使用了哪些核心技术来提升速度。1、用「Lambda 生成函数映射」代替「高频的反射操作」2、对 String 做零拷贝优化3、常见类型解析优化fastjson 是很多企业应用中处理 json 数据的基础工具,其凭借在易用性、处理速度上的优越性,支撑了很多数据处理场景。fastjson 的作者「高铁」已更新推出 2.0 版本的 fastjson,即 fastjosn2[1]。据 “相关数据” [2]显示,fastjson2 各方面性能均有提升,常规数据序列化相比 1.0 系列提升达到 30%,那么,fastjson2 使用了哪些核心技术来提升速度的呢?笔者总结包含但不限于以下几个方面:用「Lambda 生成函数映射」代替「高频的反射操作」对 String 做零拷贝优化常见类型解析优化一、用「 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。//functionjava.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: 25msBean::getId elapsed: 1ms处理速度相差居然达到 25 倍,使用 Java8 Lambda 为什么能提升这多呢?答案就是:Lambda 利用 LambdaMetafactory 生成了函数映射代替反射。下面我们详细分析下 Java反射 与 Lambda 函数映射 的底层区别。1、反射执行的底层原理注:以下只是想表达出反射调用本身的繁杂性,大可不必深究这些代码细节从代码角度,我们从 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(),这部分的实现:JNIEXPORT jobject JNICALL Java_sun_reflect_NativeMethodAccessorImpl_invoke0(JNIEnv *env, jclass unused, jobject m, jobject obj, jobjectArray args){ return JVM_InvokeMethod(env, m, obj, args);}可见,调用的是 jvm.h 模块的 JVM_InvokeMethod 方法,这部分的实现:JNIEXPORT jobject JNICALL Java_sun_reflect_NativeMethodAccessorImpl_invoke0(JNIEnv *env, jclass unused, jobject m, jobject obj, jobjectArray args){ return JVM_InvokeMethod(env, m, obj, args);}更详细的细节:https://www.zhihu.com/question/464985077/answer/19400216142、Lambda生成函数映射的底层原理具体来讲,Bean::getId 这种 Lambda 写法进过编译后,会通过 java.lang.invoke.LambdaMetafactory 调用到java.lang.invoke.InnerClassLambdaMetafactory#spinInnerClass最终实现是调用 JDK 自带的字节码库 jdk.internal.org.objectweb.asm 动态生成一个内部类,上层 call 内部类的方法执行调用。所以 Lambda 生成函数映射的方式,核心消耗就在于生成函数映射,那生成函数映射的效率究竟如何呢?我们和反射获取 Method 做个对比,Benchmark 结论:Benchmark ModeCntScoreErrorUnitsgenMethod(反射获取方法)avgt(平均耗时)50.1250.015us/opgenLambda(生成方法的函数映射)avgt551.88040.040us/op从数据来看,生成函数映射的耗时远高于反射获取 Method。那为我们不禁要问,既然生成函数映射的性能远低于反射获取方法,那为什么最终用生成函数的方式的执行速度比反射要快?答案就在于——函数复用,将一个固定签名的函数缓存起来,下次调用就可以省去函数创建的过程。比如 fastjson2 直接将常用函数的初始化缓存放在 static 代码块,这就将函数创建的消耗就被前置到类加载阶段,在数据处理阶段的耗时进一步降低。3、对比分析 & 结论从原理上来说,反射方式,在获取 Method 阶段消耗较少,但 invoke 阶段则是每次都用都调用本地方法执行,先是在 jvm 层面多出一些检查,而后转到 JNI 本地库,除了有额外的 jvm 堆栈与本地方法栈的 context 交换 ,还多出一系列 C 体系额外操作,在性能上自然是不如 Lambda 函数映射;Lambda 生成函数映射的方式,在生成代理类的过程中有部分开销,这部分开销可以通过缓存机制大量减少,而后的调用则全部属于 Java 范畴内的堆栈调用(即拿到代理类后,调用效率和原生方法调用几乎一致)。二、对 String 做零拷贝优化1、何为零拷贝零拷贝[3]是老生常谈的问题,Kafka 和 Netty 等都用到了零拷贝的知识,这里简单介绍一下其概念以便生疏的读者理解上流畅。零拷贝:是指计算机执行IO操作时,CPU不需要将数据从一个存储区域复制到另一个存储区域,进而减少上下文切换以及CPU的拷贝时间。它是一种IO操作优化技术。JDK8 中的 String 是如何拷贝的?为了实现字符串是不可变的特性,JDK 在构造 String 构造字符串的时候,会有拷贝的过程,比如上图是 JDK8 的 String 的一个构造函数的实现,其在堆内存中重新开辟了一块内存区域。如果要提升构造字符串的开销,就要避免这样的拷贝,即零拷贝。2、fastjson2 中如何实现 0 拷贝在 JDK8 中,String 有一个构造函数是不做拷贝的:但这个方法不是 public,不能直接访问到,可以反射执行,也可以使用 LambdaMetafactory 创建函数映射来调用,前面有介绍这个技巧。生成的函数映射可以缓存起来复用,而这个构造方法的签名是固定不变的,这意味着,只需要生成一次,后续所有需要初始化 String 的时候都可以复用。3、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 随处可见。三、常见类型解析优化fastjson2 里针对各种类型的优化处理很多,不能一一列举,这里仅以 Date 类型举例,我们前面举例了将 Date 格式化为 String,这次我们反过来,将 String 转换为 Date —— 如何快速将字符串解析成日期?以下给出几种实现方式,随后我们来做个对比。1、使用SimpleDateFormatSimpleDateFormat 是我们使用最广泛、最容易想到的方式,需要注意的是 SimpleDateFormat 不是线程安全的,并发场景下要 sync 同步处理。static final ThreadLocal<SimpleDateFormat> formatThreadLocal = ThreadLocal.withInitial( () -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));// get format from ThreadLocalSimpleDateFormat format = formatThreadLocal.get();format.parse(str);2、使用java.time.DateTimeFormatterJDK8 提供了 java.time API,吸收了 joda-time[4]的部分精华,功能更强大,性能也更好。同时,DateTimeFormatter 是线程安全的。static final DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");// use formatter parse DateLocalDateTime ldt = LocalDateTime.parse(str, formatter);ZoneOffset offset = DEFAULT_ZONE_ID.getRules().getOffset(ldt);long millis = ldt.toInstant(offset).toEpochMilli();Date date = new Date(millis);这种方法比使用 SimpleDateFormat 组合 ThreadLocal 代码更简洁,速度也大约要快 50%。图片源自github3、针对固定格式和固定时区优化我们在日常处理 Date 数据时,在国内最常见的格式就是 "yyyy-MM-dd HH:mm:ss",默认的时区为东 8 区,在 java.time 中的 ZonedId 是 "Asia/Shanghai"(而不是 Asia/Beijing),而东 8 区在1992年之后,不在使用夏令时,固定的 zoneOffset 是 +8,根据这个情况,我们可以针对性做优化,如下(为方便理解,以下为源码的简化版,去掉了影响阅读的边界处理等逻辑):public static Date parseYYYYMMDDHHMMSS19(String str) { char y0 = str.charAt(0); char y1 = str.charAt(1); char y2 = str.charAt(2); char y3 = str.charAt(3); char m0 = str.charAt(4); char m1 = str.charAt(5); ... char s1 = str.charAt(18); int year = (y0 - '0') * 1000 + (y1 - '0') * 100 + (y2 - '0') * 10 + (y3 - '0'); int month = (m0 - '0') * 10 + (m1 - '0'); int dom = (d0 - '0') * 10 + (d1 - '0'); int hour = (h0 - '0') * 10 + (h1 - '0'); int minute = (i0 - '0') * 10 + (i1 - '0'); int second = (s0 - '0') * 10 + (s1 - '0'); //换算成毫秒 long millis; if (year >= 1992 && (DEFAULT_ZONE_ID == SHANGHAI_ZONE_ID || DEFAULT_ZONE_ID.getRules() == IOUtils.SHANGHAI_ZONE_RULES)) { final int DAYS_PER_CYCLE = 146097; final long DAYS_0000_TO_1970 = (DAYS_PER_CYCLE * 5L) - (30L * 365L + 7L); long y = year; long m = month; long epochDay; { long total = 0; total += 365 * y; total += (y + 3) / 4 - (y + 99) / 100 + (y + 399) / 400; total += ((367 * m - 362) / 12); total += dom - 1; if (m > 2) { total--; boolean leapYear = (year & 3) == 0 && ((year % 100) != 0 || (year % 400) == 0); if (!leapYear) { total--; } } epochDay = total - DAYS_0000_TO_1970; } long seconds = epochDay * 86400 + hour * 3600 + minute * 60 + second - SHANGHAI_ZONE_OFFSET_TOTAL_SECONDS; millis = seconds * 1000L; } else { LocalDate localDate = LocalDate.of(year, month, dom); LocalTime localTime = LocalTime.of(hour, minute, second, 0); LocalDateTime ldt = LocalDateTime.of(localDate, localTime); ZoneOffset offset = DEFAULT_ZONE_ID.getRules().getOffset(ldt); millis = ldt.toEpochSecond(offset) * 1000; } return new Date(millis);}核心逻辑就是根据位数,直接开始计算给定的时间字符串,相对于参照的原点时间(1970-1-1 0点)过去了多少毫秒,这个优化,避免了parse Number的开销,精简了大量 Partten 的处理,处理流程非常高效。4、性能测试 & 结论benchmark[5]:Benchmark ModeCntScoreErrorUnitsDateParse.simpleDateFormatParseavgt(平均耗时)511.5404.170us/msDateParse.dateTimeFormatterParseavgt57.5940.200us/msDateParse.parseYYYYMMDDHHMMSS19avgt50.4250.098us/msJMH测试显示:方法 3 的耗时远低于其他方式,方法 3 这种针对性的类型解析优化可以使用在重度使用日期解析的优化场景,比如数据批量导入解析日期,大数据场景的 UDF 日期解析等。One more thingfastjson 系列相比同类 json 处理工具,虽然在安全性、鲁棒性等方面还可以提升,但其最大优势——处理速度,却使其他同类竞品望尘莫及。我们也可以在日常业务处理中,学习其精华部分,运用其中的技术亮点,优化业务处理速度,提升用户体验。参考链接:[1]https://github.com/alibaba/fastjson2[2]https://github.com/alibaba/fastjson2/wiki/fastjson_benchmark[3]https://so.csdn.net/so/search?q=零拷贝&spm=1001.2101.3001.7020[4]https://www.joda.org/joda-time/[5]https://github.com/alibaba/fastjson2/blob/main/benchmark/src/main/java/com/alibaba/fastjson2/benchmark/DateParse.java

相关阅读

  • Java 缺失的特性:操作符重载

  • 阿里妹导读本文介绍了什么是操作符重载、为什么需要操作符重载、如何在Java中实现操作符重载以及一些建议。什么是操作符重载操作符重载,就是把已经定义的、有一定功能的操作
  • Java 本地缓存选它就对了:Caffeine

  • 关注我,回复关键字“spring”,免费领取Spring学习资料。Guava Cache,他的优点是封装了get,put操作;提供线程安全的缓存操作;提供过期策略;提供回收策略;缓存监控。当缓存的数据超过
  • 菲涅尔公式:如何设计增透膜?

  • 照相机镜头常常呈现出一种特殊的光泽。这是人们为了降低光的反射,镀上一层特定厚度的氟化镁薄膜。一般称为减反射膜,也称增透膜——为了减少或消除透镜、棱镜、平面镜等光学表
  • 法学丨开学季,赢在学习方法

  • 开学季,法科生最常问的一个问题就是:如何学好法学?学习方法至关重要。程啸老师在《民法学习九讲》中写到:“工欲善其事,必先利其器。学习任何知识,如果有一套行之有效的学习方法,则
  • 武汉大学长江文明考古研究院再出新成果

  • 近日,国际学术期刊《世界考古》(World Archaeology, A&HCI收录)在线发表了武汉大学历史学院、武汉大学长江文明考古研究院的最新科研成果《从中国学者1920-2020发表的中外文核
  • 生物文化方法促进人与自然关系

  •   保护生物多样性早已成为世界共识,但生物多样性的保护范围和保护方法却一直是人们争论的焦点。其中,如何设定人类社群尤其是那些靠近保护区社群的角色,是近年来国外学界关注

热门文章

  • “复活”半年后 京东拍拍二手杀入公益事业

  • 京东拍拍二手“复活”半年后,杀入公益事业,试图让企业捐的赠品、家庭闲置品变成实实在在的“爱心”。 把“闲置品”变爱心 6月12日,“益心一益·守护梦想每一步”2018年四

最新文章

  • fastjson2为什么这么快?

  • 阿里妹导读本文作者从以下三个方面讲述了fastjson2 使用了哪些核心技术来提升速度。1、用「Lambda 生成函数映射」代替「高频的反射操作」2、对 String 做零拷贝优化3、常见
  • 《金字塔原理》:结构化总结神器

  • 阿里妹导读你是否会困惑一场汇报或者一些知识的总结该如何进行,或者你已经有了一些总结,但是还并不知道该如何结构化的组织它们,这个时候就可以采用金字塔结构进行组织。背景无
  • 工作一年,我重新理解了《重构》

  • 阿里妹导读把《重构:改善既有代码的设计》这本书推荐给已经接触了工程代码、工作一年左右的新同学,相信有了一定的经验积累,再结合日常项目实践中遇到的问题,对这本书的内容会有
  • Java 缺失的特性:操作符重载

  • 阿里妹导读本文介绍了什么是操作符重载、为什么需要操作符重载、如何在Java中实现操作符重载以及一些建议。什么是操作符重载操作符重载,就是把已经定义的、有一定功能的操作
  • 单县羊肉汤入选省乡村好物优秀案例

  • 牡丹晚报全媒体记者 文杰近日,山东省文化和旅游厅公布了2022年度“好客山东·乡村好时节”优秀案例入选名单,共有年度主题活动、乡村旅游新地标、好玩乡村、乡村好物、乡村研