8

对序列化中反射的一点思考

 3 years ago
source link: https://my.oschina.net/OutOfMemory/blog/4817316
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
对序列化中反射的一点思考 - ksfzhaohui的个人页面 - OSCHINA - 中文开源技术交流社区

序列化大家都不陌生,说白了就是把当前类对象的状态保存为二进制,然后被用来持久化或者网络传输;常用的RPC框架在数据传输前都会进行序列化操作,主流的RPC框架包含了多种序列化方式比如protobuf,fastjson,kryo,hessian,java内置序列化等等,大致可以分为二进制和字符串(json字符串)。

因为需要把当前类对象状态保存为二进制,所以往往需要获取所有类属性,这时候大部分的序列化方式都用到了反射,通过反射获取所有类属性获取方法,然后获取到属性值,大致如下:

//1.方法
Method[] methods = obj.getClass().getDeclaredMethods();
for(Method method : methods) {
    method.invoke(obj);
}
//2.字段
Field fields[] = obj.getClass().getDeclaredFields();
for (Field field : fields) {
    field.get(obj);
}

但是反射往往在性能上被大家所怀疑,所以出现了类似protobuf采用自动生成序列化代码的方式,fastjson使用ASM代替反射的方式;下面我们先用简单的测试来对比一下各种方式的性能,看反射是否真的慢;

在windows10+jdk8环境下分别对直接,反射,以及ASM调用方法分别进行压力测试,看起消耗的时间,测试中可以多次执行,取稳定的值;以下测试分别从Person对象通过方法获取属性值,如下:

public class Person {
    private String id;
    private String name;
    
    public String getId() {
        return id;
    }
    public String getName() {
        return name;
    }
}

直接调用也就是我们平时最常用的方式,直接通过对象调用方法名称获取属性值,我们在压测的时候会分别轮询两个方法:

public static void test() {
    Person person = new Person("10001", "zhaohui");
    long startTime = System.currentTimeMillis();
    for (int i = 0; i < 1_0000_0000; i++) {
        if (i % 2 == 0) {
            person.getId();
        } else {
            person.getName();
        }
    }
    long endTime = System.currentTimeMillis();
    System.out.println("Manual time:" + (endTime - startTime) + "ms");
    }

多次测试结果大概在90ms左右,直接调用速度是最快的,但是需要我们手动的写每个bean的序列化代码,或者像protobuf一样使用工具给我们生成所有的序列化代码,比如生成Person的序列化代码:

 public void writeTo(com.google.protobuf.CodedOutputStream output)
                        throws java.io.IOException {
    getSerializedSize();
    if (((bitField0_ & 0x00000001) == 0x00000001)) {
      output.writeInt32(1, id_);
    }
    if (((bitField0_ & 0x00000002) == 0x00000002)) {
      output.writeBytes(2, getNameBytes());
    }
    getUnknownFields().writeTo(output);
 }

可以看到每个生成的bean都自动生成了序列化代码,并且所有的bean都继承于统一的抽象类,这样提供一整套规范;有个缺点就是每次修改需要手动改proto文件,然后重新生成代码;

使用jdk提供的反射机制,获取Methods,然后获取属性值,具体代码如下:

    public static void test() throws Exception {
        long startTime = System.currentTimeMillis();
        Person person = new Person("10001", "zhaohui");
        Method[] ms = Person.class.getDeclaredMethods();
        for (int i = 0; i < 1_0000_0000; i++) {
            ms[i & ms.length - 1].invoke(person);
        }
        long endTime = System.currentTimeMillis();
        System.out.println("Reflex time:" + (endTime - startTime) + "ms");
    }

经测试时间大概维持在205ms左右,和直接调用还是存在一定差距的,不过jdk每一轮的升级,都在提升性能,比如jdk7中引入的MethodHandle,模拟字节码层面的调用;

ASM调用

反射是读取持久堆上存储的类信息,而ASM是直接处理.class字节码的,无需加载类,我们这里使用ReflectASM来进行测试;

ReflectASM 是一个非常小的 Java 类库,通过代码生成来提供高性能的反射处理,自动为 get/set 字段提供访问类,访问类使用字节码操作而不是 Java 的反射技术,因此非常快。

    public static void test() {
        Person person = new Person("10001", "zhaohui");
        long startTime = System.currentTimeMillis();

        MethodAccess methodAccess = MethodAccess.get(Person.class);
        String[] mns = methodAccess.getMethodNames();
        int len = mns.length;
        int indexs[] = new int[len];
        for (int i = 0; i < len; i++) {
            indexs[i] = methodAccess.getIndex(mns[i]);
        }
        for (int i = 0; i < 1_0000_0000; i++) {
            methodAccess.invoke(person, indexs[i & len - 1]);
        }

        long endTime = System.currentTimeMillis();
        System.out.println("ASM time:" + (endTime - startTime) + "ms");
    }

经测试时间维持在110ms左右,速度还是很快的,快赶上直接调用了;其中为了获得最大性能,应使用方法或字段索引而不是名称;

可以看到虽然反射性能一直在提升,但是相比直接调用和ASM的方式还是有一点差距;但其实如果用在RPC上这点时间在整个网络传输上来说可以说微乎其微;如果对性能极度追求,可以考虑使用直接调用或者ASM的方式;

关于直接调用上面说到protobuf,通过工具生成序列化代码,但是这种方式每次改动都要手动生成代码,有点麻烦,是否可以直接利用lombok这种框架做一个扩展,自动生成序列化代码,其实lombok底层也用到ASM,直接生成字节码代码,提供序列化注解

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Serialize {
}

然后可以直接把注解应用到bean中,直接帮助我们生成序列化代码,就像@Getter/@Setter一样;相当于直接调用和ASM方式的一种整合;类似如下代码:

@Serialize
public class Person {
    private String id;
    private String name;
    
    //自动生成
    public byte[] serialize(){
        ByteBuffer bb = ByteBuffer.allocate(100);
        bb.put(id.getBytes());
        bb.put(name.getBytes());
        return bb.array();
    }
}

可以关注微信公众号「 回滚吧代码」,第一时间阅读,文章持续更新;专注Java源码、架构、算法和面试


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK