분석 원리 직렬화

머리말

일반적으로 우리는 자바 직렬화 및 역 직렬화, 단순히 클래스가 구현하는 사용 Serializable의 JDK에 사물의 나머지 인터페이스를. 오늘 우리는 자바 직렬화를 달성하는 방법이 무엇인지 탐구하고 그들이 직렬화에 의해 발생하는 문제를 해결하는 방법, 몇 가지 일반적인 컬렉션 클래스를 살펴볼 것이다.

분석 과정

몇 가지 문제를 생각 할 수

  1. 왜 직렬화 객체 만 구현해야 Serializable그것에 인터페이스를.
  2. 일반적으로 우리는 권장 연습 정적을 달성하기 위해 왜 클래스, 직렬화 final멤버 변수를 serialVersionUID.
  3. 직렬화 메커니즘은 어떻게 무시하는 것이다 transient, 정적 변수는 직렬화되지 않습니다 키워드.

우리가 문제를했다 다음으로, 소스 코드에서 답을 찾습니다.

직렬화

에서 봐 Serializable인터페이스는, 소스 코드는 빈 인터페이스는 방법없이 멤버 변수가없는, 매우 간단합니다. 그러나 매우 명확하게 설명,주의 사항을 자세히 Serializable, 무엇을 사용하는 방법, 그것은 가치를 방문, 우리는 몇 가지 번역 우선 순위를 데리러 보면,

/**
 * Serializability of a class is enabled by the class implementing the
 * java.io.Serializable interface. Classes that do not implement this
 * interface will not have any of their state serialized or
 * deserialized.  All subtypes of a serializable class are themselves
 * serializable.  The serialization interface has no methods or fields
 * and serves only to identify the semantics of being serializable. 
 */
复制代码

클래스가 직렬화함으로써 달성 java.io.Serializable인터페이스를 개방. 인터페이스 클래스는 직렬화 할 수없는 직렬화를 구현하지, 직렬화는 모든 서브 클래스가 직렬화 될 수있다 달성했다. Serializable인터페이스 메서드와 속성뿐만 아니라 클래스 식별 표시가 직렬화 될 수있다.

/**
 * Classes that require special handling during the serialization and
 * deserialization process must implement special methods with these exact
 * signatures:
 *
 * <PRE>
 * private void writeObject(java.io.ObjectOutputStream out)
 *     throws IOException
 * private void readObject(java.io.ObjectInputStream in)
 *     throws IOException, ClassNotFoundException;
 * private void readObjectNoData()
 *     throws ObjectStreamException;
 * </PRE>
 */
复制代码

클래스가 뭔가 특별한 치료를 할 경우 직렬화 과정에서 다음과 같은 방법에 의해 달성 될 수있다 writeObject(), readObject(), readObjectNoData(), 상기

  • writeObject 메소드가 대응되도록, 오브젝트의 특정 클래스의 상태를 기록 할 책임이 readObject()방법은 복원 될 수있다.
  • readObject()스트림에서 읽고 클래스 필드를 복구 할 책임이 방법.
  • 슈퍼 클래스는 직렬화를 지원하지 않습니다,하지만 우리가 수행하는 방법의 기본값을 사용하지 않으려면? writeReplace()오브젝트가 스트림 오브젝트 자체를 교체 기록되기 전에있어서 만들어 질 수있다.
  • readResolve()객체가 스트림으로부터 판독 할 때 일반적으로 하나의 실시 형태에서 사용하면 객체의 다른 목적으로 대체 될 수있다.

ObjectOutputStream에

    //我们要序列化对象的方法实现一般都是在这个函数中
    public final void writeObject(Object obj) throws IOException {
        ...
        try {
            //写入的具体实现方法
            writeObject0(obj, false);
        } catch (IOException ex) {
            ...
            throw ex;
        }
    }
    
    private void writeObject0(Object obj, boolean unshared) throws IOException {
        ...省略
        
        Object orig = obj;
            Class<?> cl = obj.getClass();
            ObjectStreamClass desc;
            for (;;) {
                // REMIND: skip this check for strings/arrays?
                Class<?> repCl;
                //获取到ObjectStreamClass,这个类很重要
                //在它的构造函数初始化时会调用获取类属性的函数
                //最终会调用getDefaultSerialFields这个方法
                //在其中通过flag过滤掉类的某一个为transient或static的属性(解释了问题3)
                desc = ObjectStreamClass.lookup(cl, true);
                if (!desc.hasWriteReplaceMethod() ||
                    (obj = desc.invokeWriteReplace(obj)) == null ||
                    (repCl = obj.getClass()) == cl)
                {
                    break;
                }
                cl = repCl;
        }
            
        //其中主要的写入逻辑如下
        //String, Array, Enum本身处理了序列化
        if (obj instanceof String) {
            writeString((String) obj, unshared);
        } else if (cl.isArray()) {
            writeArray(obj, desc, unshared);
        } else if (obj instanceof Enum) {
            writeEnum((Enum<?>) obj, desc, unshared);
            //重点在这里,通过`instanceof`判断对象是否为`Serializable`
            //这也就是普通自己定义的类如果没有实现`Serializable`
            //在序列化的时候会抛出异常的原因(解释了问题1)
        } else if (obj instanceof Serializable) {
            writeOrdinaryObject(obj, desc, unshared);
        } else {
            if (extendedDebugInfo) {
                throw new NotSerializableException(
                    cl.getName() + "\n" + debugInfoStack.toString());
            } else {
                throw new NotSerializableException(cl.getName());
            }
        }
        ...
    }

    private void writeOrdinaryObject(Object obj,
                                     ObjectStreamClass desc,
                                     boolean unshared)
        throws IOException
    {
        ...
        try {
            desc.checkSerialize();
            
            //写入二进制文件,普通对象开头的魔数0x73
            bout.writeByte(TC_OBJECT);
            //写入对应的类的描述符,见底下源码
            writeClassDesc(desc, false);
            
            handles.assign(unshared ? null : obj);
            if (desc.isExternalizable() && !desc.isProxy()) {
                writeExternalData((Externalizable) obj);
            } else {
                writeSerialData(obj, desc);
            }
        } finally {
            if (extendedDebugInfo) {
                debugInfoStack.pop();
            }
        }
    }
    
    private void writeClassDesc(ObjectStreamClass desc, boolean unshared)
        throws IOException
    {
        //句柄
        int handle;
        //null描述
        if (desc == null) {
            writeNull();
            //类对象引用句柄
            //如果流中已经存在句柄,则直接拿来用,提高序列化效率
        } else if (!unshared && (handle = handles.lookup(desc)) != -1) {
            writeHandle(handle);
            //动态代理类描述符
        } else if (desc.isProxy()) {
            writeProxyDesc(desc, unshared);
            //普通类描述符
        } else {
            //该方法会调用desc.writeNonProxy(this)如下
            writeNonProxyDesc(desc, unshared);
        }
    }
    
    void writeNonProxy(ObjectOutputStream out) throws IOException {
        out.writeUTF(name);
        //写入serialVersionUID
        out.writeLong(getSerialVersionUID());
        ...
    }
    
    public long getSerialVersionUID() {
        // 如果没有定义serialVersionUID
        // 序列化机制就会调用一个函数根据类内部的属性等计算出一个hash值
        // 这也是为什么不推荐序列化的时候不自己定义serialVersionUID的原因
        // 因为这个hash值是根据类的变化而变化的
        // 如果你新增了一个属性,那么之前那些被序列化后的二进制文件将不能反序列化回来,Java会抛出异常
        // (解释了问题2)
        if (suid == null) {
            suid = AccessController.doPrivileged(
                new PrivilegedAction<Long>() {
                    public Long run() {
                        return computeDefaultSUID(cl);
                    }
                }
            );
        }
        //已经定义了SerialVersionUID,直接获取
        return suid.longValue();
    }

    //分析到这里,要插一个我对序列化后二进制文件的一点个人见解,见下面
复制代码

바이너리 파일의 순서의 해석

우리가 순서를 원하는 경우의 List<PhoneItem>어떤 PhoneItem다음과 같이이다

class PhoneItem implements Serializable {
    String phoneNumber;
}
复制代码

구성의 코드 목록은 우리가 일련의 가정을 생략한다 size의 오 List, 진은 아마 다음에 표시된보기

7372 xxxx xxxx 
7371 xxxx xxxx 
7371 xxxx xxxx 
7371 xxxx xxxx 
7371 xxxx xxxx 
复制代码

단지 소스 의한 해석은에는 0x73의 시작 부분에 매직 번호 클래스 설명 71 참조 형식을 나타내고, 일반 오브젝트 (72)는 클래스 기술자를 나타냄. 함으로써, 바이너리 해석시 자비는 약간 얇은 참조 본 마법 번호 매칭 방법의 시작 (매직 넘버)는 자바 오브젝트로 변환 시키도록. 스트림이 동일한 목적을 갖는 경우 직렬화 프로세스는, 다음의 클래스 객체의 다음 순서가 직접 핸들을 취득 할 수있는 경우, 그것은하여 시퀀스의 효율을 개선하는, 참조 형식이된다.

    //通过writeSerialData调用走到真正解析类的方法中,有没有复写writeObject处理的逻辑不太一样
    //这里以默认没有复写writeObject为例,最后会调用defaultWriteFields方法
    private void defaultWriteFields(Object obj, ObjectStreamClass desc)
        throws IOException
    {
        ...
        int primDataSize = desc.getPrimDataSize();
        if (primVals == null || primVals.length < primDataSize) {
            primVals = new byte[primDataSize];
        }
        desc.getPrimFieldValues(obj, primVals);
        //写入属性大小
        bout.write(primVals, 0, primDataSize, false);

        ObjectStreamField[] fields = desc.getFields(false);
        Object[] objVals = new Object[desc.getNumObjFields()];
        int numPrimFields = fields.length - objVals.length;
        desc.getObjFieldValues(obj, objVals);
        for (int i = 0; i < objVals.length; i++) {
            ...
            try {
                //遍历写入属性类型和属性大小
                writeObject0(objVals[i],
                             fields[numPrimFields + i].isUnshared());
            } finally {
                if (extendedDebugInfo) {
                    debugInfoStack.pop();
                }
            }
        }
    }
复制代码

비슷한 직렬화 및 직렬화 프로세스를 들어, 생략한다.

컬렉션의 일반적인 문제 직렬화

HashMap의

자바는 이전 객체가 직렬화와 일관성을 직렬화 할 필요가 있지만, 객체 해시 계산에 의해 키 해시 맵 때문이다. 직렬화 한 후 (다른 환경에서 수행 직렬화 JVM) 상기 계산 된 값과 일치하지 않을 수. HashMap의 직렬화 프로세스가 다시 작성해야 그래서 이러한 불일치를 방지하기 위해, 달성하기 위해.

인 특정 작업은 속성 정의로 취급하기 때문에 transient, 복제 writeObject, 어떤 특별한 치료

    private void writeObject(java.io.ObjectOutputStream s)
        throws IOException {
        int buckets = capacity();
        // Write out the threshold, loadfactor, and any hidden stuff
        s.defaultWriteObject();
        //写入hash桶的容量
        s.writeInt(buckets);
        //写入k-v的大小
        s.writeInt(size);
        //遍历写入不为空的k-v
        internalWriteEntries(s);
    }
复制代码

ArrayList를

어레이 요소의 실제 수보다 실질적으로 클 수 있기 때문에 소자의 배열 순서를 방지하기 위해, 용량 ArrayList와 재기록되지 writeObjectreadObject

    private void writeObject(java.io.ObjectOutputStream s)
        throws java.io.IOException{
        ...
        s.defaultWriteObject();

        // 写入arraylist当前的大小
        s.writeInt(size);

        // 按照相同顺序写入元素
        for (int i=0; i<size; i++) {
            s.writeObject(elementData[i]);
        }
        ...
    }

复制代码

추천

출처juejin.im/post/5e211c54f265da3dfa49b3fc