oracle/graal · error · RedefinitionException

SchemaChanged

SchemaChanged

Error message

class redefinition failed: attempted to change the schema (add/remove fields)

What it means

RedefinitionException(RedefinitionError.SchemaChanged) thrown during field-change detection under JVMTI restrictions when the replacement class has leftover new fields that are not merely accepted constant-value-attribute changes (newFieldsList.size() != acceptedChanges.numAcceptedFields). JVMTI redefinition forbids adding fields to the object schema.

Source

Thrown at espresso/src/com.oracle.truffle.espresso/src/com/oracle/truffle/espresso/redefinition/ClassRedefinition.java:632

                    // field pointer will have a changed type. Hence we should mark it as a new
                    // field.
                    Matcher matcher = InnerClassRedefiner.ANON_INNER_CLASS_PATTERN.matcher(oldField.getType().toString());
                    if (isPatched && matcher.matches()) {
                        break;
                    }
                    oldFieldsIt.remove();
                    newFieldsIt.remove();
                    break;
                }
            }
        }

        if (!newFieldsList.isEmpty()) {
            if (jvmtiRestrictions) {
                // only restrict is there's actual new fields, not only fields with constant value
                // attribute changes
                if (newFieldsList.size() != acceptedChanges.numAcceptedFields) {
                    throw new RedefinitionException(RedefinitionError.SchemaChanged);
                }
            }
            if (isPatched) {
                ParserField[] finalFields = finalParserKlass.getFields();
                // lookup the final new field based on the index in the parser field array
                for (ParserField parserField : newFieldsList) {
                    for (int i = 0; i < newFields.length; i++) {
                        if (parserField == newFields[i]) {
                            collectedChanges.addNewField(finalFields[i]);
                            break;
                        }
                    }
                }
            } else {
                collectedChanges.addNewFields(newFieldsList);
            }
            result = ClassChange.SCHEMA_CHANGE;
        }

View on GitHub (pinned to a66e9ccd1d)

Solutions

  1. Keep the field set unchanged for restricted hotswaps; only constant values of existing static final fields may differ.
  2. Move new state into a separate newly-loaded class (e.g. a side-table keyed by instance) if it must be introduced without restart.
  3. Restart the context or use unrestricted redefinition when fields must genuinely be added.

Example fix

// before: hotswap adds `private int cache;` to the class
// after: externalize new state
class CacheSideTable {  // new class, fresh-loaded
    static final Map<MyClass, Integer> CACHE = new WeakHashMap<>();
}
Defensive patterns

Strategy: validation

Validate before calling

// diff field sets (name+type) before hotswap
Set<String> added = fieldsOf(newBytes); added.removeAll(fieldsOf(oldBytes));
added.removeAll(constantValueChangedOnly(oldBytes, newBytes)); // accepted changes
if (!added.isEmpty()) {
    throw new UnsupportedOperationException("fields added, restart required: " + added);
}

Try / catch

catch (RedefinitionException e) {
    if (e.getError() == RedefinitionError.SchemaChanged) { /* restart context */ }
}

Prevention

When it happens

Trigger: Restricted hotswap where the new class declares fields the old class lacked — new instance or static fields, excluding the special-cased constant-value-only changes that are accepted.

Common situations: Adding a field to hold new state during development and hotswapping; Lombok/IDE generating new fields between saves; caching/memoization fields added by instrumentation agents.

Related errors


AI-assisted analysis of oracle/graal@a66e9ccd1d (2026-08-14). Data as JSON: /api/errors/1b253cc0f46be741. Report an issue: GitHub.