乐趣区

关于java:对序列化中反射的一点思考

前言

序列化大家都不生疏,说白了就是把以后类对象的状态保留为二进制,而后被用来长久化或者网络传输;罕用的 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 源码、架构、算法和面试。

退出移动版