分析FastJSON为何那么快与字节码增强技术揭秘

前言

JSON作为一种序列化协议,它有可读性强的特点,其越来越流行,越来越成为一个不同进程交换数据的序列化普遍使用的一个协议,其也不需要配置schama,JSON协议有自己独有的结构,使得反序列化一个JSON串没有语言或是其他什么限制,按照共同知道的JSON规则反序列化就可以了。在分布式环境中,数据的交换是必不可少的,除了二进制的序列化协议,我们常常也大量使用JSON的序列化协议,所以,JSON序列化与反序列化的性能就变得异常重要了。

本篇文章的议题是分析FastJSON的技术内幕,模仿FastJSON自己做一个简易的序列化(不包括反序列化,因为反序列化需要一些语法词法分析,较为繁琐)工具,并且也会使用ASM进行字节码层面的类增强。

学习一个好的作品、性能的锱铢必较和一些优化的思路,我觉得是很好的一件事,就像叶圣陶说的一句话,艺术的事情大都始于模仿,终于独创,而代码与艺术同理,通往一个结果可以有很多条道路,但高级程序员往往追求完美的道路去完成目标,而普通的程序员往往只需要能到目标就行,就像序列化POJO,可以使用反射将属性一一读取出来,但高级程序员会使用字节码增强减少反射开销,而普通程序员直接使用反射,虽然都可以达到目的,但那条达到目的的道路却是不同的,走的路不同,或许这就是高级程序员与一般程序员的区别吧。

1. FastJSON为什么这么快

下面就分几个我认为比较独特的部分,来一一分析FastJSON的技术内幕

1.1 字符数组的处理

我们知道,在序列化与反序列化时,一定少不了字符串的处理,在序列化过程中,往往需要从POJO中读取属性名与属性值,将其一一写入字符串,而字符串究其本身就是字符数组 char[]

我们第一反应其实是使用 StringBuilder 去构造、添加字符串,但FastJSON中不是,在FastJSON定制了相当多的实现,其中就有这么一个定制,其为SerializeWriter 类,为什么要定制这么一个实现呢?有以下几点优化点:

  1. 减少了字符数组内存的开辟(new一个char[])
  2. 减少了剩余容量的检查

1.1.1 减少容量检查

就像ArrayList那样,内部是数组结构,数组有一个通病,就是其创建出来必须指定一个固定的容量,这就有概率造成一些内存浪费,比如Arraylist中开辟了16个容量的int数组,但你只使用了4个容量,这样就有12*4=48字节的内存空间浪费了。并且每次加入元素都要查看数组是否有剩余空间存放,这就需要每次加入元素时都需要一次容量的检查。既然数组这么多缺点,那为什么不使用链表呢?链表有多少元素就用多少容量,唯一浪费空间的点在于存放next或pre指针,而且不需要容量检查,因为数组是一种顺序IO,其内存地址是连续的,可以利用到CPU的缓存,而链表中的元素在内存中地址是乱的,分布在内存的各个地方,这就造成了随机IO,并且对CPU缓存并不友好,两者是最基础的数据结构,各自都有各自的优缺点,需要分不同场景使用最合适的数据结构。(扯的有点远了)

其中第二点,在使用StringBuilder时,其内部也是一个char数组,在每一次append字符串时会检查是否有剩余容量可以分配,但如果我们已经知道一次要写入几个值,比如写一个开头 ‘{’ 紧接着就是双引号的属性名 “propertiesName” 然后就是一个冒号 ‘:’ ,这样连续写入3个类型,只需要一次的容量检查。

1.1.2 减少内存分配开销

假如我们使用StringBuilder来序列化,有以下几个问题:

  • 一次分配多大的内存空间比较合适呢?分配太大的char数组又太浪费空间,太小又会造成很多的数组重分配(剩余容量不够分配,new一个新的char并拷贝,操作有点耗时)
  • 序列化一次就new一个char数组,分配一次内存空间,分配一段连续的内存空间有时候也是有点耗时的操作

而FastJSON中定制的字符串处理类SerializeWriter就解决了以上两个问题。

我们先来看看SerializeWriter的构造器:

// 顾名思义,其将char数组缓存起来了
protected char buf[];
// 字符数组缓存的地方,由于有线程安全的问题,我们保证每个线程都有一个缓存的字符数组
// 这样不会多线程同时操作被共享的字符数组
private final static ThreadLocal<char[]> bufLocal = new ThreadLocal<char[]>();

public SerializeWriter(Writer writer, int defaultFeatures, SerializerFeature... features){
   
  this.writer = writer;

  // 可以看到,每次构造writer类时都会尝试去ThreadLocal查看是否有缓存的char数组
  buf = bufLocal.get();

  if (buf != null) {
   
    bufLocal.set(null);
  } else {
   
    // 如果没有缓存,这里就创建一个,默认为2048的容量
    buf = new char[2048];
  }
	// ...
}

可以看到,每次new一个SerializeWriter类时都会尝试从ThreadLocal这个缓存中查看是否有缓存起来的char数组,如果有就直接使用,减少了内存分配的开销(亲测,这部分开销在序列化次数很大的时候还蛮大的)

再来看看如何使用这个Writer,我们直接来看JSON.toJsonString的API中做了什么

public static String toJSONString(Object object, // 
                                  SerializeConfig config, // 
                                  SerializeFilter[] filters, // 
                                  String dateFormat, //
                                  int defaultFeatures, // 
                                  SerializerFeature... features) {
   
  // new一个writer
  SerializeWriter out = new SerializeWriter(null, defaultFeatures, features);

  try {
   
    // ...

    serializer.write(object);

    // 调用toString,输出序列化的数据
    return out.toString();
  } finally {
   
    // 调用close,释放资源
    out.close();
  }
}

这其中就包含了两个writer的用法,一个是toString方法,这和StringBuilder很像,输出自身字符数组所包含的值,这里Writer中也有一个游标,指示字符数组中有效部分(在下面会讲到),这里来看看toString方法

public String toString() {
   
  return new String(buf, 0, count);
}

指定输出的是字符数组中0到count(游标)的位置的内容。

那么close方法又是做什么的呢?

// 缓存阈值
private static int BUFFER_THRESHOLD = 1024 * 128;

public void close() {
   
  
  //...
  
  // 没有超过一个阈值,就进行存储重复利用
  if (buf.length <= BUFFER_THRESHOLD) {
   
    bufLocal.set(buf);
  }

  this.buf = null;
}

这里可以看到,每次序列化完成之后都会放回ThreadLocal中进行重复利用,这样就减少了分配内存的开销了。

其中Writer有一个游标值count,来指示写入位置
在这里插入图片描述
上图假设同一条线程序列化了两次的场景,可以看到,依靠count游标指示内容可以对char数组反复写入,这一点和NIO中的ByteBuf的写游标和读游标有异曲同工之妙

1.2 序列化顺序输出

这一条性能优化是为了反序列化做准备的,在FastJSON中,是按照字符来有序输出的,为什么这么做能提高性能呢?我们来举一个例子:

// pojo
public class User {
   
  
  private int id;
  private String name;
	private int age;

  // getter/setter
}

此时序列化结果就会变成这样

{"age":18,"id":1,"name":"jack"}

在反序列化的时候我根据POJO的属性进行一个排序,得出age、id、name这样一个顺序,这样我就假设第一次读取的是age,并对字符串进行验证,第一个字符是否是age,若是,做词法语法分析,读取并存放age属性到pojo,下一个继续预测属性为id。若此时id没值,JSON中只有age和name两个属性,那么在第二次预测验证的时候就会失败,进行name的预测,以此类推。

这样做与不排序有什么分别呢?假设不进行排序,无法预测第一个属性是什么,在pojo中第一个属性为id,那么你需要遍历JSON字符串来找id这个字符串,也就是说,找id,你需要遍历字符串中第一个属性age,第二个属性id,这样才找到id,存放id属性,找name属性,需要再从头开始第一个属性第二个属性,到第三次token的遍历才能得到目标值,这样,无序读取就需要6次token遍历的读取,反观有序读取的预测读取,只需要3次token的遍历读取,平均来说可以减少50%的字符串遍历!这在FastJSON中是最重要的性能优化点

1.3 使用ASM避免反射开销

这部分的详细说明将在下面,自定义一个简易FastJSON中会有说明

在序列化时使用反射读取POJO的

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值