11

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

 4 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源码、架构、算法和面试


Recommend

  • 124

    文/温国兵数据安全对于普通人而言会很陌生,然而早已在 DBA 心里生根发芽。数据对于一家企业的重要程度不言而喻,个人数据的管理也是极其重要,否则当数据损坏或者永久丢失,那将是毁灭性的灾难。既然提到备份与恢复,笔者简单介绍下 MySQL 数据库的备份与恢复。按...

  • 48
    • GAD腾讯游戏开发者平台 gad.qq.com 6 years ago
    • Cache

    对游戏UI的一点思考

        UI决定了一个游戏的初体验,甚至决定了玩家的初始留存,甚至可以说决定了一个游戏的品质,虽然看起来是表象的,却是直指游戏核心的。简单讲,玩家认可一款游戏永远都是造型场景好,剧

  • 8

    关于黑暗力量(BlackEnergy)的一点思考 腾讯电脑管家...

  • 44
    • 微信 mp.weixin.qq.com 6 years ago
    • Cache

    架构上的一点思考

  • 36
    • www.tuicool.com 5 years ago
    • Cache

    软件工程实践上的一点思考

    曾经大学时对于软件工程这类理论课不屑一顾,认为这些课本都是只在大学里讲学而并不实际参与工程的教授写的东西。但是经过这些年从自己开发程序编写代码,到与公司团队同学、兴趣圈的朋友一起开发项目,也积累、总结了一些经验和教训。正...

  • 14
    • www.jinse.com 5 years ago
    • Cache

    关于稳定币的一点冷思考

    行情坐过山车,我们又一次见证了历史。 在这次的黑天鹅事件中,币市的联动大跌,各媒体直呼“比特币避险神话被打破了”。而当各类资产都显得有些狼狈的时候,稳定币,反倒成了万紫千红中的那一抹绿。

  • 37

    本篇为对shiro-550反序列化漏洞的分析以及扩展问题的思考 PoC import os import re import base64 import uuid import subprocess import requests from Crypto.Cipher import AES JAR_FILE =...

  • 15

    serverless/serverless 部署工具的一点思考

  • 12

    枚举防止反射,克隆及序列化破环单例模式的原理   在上一篇文章中详细的介绍了实现单例模式的几种方式,以及...

  • 5
    • timeshu.github.io 2 years ago
    • Cache

    Java反序列化-反射(二)

    Time'BlogJava反序列化-反射(二)发表于2022-01-02|更新于2022-03...

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK