【Java技术指南】「序列化系列」深入挖掘FST快速序列化压缩内存的利器的特性和原理

简介: 【Java技术指南】「序列化系列」深入挖掘FST快速序列化压缩内存的利器的特性和原理

FST的概念和定义


FST序列化全称是Fast Serialization Tool,它是对Java序列化的替换实现。既然前文中提到Java序列化的两点严重不足,在FST中得到了较大的改善,FST的特征如下:


  • JDK提供的序列化提升了10倍,体积也减少3-4倍多
  • 支持堆外Maps,和堆外Maps的持久化
  • 支持序列化为JSON



FST序列化的使用


FST的使用有两种方式,一种是快捷方式,另一种需要使用ObjectOutput和ObjectInput。


直接使用FSTConfiguration提供的序列化和反序列化接口


public static void serialSample() {
    FSTConfiguration conf = FSTConfiguration.createAndroidDefaultConfiguration();
    User object = new User();
    object.setName("huaijin");
    object.setAge(30);
    System.out.println("serialization, " + object);
    byte[] bytes = conf.asByteArray(object);
    User newObject = (User) conf.asObject(bytes);
    System.out.println("deSerialization, " + newObject);
}
复制代码


FSTConfiguration也提供了注册对象的Class接口,如果不注册,默认会将对象的Class Name写入。这个提供了易用高效的API方式,不使用ByteArrayOutputStreams而直接得到byte[]。


使用ObjectOutput和ObjectInput,能更细腻控制序列化的写入写出:

static FSTConfiguration conf = FSTConfiguration.createAndroidDefaultConfiguration();
static void writeObject(OutputStream outputStream, User user) throws IOException {
    FSTObjectOutput out = conf.getObjectOutput(outputStream);
    out.writeObject(user);
    out.close();
}
static FstObject readObject(InputStream inputStream) throws Exception {
    FSTObjectInput input = conf.getObjectInput(inputStream);
    User fstObject = (User) input.readObject(User.class);
    input.close();
    return fstObject;
}
复制代码


FST在Dubbo中的应用


  • Dubbo中对FstObjectInputFstObjectOutput重新包装解决了序列化和反序列化空指针的问题。
  • 并且构造了FstFactory工厂类,使用工厂模式生成FstObjectInput和FstObjectOutput。其中同时使用单例模式,控制整个应用中FstConfiguration是单例,并且在初始化时将需要序列化的对象全部注册到FstConfiguration。
  • 对外提供了同一的序列化接口FstSerialization,提供serialize和deserialize能力。



FST序列化/反序列化


FST序列化存储格式


基本上所有以Byte形式存储的序列化对象都是类似的存储结构,不管class文件、so文件、dex文件都是类似,这方面没有什么创新的格式,最多是在字段内容上做了一些压缩优化,包括我们最常使用的utf-8编码都是这个做法。


FST的序列化存储和一般的字节格式化存储方案也没有标新立异的地方,比如下面这个FTS的序列化字节文件


00000001:  0001 0f63 6f6d 2e66 7374 2e46 5354 4265
00000010:  616e f701 fc05 7630 7374 7200 
复制代码


格式:

Header|类名长度|类名String|字段1类型(1Byte) | [长度] | 内容|字段2类型(1Byte) | [长度] | 内容|…
复制代码
  • 0000:字节数组类型:00标识OBJECT
  • 0001:类名编码,00标识UTF编码,01表示ASCII编码
  • 0002:Length of class name (1Byte) = 15
  • 0003~0011:Class name string (15Byte)
  • 0012:Integer类型标识 0xf7
  • 0013:Integer的值=1
  • 0014:String类型标识 0xfc
  • 0015:String的长度=5
  • 0016~001a:String的值"v0str"
  • 001b~001c:END

从上面可以看到Integer类型序列化后只占用了一个字节(值等于1),并不像在内存中占用4Byte,所以可以看出是根据一定规则做了压缩,具体代码看FSTObjectInput#instantiateSpecialTag中对不同类型的读取,FSTObjectInput也定义不同类型对应的枚举值:

public class FSTObjectOutput implements ObjectOutput {
    private static final FSTLogger LOGGER = FSTLogger.getLogger(FSTObjectOutput.class);
    public static Object NULL_PLACEHOLDER = new Object() { 
    public String toString() { return "NULL_PLACEHOLDER"; }};
    public static final byte SPECIAL_COMPATIBILITY_OBJECT_TAG = -19; // see issue 52
    public static final byte ONE_OF = -18;
    public static final byte BIG_BOOLEAN_FALSE = -17;
    public static final byte BIG_BOOLEAN_TRUE = -16;
    public static final byte BIG_LONG = -10;
    public static final byte BIG_INT = -9;
    public static final byte DIRECT_ARRAY_OBJECT = -8;
    public static final byte HANDLE = -7;
    public static final byte ENUM = -6;
    public static final byte ARRAY = -5;
    public static final byte STRING = -4;
    public static final byte TYPED = -3; // var class == object written class
    public static final byte DIRECT_OBJECT = -2;
    public static final byte NULL = -1;
    public static final byte OBJECT = 0;
    protected FSTEncoder codec;
    ...
}
复制代码



FST序列化和反序列化原理


对Object进行Byte序列化,相当于做了持久化的存储,在反序列的时候,如果Bean的定义发生了改变,那么反序列化器就要做兼容的解决方案,我们知道对于JDK的序列化和反序列,serialVersionUID对版本控制起了很重要的作用。FST对这个问题的解决方案是通过@Version注解进行排序。


在进行反序列操作的时候,FST会先反射或者对象Class的所有成员,并对这些成员进行了排序,这个排序对兼容起了关键作用,也就是@Version的原理。在FSTClazzInfo中定义了一个

defFieldComparator比较器,用于对Bean的所有Field进行排序:

public final class FSTClazzInfo {
    public static final Comparator<FSTFieldInfo> defFieldComparator = new Comparator<FSTFieldInfo>() {
        @Override
        public int compare(FSTFieldInfo o1, FSTFieldInfo o2) {
            int res = 0;
            if ( o1.getVersion() != o2.getVersion() ) {
                return o1.getVersion() < o2.getVersion() ? -1 : 1;
            }
            // order: version, boolean, primitives, conditionals, object references
            if (o1.getType() == boolean.class && o2.getType() != boolean.class) {
                return -1;
            }
            if (o1.getType() != boolean.class && o2.getType() == boolean.class) {
                return 1;
            }
            if (o1.isConditional() && !o2.isConditional()) {
                res = 1;
            } else if (!o1.isConditional() && o2.isConditional()) {
                res = -1;
            } else if (o1.isPrimitive() && !o2.isPrimitive()) {
                res = -1;
            } else if (!o1.isPrimitive() && o2.isPrimitive())
                res = 1;
//                if (res == 0) // 64 bit / 32 bit issues
//                    res = (int) (o1.getMemOffset() - o2.getMemOffset());
            if (res == 0)
                res = o1.getType().getSimpleName().compareTo(o2.getType().getSimpleName());
            if (res == 0)
                res = o1.getName().compareTo(o2.getName());
            if (res == 0) {
                return o1.getField().getDeclaringClass().getName().compareTo(o2.getField().getDeclaringClass().getName());
            }
            return res;
        }
    };
    ...
}
复制代码


从代码实现上可以看到,比较的优先级是Field的Version大小,然后是Field类型,所以总的来说Version越大排序越靠后,至于为什么要排序,看下FSTObjectInput#instantiateAndReadNoSer方法

public class FSTObjectInput implements ObjectInput {
  protected Object instantiateAndReadNoSer(Class c, FSTClazzInfo clzSerInfo, FSTClazzInfo.FSTFieldInfo referencee, int readPos) throws Exception {
        Object newObj;
        newObj = clzSerInfo.newInstance(getCodec().isMapBased());
        ...
        } else {
            FSTClazzInfo.FSTFieldInfo[] fieldInfo = clzSerInfo.getFieldInfo();
            readObjectFields(referencee, clzSerInfo, fieldInfo, newObj,0,0);
        }
        return newObj;
    }
    protected void readObjectFields(FSTClazzInfo.FSTFieldInfo referencee, FSTClazzInfo serializationInfo, FSTClazzInfo.FSTFieldInfo[] fieldInfo, Object newObj, int startIndex, int version) throws Exception {
        if ( getCodec().isMapBased() ) {
            readFieldsMapBased(referencee, serializationInfo, newObj);
            if ( version >= 0 && newObj instanceof Unknown == false)
                getCodec().readObjectEnd();
            return;
        }
        if ( version < 0 )
            version = 0;
        int booleanMask = 0;
        int boolcount = 8;
        final int length = fieldInfo.length;
        int conditional = 0;
        for (int i = startIndex; i < length; i++) { // 注意这里的循环
            try {
                FSTClazzInfo.FSTFieldInfo subInfo = fieldInfo[i];
                if (subInfo.getVersion() > version ) {   // 需要进入下一个版本的迭代
                    int nextVersion = getCodec().readVersionTag();  // 对象流的下一个版本
                    if ( nextVersion == 0 ) // old object read
                    {
                        oldVersionRead(newObj);
                        return;
                    }
                    if ( nextVersion != subInfo.getVersion() ) {  // 同一个Field的版本不允许变,并且版本变更和流的版本保持同步
                        throw new RuntimeException("read version tag "+nextVersion+" fieldInfo has "+subInfo.getVersion());
                    }
          readObjectFields(referencee,serializationInfo,fieldInfo,newObj,i,nextVersion);  // 开始下一个Version的递归
                    return;
                }
                if (subInfo.isPrimitive()) {
                  ...
                } else {
                    if ( subInfo.isConditional() ) {
                      ...
                    }
                    // object 把读出来的值保存到FSTFieldInfo中
                    Object subObject = readObjectWithHeader(subInfo);
                    subInfo.setObjectValue(newObj, subObject);
        }
        ...
复制代码


从这段代码的逻辑基本就可以知道FST的序列化和反序列化兼容的原理了,注意里面的循环,正是按照排序后的Filed进行循环,而每个FSTFieldInfo都记录自己在对象流中的位置、类型等详细信息:


序列化:


  • 按照Version对Bean的所有Field进行排序(不包括static和transient修饰的member),没有@Version注解的Field默认version=0;如果version相同,按照version, boolean, primitives, conditionals, object references排序
  • 按照排序的Field把Bean的Field逐个写到输出流
  • @Version的版本只能加不能减小,如果相等的话,有可能因为默认的排序规则,导致流中的Filed顺序和内存中的FSTFieldInfo[]数组的顺序不一致,而注入错误



反序列化:


  • 反序列化按照对象流的格式进行解析,对象流中保存的Field顺序和内存中的FSTFieldInfo顺序保持一致
  • 相同版本的Field在对象流中存在,在内存Bean中缺失:可能抛异常(会有后向兼容问题)
  • 对象流中包含内存Bean中没有的高版本Field:正常(老版本兼容新)
  • 相同版本的Field在对象流中缺失,在内存Bean中存在:抛出异常
  • 相同的Field在对象流和内存Bean中的版本不一致:抛出异常
  • 内存Bean增加了不高于最大版本的Field:抛出异常


所以从上面的代码逻辑就可以分析出这个使用规则:@Version的使用原则就是,每新增一个Field,就对应的加上@Version注解,并且把version的值设置为当前版本的最大值加一,不允许删除Field

另外再看一下@Version注解的注释:明确说明了用于后向兼容


package org.nustaq.serialization.annotations;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.FIELD})
/**
 * support for adding fields without breaking compatibility to old streams.
 * For each release of your app increment the version value. No Version annotation means version=0.
 * Note that each added field needs to be annotated.
 *
 * e.g.
 *
 * class MyClass implements Serializable {
 *
 *     // fields on initial release 1.0
 *     int x;
 *     String y;
 *
 *     // fields added with release 1.5
 *     @Version(1) String added;
 *     @Version(1) String alsoAdded;
 *
 *     // fields added with release 2.0
 *     @Version(2) String addedv2;
 *     @Version(2) String alsoAddedv2;
 *
 * }
 *
 * If an old class is read, new fields will be set to default values. You can register a VersionConflictListener
 * at FSTObjectInput in order to fill in defaults for new fields.
 *
 * Notes/Limits:
 * - Removing fields will break backward compatibility. You can only Add new fields.
 * - Can slow down serialization over time (if many versions)
 * - does not work for Externalizable or Classes which make use of JDK-special features such as readObject/writeObject
 *   (AKA does not work if fst has to fall back to 'compatible mode' for an object).
 * - in case you use custom serializers, your custom serializer has to handle versioning
 *
 */
public @interface Version {
    byte value();
}
复制代码
public class FSTBean implements Serializable {
    /** serialVersionUID */
    private static final long serialVersionUID = -2708653783151699375L;
    private Integer v0int
    private String v0str;
}
复制代码



准备序列化和反序列化方法

public class FSTSerial {
    private static void serialize(FstSerializer fst, String fileName) {
        try {
            FSTBean fstBean = new FSTBean();
            fstBean.setV0int(1);
            fstBean.setV0str("v0str");
            byte[] v1 = fst.serialize(fstBean);
            FileOutputStream fos = new FileOutputStream(new File("byte.bin"));
            fos.write(v1, 0, v1.length);
            fos.close();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    private static void deserilize(FstSerializer fst, String fileName) {
        try {
            FileInputStream fis = new FileInputStream(new File("byte.bin"));
            ByteArrayOutputStream baos = new ByteArrayOutputStream();
            byte[] buf = new byte[256];
            int length = 0;
            while ((length = fis.read(buf)) > 0) {
                baos.write(buf, 0, length);
            }
            fis.close();
            buf = baos.toByteArray();
            FSTBean deserial = fst.deserialize(buf, FSTBean.class);
            System.out.println(deserial);
            System.out.println(deserial);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    public static void main(String[] args) {
        FstSerializer fst = new FstSerializer();
        serialize(fst, "byte.bin");
        deserilize(fst, "byte.bin");
    }
}




相关文章
|
2月前
|
机器学习/深度学习 存储 算法
NoProp:无需反向传播,基于去噪原理的非全局梯度传播神经网络训练,可大幅降低内存消耗
反向传播算法虽是深度学习基石,但面临内存消耗大和并行扩展受限的问题。近期,牛津大学等机构提出NoProp方法,通过扩散模型概念,将训练重塑为分层去噪任务,无需全局前向或反向传播。NoProp包含三种变体(DT、CT、FM),具备低内存占用与高效训练优势,在CIFAR-10等数据集上达到与传统方法相当的性能。其层间解耦特性支持分布式并行训练,为无梯度深度学习提供了新方向。
103 1
NoProp:无需反向传播,基于去噪原理的非全局梯度传播神经网络训练,可大幅降低内存消耗
|
23天前
|
存储 缓存 Java
【高薪程序员必看】万字长文拆解Java并发编程!(5):深入理解JMM:Java内存模型的三大特性与volatile底层原理
JMM,Java Memory Model,Java内存模型,定义了主内存,工作内存,确保Java在不同平台上的正确运行主内存Main Memory:所有线程共享的内存区域,所有的变量都存储在主存中工作内存Working Memory:每个线程拥有自己的工作内存,用于保存变量的副本.线程执行过程中先将主内存中的变量读到工作内存中,对变量进行操作之后再将变量写入主内存,jvm概念说明主内存所有线程共享的内存区域,存储原始变量(堆内存中的对象实例和静态变量)工作内存。
59 0
|
2月前
|
数据采集 监控 Oracle
GraalVM 24 正式发布阿里巴巴贡献重要特性 —— 支持 Java Agent 插桩
阿里巴巴是 GraalVM 全球顾问委员会的唯一中国代表,阿里云程序语言与编译器团队和可观测团队合作实现了 GraalVM 应用的无侵入可观测能力,并在 ARMS 平台上线了该功能。目前在 GraalVM 24 中发布的是支持 Java agent 的第一步,其余能力将在 GraalVM 的后续版本中陆续发布。
205 21
|
3月前
|
存储 缓存 算法
JVM简介—1.Java内存区域
本文详细介绍了Java虚拟机运行时数据区的各个方面,包括其定义、类型(如程序计数器、Java虚拟机栈、本地方法栈、Java堆、方法区和直接内存)及其作用。文中还探讨了各版本内存区域的变化、直接内存的使用、从线程角度分析Java内存区域、堆与栈的区别、对象创建步骤、对象内存布局及访问定位,并通过实例说明了常见内存溢出问题的原因和表现形式。这些内容帮助开发者深入理解Java内存管理机制,优化应用程序性能并解决潜在的内存问题。
215 29
JVM简介—1.Java内存区域
|
3月前
|
Java 数据库
【YashanDB知识库】kettle同步大表提示java内存溢出
在数据导入导出场景中,使用Kettle进行大表数据同步时出现“ERROR:could not create the java virtual machine!”问题,原因为Java内存溢出。解决方法包括:1) 编辑Spoon.bat增大JVM堆内存至2GB;2) 优化Kettle转换流程,如调整批量大小、精简步骤;3) 合理设置并行线程数(PARALLELISM参数)。此问题影响所有版本,需根据实际需求调整相关参数以避免内存不足。
|
3月前
|
缓存 运维 Java
Java静态代码块深度剖析:机制、特性与最佳实践
在Java中,静态代码块(或称静态初始化块)是指类中定义的一个或多个`static { ... }`结构。其主要功能在于初始化类级别的数据,例如静态变量的初始化或执行仅需运行一次的初始化逻辑。
110 4
|
4月前
|
存储 IDE Java
java设置栈内存大小
在Java应用中合理设置栈内存大小是确保程序稳定性和性能的重要措施。通过JVM参数 `-Xss`,可以灵活调整栈内存大小,以适应不同的应用场景。本文介绍了设置栈内存大小的方法、应用场景和注意事项,希望能帮助开发者更好地管理Java应用的内存资源。
181 4
|
4月前
|
Java Shell 数据库
【YashanDB 知识库】kettle 同步大表提示 java 内存溢出
【问题分类】数据导入导出 【关键字】数据同步,kettle,数据迁移,java 内存溢出 【问题描述】kettle 同步大表提示 ERROR:could not create the java virtual machine! 【问题原因分析】java 内存溢出 【解决/规避方法】 ①增加 JVM 的堆内存大小。编辑 Spoon.bat,增加堆大小到 2GB,如: if "%PENTAHO_DI_JAVA_OPTIONS%"=="" set PENTAHO_DI_JAVA_OPTIONS="-Xms512m" "-Xmx512m" "-XX:MaxPermSize=256m" "-
|
4月前
|
安全 C语言 C++
彻底摘明白 C++ 的动态内存分配原理
大家好,我是V哥。C++的动态内存分配允许程序在运行时请求和释放内存,主要通过`new`/`delete`(用于对象)及`malloc`/`calloc`/`realloc`/`free`(继承自C语言)实现。`new`分配并初始化对象内存,`delete`释放并调用析构函数;而`malloc`等函数仅处理裸内存,不涉及构造与析构。掌握这些可有效管理内存,避免泄漏和悬空指针问题。智能指针如`std::unique_ptr`和`std::shared_ptr`能自动管理内存,确保异常安全。关注威哥爱编程,了解更多全栈开发技巧。 先赞再看后评论,腰缠万贯财进门。
228 0
|
14天前
|
算法 Java 调度
Java多线程基础
本文主要讲解多线程相关知识,分为两部分。第一部分涵盖多线程概念(并发与并行、进程与线程)、Java程序运行原理(JVM启动多线程特性)、实现多线程的两种方式(继承Thread类与实现Runnable接口)及其区别。第二部分涉及线程同步(同步锁的应用场景与代码示例)及线程间通信(wait()与notify()方法的使用)。通过多个Demo代码实例,深入浅出地解析多线程的核心知识点,帮助读者掌握其实现与应用技巧。