diff a/src/java.base/share/classes/java/io/ObjectStreamClass.java b/src/java.base/share/classes/java/io/ObjectStreamClass.java
--- a/src/java.base/share/classes/java/io/ObjectStreamClass.java
+++ b/src/java.base/share/classes/java/io/ObjectStreamClass.java
@@ -1,7 +1,7 @@
 /*
- * Copyright (c) 1996, 2023, Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 1996, 2024, Oracle and/or its affiliates. All rights reserved.
  * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
  *
  * This code is free software; you can redistribute it and/or modify it
  * under the terms of the GNU General Public License version 2 only, as
  * published by the Free Software Foundation.  Oracle designates this
@@ -28,10 +28,11 @@
 import java.lang.invoke.MethodHandle;
 import java.lang.invoke.MethodHandles;
 import java.lang.invoke.MethodType;
 import java.lang.reflect.Constructor;
 import java.lang.reflect.Field;
+import java.lang.reflect.InaccessibleObjectException;
 import java.lang.reflect.InvocationTargetException;
 import java.lang.reflect.RecordComponent;
 import java.lang.reflect.UndeclaredThrowableException;
 import java.lang.reflect.Member;
 import java.lang.reflect.Method;
@@ -50,32 +51,38 @@
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.stream.Stream;
 
+import jdk.internal.MigratedValueClass;
 import jdk.internal.event.SerializationMisdeclarationEvent;
 import jdk.internal.misc.Unsafe;
 import jdk.internal.reflect.CallerSensitive;
 import jdk.internal.reflect.Reflection;
 import jdk.internal.reflect.ReflectionFactory;
 import jdk.internal.access.SharedSecrets;
 import jdk.internal.access.JavaSecurityAccess;
 import jdk.internal.util.ByteArray;
+import jdk.internal.value.DeserializeConstructor;
 import sun.reflect.misc.ReflectUtil;
 
+import static java.io.ObjectInputStream.TRACE;
+
 /**
  * Serialization's descriptor for classes.  It contains the name and
  * serialVersionUID of the class.  The ObjectStreamClass for a specific class
  * loaded in this Java VM can be found/created using the lookup method.
  *
  * <p>The algorithm to compute the SerialVersionUID is described in
  * <a href="{@docRoot}/../specs/serialization/class.html#stream-unique-identifiers">
- *    <cite>Java Object Serialization Specification,</cite> Section 4.6, "Stream Unique Identifiers"</a>.
+ *    <cite>Java Object Serialization Specification</cite>, Section 4.6, "Stream Unique Identifiers"</a>.
  *
  * @spec serialization/index.html Java Object Serialization Specification
  * @author      Mike Warres
  * @author      Roger Riggs
  * @see ObjectStreamField
@@ -102,10 +109,75 @@
     @SuppressWarnings("removal")
     private static final ReflectionFactory reflFactory =
         AccessController.doPrivileged(
             new ReflectionFactory.GetReflectionFactoryAction());
 
+    /**
+     * The mode of deserialization for a class depending on its type and interfaces.
+     * The markers used are {@linkplain java.io.Serializable}, {@linkplain java.io.Externalizable},
+     * Class.isRecord(), Class.isValue(), constructors, and
+     * the presence of methods `readObject`, `writeObject`, `readObjectNoData`, `writeObject`.
+     * ObjectInputStream dispatches on the mode to construct objects from the stream.
+     */
+    enum DeserializationMode {
+        /**
+         * Construct an object from the stream for a class that has only default read object behaviors.
+         * All classes and superclasses use defaultReadObject; no custom readObject or readObjectNoData.
+         * The new instance is entered in the handle table if it is unshared,
+         * allowing it to escape before it is initialized.
+         * For each object, all the fields are read before any are assigned.
+         * The `readObject` and `readObjectNoData` methods are not present and are not called.
+         */
+        READ_OBJECT_DEFAULT,
+        /**
+         * 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.  The new object is entered in the handle table immediately,
+         * allowing it to leak before it is completely read.
+         */
+        READ_EXTERNALIZABLE,
+        /**
+         * Read all the record fields and invoke its canonical constructor.
+         * Construct the record using its canonical constructor.
+         * The new record is entered in the handle table only after the constructor returns.
+         */
+        READ_RECORD,
+        /**
+         * Fully custom read from the stream to create an instance.
+         * If the class is not instantiatable or is tagged with ClassNotFoundException
+         * the data in the stream for the class is read and discarded. {@link #READ_NO_LOCAL_CLASS}
+         * The instance is created and set in the handle table, allowing it to leak before it is initialized.
+         * For each serializable class in the stream, from superclass to subclass the
+         * stream values are read by the `readObject` method, if present, or defaultReadObject.
+         * Custom inline data is discarded if not consumed by the class `readObject` method.
+         */
+        READ_OBJECT_CUSTOM,
+        /**
+         * Construct an object by reading the values of all fields and
+         * invoking a constructor or static factory method.
+         * The constructor or static factory method is selected by matching its parameters with the
+         * sequence of field types of the serializable fields of the local class and superclasses.
+         * Invoke the constructor with all the values from the stream, inserting
+         * defaults and dropping extra values as necessary.
+         * This is very similar to the reading of records, except for the identification of
+         * the constructor or static factory.
+         */
+        READ_OBJECT_VALUE,
+        /**
+         * 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.
+         */
+        READ_NO_LOCAL_CLASS,
+    }
+
     private static class Caches {
         /** cache mapping local classes -> descriptors */
         static final ClassCache<ObjectStreamClass> localDescs =
             new ClassCache<>() {
                 @Override
@@ -135,10 +207,14 @@
     private boolean isProxy;
     /** true if represents enum type */
     private boolean isEnum;
     /** true if represents record type */
     private boolean isRecord;
+    /** true if represents a value class */
+    private boolean isValue;
+    /** The DeserializationMode for this class. */
+    private DeserializationMode factoryMode;
     /** true if represented class implements Serializable */
     private boolean serializable;
     /** true if represented class implements Externalizable */
     private boolean externalizable;
     /** true if desc has data written by class-defined writeObject method */
@@ -192,11 +268,11 @@
     /** number of non-primitive fields */
     private int numObjFields;
     /** reflector for setting/getting serializable field values */
     private FieldReflector fieldRefl;
     /** data layout of serialized objects described by this class desc */
-    private volatile ClassDataSlot[] dataLayout;
+    private volatile List<ClassDataSlot> dataLayout;
 
     /** serialization-appropriate constructor, or null if none */
     private Constructor<?> cons;
     /** record canonical constructor (shared among OSCs for same class), or null */
     private MethodHandle canonicalCtr;
@@ -374,10 +450,11 @@
         this.cl = cl;
         name = cl.getName();
         isProxy = Proxy.isProxyClass(cl);
         isEnum = Enum.class.isAssignableFrom(cl);
         isRecord = cl.isRecord();
+        isValue = cl.isValue();
         serializable = Serializable.class.isAssignableFrom(cl);
         externalizable = Externalizable.class.isAssignableFrom(cl);
 
         Class<?> superCl = cl.getSuperclass();
         superDesc = (superCl != null) ? lookup(superCl, false) : null;
@@ -405,14 +482,38 @@
                             new ExceptionInfo(e.classname, e.getMessage());
                         fields = NO_FIELDS;
                     }
 
                     if (isRecord) {
+                        factoryMode = DeserializationMode.READ_RECORD;
                         canonicalCtr = canonicalRecordCtr(cl);
                         deserializationCtrs = new DeserializationConstructorsCache();
                     } else if (externalizable) {
-                        cons = getExternalizableConstructor(cl);
+                        factoryMode = DeserializationMode.READ_EXTERNALIZABLE;
+                        if (cl.isIdentity()) {
+                            cons = getExternalizableConstructor(cl);
+                        } else {
+                            serializeEx = deserializeEx = new ExceptionInfo(cl.getName(),
+                                    "Externalizable not valid for value class");
+                        }
+                    } else if (cl.isValue()) {
+                        factoryMode = DeserializationMode.READ_OBJECT_VALUE;
+                        if (!cl.isAnnotationPresent(MigratedValueClass.class)) {
+                            serializeEx = deserializeEx = new ExceptionInfo(cl.getName(),
+                                    "Value class serialization is only supported with `writeReplace`");
+                        } else if (Modifier.isAbstract(cl.getModifiers())) {
+                            serializeEx = deserializeEx = new ExceptionInfo(cl.getName(),
+                                    "value class is abstract");
+                        } else {
+                            // Value classes should have constructor(s) annotated with {@link DeserializeConstructor}
+                            canonicalCtr = getDeserializingValueCons(cl, fields);
+                            deserializationCtrs = new DeserializationConstructorsCache();                            factoryMode = DeserializationMode.READ_OBJECT_VALUE;
+                            if (canonicalCtr == null) {
+                                serializeEx = deserializeEx = new ExceptionInfo(cl.getName(),
+                                        "no constructor or factory found for migrated value class");
+                            }
+                        }
                     } else {
                         cons = getSerializableConstructor(cl);
                         writeObjectMethod = getPrivateMethod(cl, "writeObject",
                             new Class<?>[] { ObjectOutputStream.class },
                             Void.TYPE);
@@ -420,10 +521,14 @@
                             new Class<?>[] { ObjectInputStream.class },
                             Void.TYPE);
                         readObjectNoDataMethod = getPrivateMethod(
                             cl, "readObjectNoData", null, Void.TYPE);
                         hasWriteObjectData = (writeObjectMethod != null);
+                        factoryMode = ((superDesc == null || superDesc.factoryMode() == DeserializationMode.READ_OBJECT_DEFAULT)
+                                && readObjectMethod == null && readObjectNoDataMethod == null)
+                                ? DeserializationMode.READ_OBJECT_DEFAULT
+                                : DeserializationMode.READ_OBJECT_CUSTOM;
                     }
                     domains = getProtectionDomains(cons, cl);
                     writeReplaceMethod = getInheritableMethod(
                         cl, "writeReplace", null, Object.class);
                     readResolveMethod = getInheritableMethod(
@@ -444,11 +549,11 @@
         }
 
         if (deserializeEx == null) {
             if (isEnum) {
                 deserializeEx = new ExceptionInfo(name, "enum type");
-            } else if (cons == null && !isRecord) {
+            } else if (cons == null && !(isRecord | isValue)) {
                 deserializeEx = new ExceptionInfo(name, "no valid constructor");
             }
         }
         if (isRecord && canonicalCtr == null) {
             deserializeEx = new ExceptionInfo(name, "record canonical constructor not found");
@@ -564,10 +669,13 @@
             writeReplaceMethod = localDesc.writeReplaceMethod;
             readResolveMethod = localDesc.readResolveMethod;
             deserializeEx = localDesc.deserializeEx;
             domains = localDesc.domains;
             cons = localDesc.cons;
+            factoryMode = localDesc.factoryMode;
+        } else {
+            factoryMode = DeserializationMode.READ_OBJECT_DEFAULT;
         }
         fieldRefl = getReflector(fields, localDesc);
         initialized = true;
     }
 
@@ -642,10 +750,11 @@
         numObjFields = model.numObjFields;
 
         if (osc != null) {
             localDesc = osc;
             isRecord = localDesc.isRecord;
+            isValue = localDesc.isValue;
             // canonical record constructor is shared
             canonicalCtr = localDesc.canonicalCtr;
             // cache of deserialization constructors is shared
             deserializationCtrs = localDesc.deserializationCtrs;
             writeObjectMethod = localDesc.writeObjectMethod;
@@ -657,10 +766,16 @@
                 deserializeEx = localDesc.deserializeEx;
             }
             domains = localDesc.domains;
             assert cl.isRecord() ? localDesc.cons == null : true;
             cons = localDesc.cons;
+            factoryMode = localDesc.factoryMode;
+        } else {
+            // No local class, read data using only the schema from the stream
+            factoryMode = (externalizable)
+                    ? DeserializationMode.READ_EXTERNALIZABLE
+                    : DeserializationMode.READ_NO_LOCAL_CLASS;
         }
 
         fieldRefl = getReflector(fields, localDesc);
         // reassign to matched fields so as to reflect local unshared settings
         fields = fieldRefl.getFields();
@@ -712,11 +827,11 @@
             char tcode = (char) in.readByte();
             String fname = in.readUTF();
             String signature = ((tcode == 'L') || (tcode == '[')) ?
                 in.readTypeString() : String.valueOf(tcode);
             try {
-                fields[i] = new ObjectStreamField(fname, signature, false);
+                fields[i] = new ObjectStreamField(fname, signature, false, -1);
             } catch (RuntimeException e) {
                 throw new InvalidClassException(name,
                                                 "invalid descriptor for field " +
                                                 fname, e);
             }
@@ -923,10 +1038,26 @@
     boolean isSerializable() {
         requireInitialized();
         return serializable;
     }
 
+    /**
+     * {@return {code true} if the class is a value class, {@code false} otherwise}
+     */
+    boolean isValue() {
+        requireInitialized();
+        return isValue;
+    }
+
+    /**
+     * {@return the factory mode for deserialization}
+     */
+    DeserializationMode factoryMode() {
+        requireInitialized();
+        return factoryMode;
+    }
+
     /**
      * Returns true if class descriptor represents externalizable class that
      * has written its data in 1.2 (block data) format, false otherwise.
      */
     boolean hasBlockExternalData() {
@@ -945,17 +1076,19 @@
     }
 
     /**
      * Returns true if represented class is serializable/externalizable and can
      * be instantiated by the serialization runtime--i.e., if it is
-     * externalizable and defines a public no-arg constructor, or if it is
+     * externalizable and defines a public no-arg constructor, if it is
      * non-externalizable and its first non-serializable superclass defines an
-     * accessible no-arg constructor.  Otherwise, returns false.
+     * accessible no-arg constructor, or if the class is a value class with a @DeserializeConstructor
+     * constructor or static factory.
+     * Otherwise, returns false.
      */
     boolean isInstantiable() {
         requireInitialized();
-        return (cons != null);
+        return (cons != null || (isValue() && canonicalCtr != null));
     }
 
     /**
      * Returns true if represented class is serializable (but not
      * externalizable) and defines a conformant writeObject method.  Otherwise,
@@ -1237,27 +1370,22 @@
             this.hasData = hasData;
         }
     }
 
     /**
-     * Returns array of ClassDataSlot instances representing the data layout
+     * Returns a List of ClassDataSlot instances representing the data layout
      * (including superclass data) for serialized objects described by this
      * class descriptor.  ClassDataSlots are ordered by inheritance with those
      * containing "higher" superclasses appearing first.  The final
      * ClassDataSlot contains a reference to this descriptor.
      */
-    ClassDataSlot[] getClassDataLayout() throws InvalidClassException {
+    List<ClassDataSlot> getClassDataLayout() throws InvalidClassException {
         // REMIND: synchronize instead of relying on volatile?
-        if (dataLayout == null) {
-            dataLayout = getClassDataLayout0();
-        }
-        return dataLayout;
-    }
+        List<ClassDataSlot> layout = dataLayout;
+        if (layout != null)
+            return layout;
 
-    private ClassDataSlot[] getClassDataLayout0()
-        throws InvalidClassException
-    {
         ArrayList<ClassDataSlot> slots = new ArrayList<>();
         Class<?> start = cl, end = cl;
 
         // locate closest non-serializable superclass
         while (end != null && Serializable.class.isAssignableFrom(end)) {
@@ -1302,11 +1430,12 @@
                 ObjectStreamClass.lookup(c, true), false));
         }
 
         // order slots from superclass -> subclass
         Collections.reverse(slots);
-        return slots.toArray(new ClassDataSlot[slots.size()]);
+        dataLayout = slots;
+        return slots;
     }
 
     /**
      * Returns aggregate size (in bytes) of marshalled primitive field values
      * for represented class.
@@ -1430,10 +1559,70 @@
             desc.initNonProxy(this, cl, null, superDesc);
         }
         return desc;
     }
 
+    /**
+     * Return a method handle for the static method or constructor(s) that matches the
+     * serializable fields and annotated with {@link DeserializeConstructor}.
+     * The descriptor for the class is still being initialized, so is passed the fields needed.
+     * @param clazz The class to query
+     * @param fields the serializable fields of the class
+     * @return a MethodHandle, null if none found
+     */
+    @SuppressWarnings("unchecked")
+    private static MethodHandle getDeserializingValueCons(Class<?> clazz,
+                                                          ObjectStreamField[] fields) {
+        // Search for annotated static factory in methods or constructors
+        MethodHandles.Lookup lookup = MethodHandles.lookup();
+        MethodHandle mh = Stream.concat(
+                Arrays.stream(clazz.getDeclaredMethods()).filter(m -> Modifier.isStatic(m.getModifiers())),
+                Arrays.stream(clazz.getDeclaredConstructors()))
+                .filter(m -> m.isAnnotationPresent(DeserializeConstructor.class))
+                .map(m -> {
+                    try {
+                        m.setAccessible(true);
+                        return (m instanceof Constructor<?> cons)
+                                ? lookup.unreflectConstructor(cons)
+                                : lookup.unreflect(((Method) m));
+                    } catch (IllegalAccessException iae) {
+                        throw new InternalError(iae);   // should not occur after setAccessible
+                    }})
+                .filter(m -> matchFactoryParamTypes(clazz, m, fields))
+                .findFirst().orElse(null);
+        TRACE("DeserializeConstructor for %s, mh: %s", clazz,  mh);
+        return mh;
+    }
+
+    /**
+     * Check that the parameters of the factory method match the fields of this class.
+     *
+     * @param mh a MethodHandle for a constructor or factory
+     * @return true if all fields match the parameters, false if not
+     */
+    private static boolean matchFactoryParamTypes(Class<?> clazz,
+                                                       MethodHandle mh,
+                                                       ObjectStreamField[] fields) {
+        TRACE("  matchFactoryParams checking class: %s, mh: %s", clazz, mh);
+        var params = mh.type().parameterList();
+        if (params.size() != fields.length) {
+            TRACE("   matchFactoryParams %s, arg count mismatch %d params != %d fields",
+                    clazz, params.size(), fields.length);
+            return false;    // Mismatch in count of fields and parameters
+        }
+        for (ObjectStreamField field : fields) {
+            int argIndex = field.getArgIndex();
+            final Class<?> paramtype = params.get(argIndex);
+            if (!field.getType().equals(paramtype)) {
+                TRACE("   matchFactoryParams %s: argIndex: %d type mismatch field: %s != param: %s",
+                        clazz, argIndex, field.getType(), paramtype);
+                return false;
+            }
+        }
+        return true;
+    }
+
     /**
      * Returns public no-arg constructor of given class, or null if none found.
      * Access checks are disabled on the returned constructor (if any), since
      * the defining class may still be non-public.
      */
@@ -1441,11 +1630,11 @@
         try {
             Constructor<?> cons = cl.getDeclaredConstructor((Class<?>[]) null);
             cons.setAccessible(true);
             return ((cons.getModifiers() & Modifier.PUBLIC) != 0) ?
                 cons : null;
-        } catch (NoSuchMethodException ex) {
+        } catch (NoSuchMethodException | InaccessibleObjectException ex) {
             return null;
         }
     }
 
     /**
@@ -1676,18 +1865,16 @@
             try {
                 Field f = cl.getDeclaredField(fname);
                 if ((f.getType() == spf.getType()) &&
                     ((f.getModifiers() & Modifier.STATIC) == 0))
                 {
-                    boundFields[i] =
-                        new ObjectStreamField(f, spf.isUnshared(), true);
+                    boundFields[i] = new ObjectStreamField(f, spf.isUnshared(), true, i);
                 }
             } catch (NoSuchFieldException ex) {
             }
             if (boundFields[i] == null) {
-                boundFields[i] = new ObjectStreamField(
-                    fname, spf.getType(), spf.isUnshared());
+                boundFields[i] = new ObjectStreamField(fname, spf.getType(), spf.isUnshared(), i);
             }
         }
         return boundFields;
     }
 
@@ -1700,13 +1887,13 @@
     private static ObjectStreamField[] getDefaultSerialFields(Class<?> cl) {
         Field[] clFields = cl.getDeclaredFields();
         ArrayList<ObjectStreamField> list = new ArrayList<>();
         int mask = Modifier.STATIC | Modifier.TRANSIENT;
 
-        for (int i = 0; i < clFields.length; i++) {
+        for (int i = 0, argIndex = 0; i < clFields.length; i++) {
             if ((clFields[i].getModifiers() & mask) == 0) {
-                list.add(new ObjectStreamField(clFields[i], false, true));
+                list.add(new ObjectStreamField(clFields[i], false, true, argIndex++));
             }
         }
         int size = list.size();
         return (size == 0) ? NO_FIELDS :
             list.toArray(new ObjectStreamField[size]);
@@ -2054,12 +2241,16 @@
             /* assuming checkDefaultSerialize() has been called on the class
              * descriptor this FieldReflector was obtained from, no field keys
              * in array should be equal to Unsafe.INVALID_FIELD_OFFSET.
              */
             for (int i = numPrimFields; i < fields.length; i++) {
+                Field f = fields[i].getField();
                 vals[offsets[i]] = switch (typeCodes[i]) {
-                    case 'L', '[' -> UNSAFE.getReference(obj, readKeys[i]);
+                    case 'L', '[' ->
+                            UNSAFE.isFlatField(f)
+                                    ? UNSAFE.getValue(obj, readKeys[i], f.getType())
+                                    : UNSAFE.getReference(obj, readKeys[i]);
                     default       -> throw new InternalError();
                 };
             }
         }
 
@@ -2082,35 +2273,40 @@
         void setObjFieldValues(Object obj, Object[] vals) {
             setObjFieldValues(obj, vals, false);
         }
 
         private void setObjFieldValues(Object obj, Object[] vals, boolean dryRun) {
-            if (obj == null) {
+            if (obj == null && !dryRun) {
                 throw new NullPointerException();
             }
             for (int i = numPrimFields; i < fields.length; i++) {
                 long key = writeKeys[i];
                 if (key == Unsafe.INVALID_FIELD_OFFSET) {
                     continue;           // discard value
                 }
                 switch (typeCodes[i]) {
                     case 'L', '[' -> {
+                        Field f = fields[i].getField();
                         Object val = vals[offsets[i]];
                         if (val != null &&
                             !types[i - numPrimFields].isInstance(val))
                         {
-                            Field f = fields[i].getField();
                             throw new ClassCastException(
                                 "cannot assign instance of " +
                                 val.getClass().getName() + " to field " +
                                 f.getDeclaringClass().getName() + "." +
                                 f.getName() + " of type " +
                                 f.getType().getName() + " in instance of " +
                                 obj.getClass().getName());
                         }
-                        if (!dryRun)
-                            UNSAFE.putReference(obj, key, val);
+                        if (!dryRun) {
+                            if (UNSAFE.isFlatField(f)) {
+                                UNSAFE.putValue(obj, key, f.getType(), val);
+                            } else {
+                                UNSAFE.putReference(obj, key, val);
+                            }
+                        }
                     }
                     default -> throw new InternalError();
                 }
             }
         }
@@ -2218,20 +2414,20 @@
                         throw new InvalidClassException(localDesc.name,
                             "incompatible types for field " + f.getName());
                     }
                     if (lf.getField() != null) {
                         m = new ObjectStreamField(
-                            lf.getField(), lf.isUnshared(), false);
+                            lf.getField(), lf.isUnshared(), true, lf.getArgIndex()); // Don't hide type
                     } else {
                         m = new ObjectStreamField(
-                            lf.getName(), lf.getSignature(), lf.isUnshared());
+                            lf.getName(), lf.getSignature(), lf.isUnshared(), lf.getArgIndex());
                     }
                 }
             }
             if (m == null) {
                 m = new ObjectStreamField(
-                    f.getName(), f.getSignature(), false);
+                    f.getName(), f.getSignature(), false, -1);
             }
             m.setOffset(f.getOffset());
             matches[i] = m;
         }
         return matches;
@@ -2349,11 +2545,11 @@
             }
         }
     }
 
     /** Record specific support for retrieving and binding stream field values. */
-    static final class RecordSupport {
+    static final class ConstructorSupport {
         /**
          * Returns canonical record constructor adapted to take two arguments:
          * {@code (byte[] primValues, Object[] objValues)}
          * and return
          * {@code Object}
@@ -2406,10 +2602,69 @@
             // store it into cache and return the 1st value stored
             return desc.deserializationCtr =
                 desc.deserializationCtrs.putIfAbsentAndGet(desc.getFields(false), mh);
         }
 
+        /**
+         * Returns value object constructor adapted to take two arguments:
+         * {@code (byte[] primValues, Object[] objValues)} and return {@code Object}
+         */
+        static MethodHandle deserializationValueCons(ObjectStreamClass desc) {
+            // check the cached value 1st
+            MethodHandle mh = desc.deserializationCtr;
+            if (mh != null) return mh;
+            mh = desc.deserializationCtrs.get(desc.getFields(false));
+            if (mh != null) return desc.deserializationCtr = mh;
+
+            // retrieve the selected constructor
+            // (T1, T2, ..., Tn):TR
+            ObjectStreamClass localDesc = desc.localDesc;
+            mh = localDesc.canonicalCtr;
+            MethodType mt = mh.type();
+
+            // change return type to Object
+            // (T1, T2, ..., Tn):TR -> (T1, T2, ..., Tn):Object
+            mh = mh.asType(mh.type().changeReturnType(Object.class));
+
+            // drop last 2 arguments representing primValues and objValues arrays
+            // (T1, T2, ..., Tn):Object -> (T1, T2, ..., Tn, byte[], Object[]):Object
+            mh = MethodHandles.dropArguments(mh, mh.type().parameterCount(), byte[].class, Object[].class);
+
+            Class<?>[] params = mt.parameterArray();
+            for (int i = params.length-1; i >= 0; i--) {
+                // Get the name from the local descriptor matching the argIndex
+                var field = getFieldForArgIndex(localDesc, i);
+                String name = (field == null) ? "" : field.getName();   // empty string to supply default
+                Class<?> type = params[i];
+                // obtain stream field extractor that extracts argument at
+                // position i (Ti+1) from primValues and objValues arrays
+                // (byte[], Object[]):Ti+1
+                MethodHandle combiner = streamFieldExtractor(name, type, desc);
+                // fold byte[] privValues and Object[] objValues into argument at position i (Ti+1)
+                // (..., Ti, Ti+1, byte[], Object[]):Object -> (..., Ti, byte[], Object[]):Object
+                mh = MethodHandles.foldArguments(mh, i, combiner);
+            }
+            // what we are left with is a MethodHandle taking just the primValues
+            // and objValues arrays and returning the constructed instance
+            // (byte[], Object[]):Object
+
+            // store it into cache and return the 1st value stored
+            return desc.deserializationCtr =
+                desc.deserializationCtrs.putIfAbsentAndGet(desc.getFields(false), mh);
+        }
+
+        // Find the ObjectStreamField for the argument index, otherwise null
+        private static ObjectStreamField getFieldForArgIndex(ObjectStreamClass desc, int argIndex) {
+            for (var field : desc.fields) {
+                if (field.getArgIndex() == argIndex)
+                    return field;
+            }
+            TRACE("field for ArgIndex is null: %s, index: %d, fields: %s",
+                    desc, argIndex, Arrays.toString(desc.fields));
+            return null;
+        }
+
         /** Returns the number of primitive fields for the given descriptor. */
         private static int numberPrimValues(ObjectStreamClass desc) {
             ObjectStreamField[] fields = desc.getFields();
             int primValueCount = 0;
             for (int i = 0; i < fields.length; i++) {
