diff a/src/java.base/share/classes/java/io/ObjectInputStream.java b/src/java.base/share/classes/java/io/ObjectInputStream.java
--- a/src/java.base/share/classes/java/io/ObjectInputStream.java
+++ b/src/java.base/share/classes/java/io/ObjectInputStream.java
@@ -24,35 +24,39 @@
  */
 
 package java.io;
 
 import java.io.ObjectInputFilter.Config;
-import java.io.ObjectStreamClass.RecordSupport;
+import java.io.ObjectStreamClass.ConstructorSupport;
+import java.io.ObjectStreamClass.ClassDataSlot;
 import java.lang.System.Logger;
 import java.lang.invoke.MethodHandle;
 import java.lang.reflect.Array;
 import java.lang.reflect.InvocationHandler;
+import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.Modifier;
 import java.lang.reflect.Proxy;
 import java.nio.charset.StandardCharsets;
 import java.security.AccessControlContext;
 import java.security.AccessController;
 import java.security.PrivilegedAction;
 import java.security.PrivilegedActionException;
 import java.security.PrivilegedExceptionAction;
 import java.util.Arrays;
-import java.util.Map;
+import java.util.List;
+import java.util.Locale;
 import java.util.Objects;
 
 import jdk.internal.access.JavaLangAccess;
 import jdk.internal.access.SharedSecrets;
 import jdk.internal.event.DeserializationEvent;
 import jdk.internal.misc.Unsafe;
 import jdk.internal.util.ByteArray;
 import sun.reflect.misc.ReflectUtil;
 import sun.security.action.GetBooleanAction;
 import sun.security.action.GetIntegerAction;
+import sun.security.action.GetPropertyAction;
 
 /**
  * An ObjectInputStream deserializes primitive data and objects previously
  * written using an ObjectOutputStream.
  *
@@ -217,10 +221,12 @@
  * form.  The methods of the Externalizable interface, writeExternal and
  * readExternal, are called to save and restore the objects state.  When
  * implemented by a class they can write and read their own state using all of
  * the methods of ObjectOutput and ObjectInput.  It is the responsibility of
  * the objects to handle any versioning that occurs.
+ * Value objects cannot be `java.io.Externalizable` because value objects are
+ * immutable and `Externalizable.readExternal` is unable to modify the fields of the value.
  *
  * <p>Enum constants are deserialized differently than ordinary serializable or
  * externalizable objects.  The serialized form of an enum constant consists
  * solely of its name; field values of the constant are not transmitted.  To
  * deserialize an enum constant, ObjectInputStream reads the constant name from
@@ -242,10 +248,14 @@
  * as readObject and writeObject, are ignored for serializable records. See
  * <a href="{@docRoot}/../specs/serialization/serial-arch.html#serialization-of-records">
  * <cite>Java Object Serialization Specification,</cite> Section 1.13,
  * "Serialization of Records"</a> for additional information.
  *
+ * <p>Value classes are {@linkplain Serializable} through the use of the serialization proxy pattern.
+ * See {@linkplain ObjectOutputStream##valueclass-serialization value class serialization} for details.
+ * When the proxy is deserialized it re-constructs and returns the value object.
+ *
  * @spec serialization/index.html Java Object Serialization Specification
  * @author      Mike Warres
  * @author      Roger Riggs
  * @see java.io.DataInput
  * @see java.io.ObjectOutputStream
@@ -255,10 +265,20 @@
  * @since   1.1
  */
 public class ObjectInputStream
     extends InputStream implements ObjectInput, ObjectStreamConstants
 {
+    private static final String TRACE_DEST =
+            GetPropertyAction.privilegedGetProperty("TRACE");
+
+    static void TRACE(String format, Object... args) {
+        if (TRACE_DEST != null) {
+            var ps = "OUT".equals(TRACE_DEST.toUpperCase(Locale.ROOT)) ? System.out : System.err;
+            ps.println(("TRACE " + format).formatted(args));
+        }
+    }
+
     /** handle value representing null */
     private static final int NULL_HANDLE = -1;
 
     /** marker for unshared objects in internal handle table */
     private static final Object unsharedMarker = new Object();
@@ -464,10 +484,18 @@
      *
      * <p>The deserialization filter, when not {@code null}, is invoked for
      * each object (regular or class) read to reconstruct the root object.
      * See {@link #setObjectInputFilter(ObjectInputFilter) setObjectInputFilter} for details.
      *
+     * <p>Serialization and deserialization of value classes is described in
+     * {@linkplain ObjectOutputStream##valueclass-serialization value class serialization}.
+     *
+     * @implSpec
+     * When enabled with {@code --enable-preview}, serialization and deserialization of
+     * Core Library value classes migrated from pre-JEP 401 identity classes is
+     * implementation specific.
+     *
      * <p>Exceptions are thrown for problems with the InputStream and for
      * classes that should not be deserialized.  All exceptions are fatal to
      * the InputStream and leave it in an indeterminate state; it is up to the
      * caller to ignore or recover the stream state.
      *
@@ -597,10 +625,13 @@
      *
      * <p>The deserialization filter, when not {@code null}, is invoked for
      * each object (regular or class) read to reconstruct the root object.
      * See {@link #setObjectInputFilter(ObjectInputFilter) setObjectInputFilter} for details.
      *
+     * <p>Serialization and deserialization of value classes is described in
+     * {@linkplain ObjectOutputStream##valueclass-serialization value class serialization}.
+     *
      * <p>ObjectInputStream subclasses which override this method can only be
      * constructed in security contexts possessing the
      * "enableSubclassImplementation" SerializablePermission; any attempt to
      * instantiate such a subclass without this permission will cause a
      * SecurityException to be thrown.
@@ -2250,71 +2281,134 @@
         if (cl == String.class || cl == Class.class
                 || cl == ObjectStreamClass.class) {
             throw new InvalidClassException("invalid class descriptor");
         }
 
-        Object obj;
-        try {
-            obj = desc.isInstantiable() ? desc.newInstance() : null;
-        } catch (Exception ex) {
-            throw new InvalidClassException(desc.forClass().getName(),
-                                            "unable to create instance", ex);
-        }
-
-        passHandle = handles.assign(unshared ? unsharedMarker : obj);
+        // Assign the handle and initially set to null or the unsharedMarker
+        passHandle = handles.assign(unshared ? unsharedMarker : null);
         ClassNotFoundException resolveEx = desc.getResolveException();
         if (resolveEx != null) {
             handles.markException(passHandle, resolveEx);
         }
 
-        final boolean isRecord = desc.isRecord();
-        if (isRecord) {
-            assert obj == null;
-            obj = readRecord(desc);
-            if (!unshared)
-                handles.setObject(passHandle, obj);
-        } else if (desc.isExternalizable()) {
-            readExternalData((Externalizable) obj, desc);
-        } else {
-            readSerialData(obj, desc);
-        }
+        try {
+            // Dispatch on the factory mode to read an object from the stream.
+            Object obj = switch (desc.factoryMode()) {
+                case READ_OBJECT_DEFAULT -> readSerialDefaultObject(desc, unshared);
+                case READ_OBJECT_CUSTOM -> readSerialCustomData(desc, unshared);
+                case READ_RECORD -> readRecord(desc, unshared);
+                case READ_EXTERNALIZABLE -> readExternalObject(desc, unshared);
+                case READ_OBJECT_VALUE -> readObjectValue(desc, unshared);
+                case READ_NO_LOCAL_CLASS -> readAbsentLocalClass(desc, unshared);
+                case null -> throw new AssertionError("Unknown factoryMode for: " + desc.getName(),
+                        resolveEx);
+            };
 
-        handles.finish(passHandle);
+            handles.finish(passHandle);
 
-        if (obj != null &&
-            handles.lookupException(passHandle) == null &&
-            desc.hasReadResolveMethod())
-        {
-            Object rep = desc.invokeReadResolve(obj);
-            if (unshared && rep.getClass().isArray()) {
-                rep = cloneArray(rep);
-            }
-            if (rep != obj) {
-                // Filter the replacement object
-                if (rep != null) {
-                    if (rep.getClass().isArray()) {
-                        filterCheck(rep.getClass(), Array.getLength(rep));
-                    } else {
-                        filterCheck(rep.getClass(), -1);
+            if (obj != null &&
+                handles.lookupException(passHandle) == null &&
+                desc.hasReadResolveMethod())
+            {
+                Object rep = desc.invokeReadResolve(obj);
+                if (unshared && rep.getClass().isArray()) {
+                    rep = cloneArray(rep);
+                }
+                if (rep != obj) {
+                    // Filter the replacement object
+                    if (rep != null) {
+                        if (rep.getClass().isArray()) {
+                            filterCheck(rep.getClass(), Array.getLength(rep));
+                        } else {
+                            filterCheck(rep.getClass(), -1);
+                        }
                     }
+                    handles.setObject(passHandle, obj = rep);
                 }
-                handles.setObject(passHandle, obj = rep);
+            }
+
+            return obj;
+        } catch (UncheckedIOException uioe) {
+            // Consistent re-throw for nested UncheckedIOExceptions
+            throw uioe.getCause();
+        }
+    }
+
+    /**
+     * {@return a value class instance by invoking its constructor with field values read from the stream.
+     * The fields of the class in the stream are matched to the local fields and applied to
+     * the constructor.
+     * If the stream contains superclasses with serializable fields,
+     * an InvalidClassException is thrown with an incompatible class change message.
+     *
+     * @param desc the class descriptor read from the stream, the local class is a value class
+     * @param unshared if the object is not to be shared
+     * @throws InvalidClassException if the stream contains a superclass with serializable fields.
+     * @throws IOException if there are I/O errors while reading from the
+     *         underlying {@code InputStream}
+     */
+    private Object readObjectValue(ObjectStreamClass desc, boolean unshared) throws IOException {
+        final ObjectStreamClass localDesc = desc.getLocalDesc();
+        TRACE("readObjectValue: %s, local class: %s", desc.getName(), localDesc.getName());
+        // Check for un-expected fields in superclasses
+        List<ClassDataSlot> slots = desc.getClassDataLayout();
+        for (int i = 0; i < slots.size()-1; i++) {
+            ClassDataSlot slot = slots.get(i);
+            if (slot.hasData && slot.desc.getFields(false).length > 0) {
+                throw new InvalidClassException("incompatible class change to value class: " +
+                        "stream class has non-empty super type: " + desc.getName());
             }
         }
+        // Read values for the value class fields
+        FieldValues fieldValues = new FieldValues(desc, true);
 
-        return obj;
+        // Get value object constructor adapted to take primitive value buffer and object array.
+        MethodHandle consMH = ConstructorSupport.deserializationValueCons(desc);
+        try {
+            Object obj = (Object) consMH.invokeExact(fieldValues.primValues, fieldValues.objValues);
+            if (!unshared)
+                handles.setObject(passHandle, obj);
+            return obj;
+        } catch (Exception e) {
+            throw new InvalidObjectException(e.getMessage(), e);
+        } catch (Error e) {
+            throw e;
+        } catch (Throwable t) {
+            throw new InvalidObjectException("ReflectiveOperationException " +
+                    "during deserialization", t);
+        }
     }
 
     /**
-     * If obj is non-null, reads externalizable data by invoking readExternal()
+     * Creates a new object and invokes its readExternal method to read its contents.
+     *
+     * If the class is instantiable, read externalizable data by invoking readExternal()
      * method of obj; otherwise, attempts to skip over externalizable data.
      * Expects that passHandle is set to obj's handle before this method is
-     * called.
+     * called.  The new object is entered in the handle table immediately,
+     * allowing it to leak before it is completely read.
      */
-    private void readExternalData(Externalizable obj, ObjectStreamClass desc)
+    private Object readExternalObject(ObjectStreamClass desc, boolean unshared)
         throws IOException
     {
+        TRACE("readExternalObject: %s", desc.getName());
+
+        // For Externalizable objects,
+        // create the instance, publish the ref, and read the data
+        Externalizable obj = null;
+        try {
+            if (desc.isInstantiable()) {
+                obj = (Externalizable) desc.newInstance();
+            }
+        } catch (Exception ex) {
+            throw new InvalidClassException(desc.getName(),
+                    "unable to create instance", ex);
+        }
+
+        if (!unshared)
+            handles.setObject(passHandle, obj);
+
         SerialCallbackContext oldContext = curContext;
         if (oldContext != null)
             oldContext.check();
         curContext = null;
         try {
@@ -2354,26 +2448,28 @@
          * we mimic the behavior of past serialization implementations and
          * blindly hope that the stream is in sync; if it isn't and additional
          * externalizable data remains in the stream, a subsequent read will
          * most likely throw a StreamCorruptedException.
          */
+        return obj;
     }
 
     /**
      * Reads and returns a record.
      * If an exception is marked for any of the fields, the dependency
      * mechanism marks the record as having an exception.
      * Null is returned from readRecord and later the exception is thrown at
      * the exit of {@link #readObject(Class)}.
-     **/
-    private Object readRecord(ObjectStreamClass desc) throws IOException {
-        ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
-        if (slots.length != 1) {
+     */
+    private Object readRecord(ObjectStreamClass desc, boolean unshared) throws IOException {
+        TRACE("invoking readRecord: %s", desc.getName());
+        List<ClassDataSlot> slots = desc.getClassDataLayout();
+        if (slots.size() != 1) {
             // skip any superclass stream field values
-            for (int i = 0; i < slots.length-1; i++) {
-                if (slots[i].hasData) {
-                    new FieldValues(slots[i].desc, true);
+            for (int i = 0; i < slots.size()-1; i++) {
+                if (slots.get(i).hasData) {
+                    new FieldValues(slots.get(i).desc, true);
                 }
             }
         }
 
         FieldValues fieldValues = new FieldValues(desc, true);
@@ -2383,14 +2479,17 @@
 
         // get canonical record constructor adapted to take two arguments:
         // - byte[] primValues
         // - Object[] objValues
         // and return Object
-        MethodHandle ctrMH = RecordSupport.deserializationCtr(desc);
+        MethodHandle ctrMH = ConstructorSupport.deserializationCtr(desc);
 
         try {
-            return (Object) ctrMH.invokeExact(fieldValues.primValues, fieldValues.objValues);
+            Object obj = (Object) ctrMH.invokeExact(fieldValues.primValues, fieldValues.objValues);
+            if (!unshared)
+                handles.setObject(passHandle, obj);
+            return obj;
         } catch (Exception e) {
             throw new InvalidObjectException(e.getMessage(), e);
         } catch (Error e) {
             throw e;
         } catch (Throwable t) {
@@ -2398,118 +2497,211 @@
                                              "during deserialization", t);
         }
     }
 
     /**
-     * Reads (or attempts to skip, if obj is null or is tagged with a
+     * Construct an object from the stream for a class that has only default read object behaviors.
+     * For each object, the fields are read before any are assigned.
+     * The new instance is entered in the handle table if it is unshared,
+     * allowing it to escape before it is initialized.
+     * The `readObject` and `readObjectNoData` methods are not present and are not called.
+     *
+     * @param desc the class descriptor
+     * @param unshared true if the object should be shared
+     * @return the object constructed from the stream data
+     * @throws IOException if there are I/O errors while reading from the
+     *         underlying {@code InputStream}
+     * @throws InvalidClassException if the instance creation fails
+     */
+    private Object readSerialDefaultObject(ObjectStreamClass desc, boolean unshared)
+            throws IOException, InvalidClassException {
+        if (!desc.isInstantiable()) {
+            // No local class to create, read and discard
+            return readAbsentLocalClass(desc, unshared);
+        }
+        TRACE("readSerialDefaultObject: %s", desc.getName());
+        try {
+            final Object obj = desc.newInstance();
+            if (!unshared)
+                handles.setObject(passHandle, obj);
+
+            // Best effort Failure Atomicity; slotValues will be non-null if field
+            // values can be set after reading all field data in the hierarchy.
+            List<FieldValues> slotValues = desc.getClassDataLayout().stream()
+                    .filter(s -> s.hasData)
+                    .map(s1 -> {
+                        var values = new FieldValues(s1.desc, true);
+                        finishBlockData(s1.desc);
+                        return values;
+                    })
+                    .toList();
+
+            if (handles.lookupException(passHandle) != null) {
+                return null;    // some exception for a class, do not return the object
+            }
+
+            // Check that the types are assignable for all slots before assigning.
+            slotValues.forEach(v -> v.defaultCheckFieldValues(obj));
+            slotValues.forEach(v -> v.defaultSetFieldValues(obj));
+            return obj;
+        } catch (InstantiationException | InvocationTargetException ex) {
+            throw new InvalidClassException(desc.forClass().getName(),
+                    "unable to create instance", ex);
+        }
+    }
+
+
+    /**
+     * Reads (or attempts to skip, if not instantiatable or is tagged with a
      * ClassNotFoundException) instance data for each serializable class of
-     * object in stream, from superclass to subclass.  Expects that passHandle
-     * is set to obj's handle before this method is called.
+     * object in stream, from superclass to subclass.
+     * Expects that passHandle is set to current handle before this method is called.
      */
-    private void readSerialData(Object obj, ObjectStreamClass desc)
+    private Object readSerialCustomData(ObjectStreamClass desc, boolean unshared)
         throws IOException
     {
-        ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();
-        // Best effort Failure Atomicity; slotValues will be non-null if field
-        // values can be set after reading all field data in the hierarchy.
-        // Field values can only be set after reading all data if there are no
-        // user observable methods in the hierarchy, readObject(NoData). The
-        // top most Serializable class in the hierarchy can be skipped.
-        FieldValues[] slotValues = null;
-
-        boolean hasSpecialReadMethod = false;
-        for (int i = 1; i < slots.length; i++) {
-            ObjectStreamClass slotDesc = slots[i].desc;
-            if (slotDesc.hasReadObjectMethod()
-                  || slotDesc.hasReadObjectNoDataMethod()) {
-                hasSpecialReadMethod = true;
-                break;
-            }
+        if (!desc.isInstantiable()) {
+            // No local class to create, read and discard
+            return readAbsentLocalClass(desc, unshared);
         }
-        // No special read methods, can store values and defer setting.
-        if (!hasSpecialReadMethod)
-            slotValues = new FieldValues[slots.length];
 
-        for (int i = 0; i < slots.length; i++) {
-            ObjectStreamClass slotDesc = slots[i].desc;
-
-            if (slots[i].hasData) {
-                if (obj == null || handles.lookupException(passHandle) != null) {
-                    // Read fields of the current descriptor into a new FieldValues and discard
-                    new FieldValues(slotDesc, true);
-                } else if (slotDesc.hasReadObjectMethod()) {
-                    SerialCallbackContext oldContext = curContext;
-                    if (oldContext != null)
-                        oldContext.check();
-                    try {
-                        curContext = new SerialCallbackContext(obj, slotDesc);
+        TRACE("readSerialCustomData: %s, ex: %s", desc.getName(), handles.lookupException(passHandle));
+        try {
+            Object obj = desc.newInstance();
+            if (!unshared)
+                handles.setObject(passHandle, obj);
+            // Read data into each of the slots for the class
+            return readSerialCustomSlots(obj, desc.getClassDataLayout());
+        } catch (InstantiationException | InvocationTargetException ex) {
+            throw new InvalidClassException(desc.forClass().getName(),
+                    "unable to create instance", ex);
+        }
+    }
 
-                        bin.setBlockDataMode(true);
-                        slotDesc.invokeReadObject(obj, this);
-                    } catch (ClassNotFoundException ex) {
-                        /*
-                         * In most cases, the handle table has already
-                         * propagated a CNFException to passHandle at this
-                         * point; this mark call is included to address cases
-                         * where the custom readObject method has cons'ed and
-                         * thrown a new CNFException of its own.
-                         */
-                        handles.markException(passHandle, ex);
-                    } finally {
-                        curContext.setUsed();
-                        if (oldContext!= null)
-                            oldContext.check();
-                        curContext = oldContext;
-                    }
+    /**
+     * Reads from the stream using custom or default readObject methods appropriate.
+     * For each slot, either the custom readObject method or the default reader of fields
+     * is invoked. Unused slot specific custom data is discarded.
+     * This function is used by {@link #readSerialCustomData}.
+     *
+     * @param obj the object to assign the values to
+     * @param slots a list of slots to read from the stream
+     * @return the object being initialized
+     * @throws IOException if there are I/O errors while reading from the
+     *         underlying {@code InputStream}
+     */
+    private Object readSerialCustomSlots(Object obj, List<ClassDataSlot> slots) throws IOException {
+        TRACE("    readSerialCustomSlots: %s", slots);
 
-                    /*
-                     * defaultDataEnd may have been set indirectly by custom
-                     * readObject() method when calling defaultReadObject() or
-                     * readFields(); clear it to restore normal read behavior.
-                     */
-                    defaultDataEnd = false;
+        for (ClassDataSlot slot : slots) {
+            ObjectStreamClass slotDesc = slot.desc;
+            if (slot.hasData) {
+                if (slotDesc.hasReadObjectMethod() &&
+                        handles.lookupException(passHandle) == null) {
+                    // Invoke slot custom readObject method
+                    readSlotViaReadObject(obj, slotDesc);
                 } else {
                     // Read fields of the current descriptor into a new FieldValues
                     FieldValues values = new FieldValues(slotDesc, true);
-                    if (slotValues != null) {
-                        slotValues[i] = values;
-                    } else if (obj != null) {
-                        if (handles.lookupException(passHandle) == null) {
-                            // passHandle NOT marked with an exception; set field values
-                            values.defaultCheckFieldValues(obj);
-                            values.defaultSetFieldValues(obj);
-                        }
+                    if (handles.lookupException(passHandle) == null) {
+                        // Set the instance fields if no previous exception
+                        values.defaultCheckFieldValues(obj);
+                        values.defaultSetFieldValues(obj);
                     }
-                }
-
-                if (slotDesc.hasWriteObjectData()) {
-                    skipCustomData();
-                } else {
-                    bin.setBlockDataMode(false);
+                    finishBlockData(slotDesc);
                 }
             } else {
-                if (obj != null &&
-                    slotDesc.hasReadObjectNoDataMethod() &&
-                    handles.lookupException(passHandle) == null)
-                {
+                if (slotDesc.hasReadObjectNoDataMethod() &&
+                        handles.lookupException(passHandle) == null) {
                     slotDesc.invokeReadObjectNoData(obj);
                 }
             }
         }
+        return obj;
+    }
 
-        if (obj != null && slotValues != null && handles.lookupException(passHandle) == null) {
-            // passHandle NOT marked with an exception
-            // Check that the non-primitive types are assignable for all slots
-            // before assigning.
-            for (int i = 0; i < slots.length; i++) {
-                if (slotValues[i] != null)
-                    slotValues[i].defaultCheckFieldValues(obj);
-            }
-            for (int i = 0; i < slots.length; i++) {
-                if (slotValues[i] != null)
-                    slotValues[i].defaultSetFieldValues(obj);
+    /**
+     * Invoke the readObject method of the class to read and store the state from the stream.
+     *
+     * @param obj an instance of the class being created, only partially initialized.
+     * @param slotDesc the ObjectStreamDescriptor for the current class
+     * @throws IOException if there are I/O errors while reading from the
+     *         underlying {@code InputStream}
+     */
+    private void readSlotViaReadObject(Object obj, ObjectStreamClass slotDesc) throws IOException {
+        TRACE("readSlotViaReadObject: %s", slotDesc.getName());
+        assert obj != null : "readSlotViaReadObject called when obj == null";
+
+        SerialCallbackContext oldContext = curContext;
+        if (oldContext != null)
+            oldContext.check();
+        try {
+            curContext = new SerialCallbackContext(obj, slotDesc);
+
+            bin.setBlockDataMode(true);
+            slotDesc.invokeReadObject(obj, this);
+        } catch (ClassNotFoundException ex) {
+            /*
+             * In most cases, the handle table has already
+             * propagated a CNFException to passHandle at this
+             * point; this mark call is included to address cases
+             * where the custom readObject method has cons'ed and
+             * thrown a new CNFException of its own.
+             */
+            handles.markException(passHandle, ex);
+        } finally {
+            curContext.setUsed();
+            if (oldContext!= null)
+                oldContext.check();
+            curContext = oldContext;
+        }
+
+        /*
+         * defaultDataEnd may have been set indirectly by custom
+         * readObject() method when calling defaultReadObject() or
+         * readFields(); clear it to restore normal read behavior.
+         */
+        defaultDataEnd = false;
+
+        finishBlockData(slotDesc);
+    }
+
+
+    /**
+     * Read and discard an entire object, leaving a null reference in the HandleTable.
+     * The descriptor of the class in the stream is used to read the fields from the stream.
+     * There is no instance in which to store the field values.
+     * Custom data following the fields of any slot is read and discarded.
+     * References to nested objects are read and retained in the
+     * handle table using the regular mechanism.
+     * Handles later in the stream may refer to the nested objects.
+     *
+     * @param desc the stream class descriptor
+     * @param unshared the unshared flag, ignored since no object is created
+     * @return null, no object is created
+     * @throws IOException if there are I/O errors while reading from the
+     *         underlying {@code InputStream}
+     */
+    private Object readAbsentLocalClass(ObjectStreamClass desc, boolean unshared)
+            throws IOException {
+        TRACE("readAbsentLocalClass: %s", desc.getName());
+        desc.getClassDataLayout().stream()
+                .filter(s -> s.hasData)
+                .forEach(s2 -> {new FieldValues(s2.desc, true); finishBlockData(s2.desc);});
+        return null;
+    }
+
+    // Finish handling of block data by skipping any remaining and setting BlockDataMode = false
+    private void finishBlockData(ObjectStreamClass slotDesc) throws UncheckedIOException {
+        try {
+            if (slotDesc.hasWriteObjectData()) {
+                skipCustomData();
+            } else {
+                bin.setBlockDataMode(false);
             }
+        } catch (IOException ioe) {
+            throw new UncheckedIOException(ioe);
         }
     }
 
     /**
      * Skips over all block data and objects until TC_ENDBLOCKDATA is
@@ -2601,36 +2793,42 @@
          * Creates FieldValues object for reading fields defined in given
          * class descriptor.
          * @param desc the ObjectStreamClass to read
          * @param recordDependencies if true, record the dependencies
          *                           from current PassHandle and the object's read.
+         * @throws UncheckedIOException if any IOException occurs
          */
-        FieldValues(ObjectStreamClass desc, boolean recordDependencies) throws IOException {
-            this.desc = desc;
+        FieldValues(ObjectStreamClass desc, boolean recordDependencies) throws UncheckedIOException {
+            try {
+                this.desc = desc;
+                TRACE("    reading FieldValues: %s", desc.getName());
+                int primDataSize = desc.getPrimDataSize();
+                primValues = (primDataSize > 0) ? new byte[primDataSize] : null;
+                if (primDataSize > 0) {
+                    bin.readFully(primValues, 0, primDataSize, false);
+                }
 
-            int primDataSize = desc.getPrimDataSize();
-            primValues = (primDataSize > 0) ? new byte[primDataSize] : null;
-            if (primDataSize > 0) {
-                bin.readFully(primValues, 0, primDataSize, false);
-            }
 
-            int numObjFields = desc.getNumObjFields();
-            objValues = (numObjFields > 0) ? new Object[numObjFields] : null;
-            objHandles = (numObjFields > 0) ? new int[numObjFields] : null;
-            if (numObjFields > 0) {
-                int objHandle = passHandle;
-                ObjectStreamField[] fields = desc.getFields(false);
-                int numPrimFields = fields.length - objValues.length;
-                for (int i = 0; i < objValues.length; i++) {
-                    ObjectStreamField f = fields[numPrimFields + i];
-                    objValues[i] = readObject0(Object.class, f.isUnshared());
-                    objHandles[i] = passHandle;
-                    if (recordDependencies && f.getField() != null) {
-                        handles.markDependency(objHandle, passHandle);
+                int numObjFields = desc.getNumObjFields();
+                objValues = (numObjFields > 0) ? new Object[numObjFields] : null;
+                objHandles = (numObjFields > 0) ? new int[numObjFields] : null;
+                if (numObjFields > 0) {
+                    int objHandle = passHandle;
+                    ObjectStreamField[] fields = desc.getFields(false);
+                    int numPrimFields = fields.length - objValues.length;
+                    for (int i = 0; i < objValues.length; i++) {
+                        ObjectStreamField f = fields[numPrimFields + i];
+                        objValues[i] = readObject0(Object.class, f.isUnshared());
+                        objHandles[i] = passHandle;
+                        if (recordDependencies && f.getField() != null) {
+                            handles.markDependency(objHandle, passHandle);
+                        }
                     }
+                    passHandle = objHandle;
                 }
-                passHandle = objHandle;
+            } catch (IOException ioe) {
+                throw new UncheckedIOException(ioe);
             }
         }
 
         public ObjectStreamClass getObjectStreamClass() {
             return desc;
