oracle/graal · error · RedefinitionException

ClassModifiersChanged

ClassModifiersChanged

Error message

class redefinition failed: attempted to change the class modifiers

What it means

RedefinitionException(RedefinitionError.ClassModifiersChanged) thrown under JVMTI restrictions when the class-level access/modifier flags differ between oldParserKlass and newParserKlass (e.g. public, final, abstract, interface bit changes). Without restrictions the change would just be classified as SCHEMA_CHANGE; with jvmtiRestrictions it is rejected outright.

Source

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

            result = ClassChange.SCHEMA_CHANGE;
        }

        if (!oldFieldsList.isEmpty()) {
            if (jvmtiRestrictions) {
                // only restrict is there's actual removed fields, not only fields with constant
                // value attribute changes
                if (oldFieldsList.size() != acceptedChanges.numAcceptedFields) {
                    throw new RedefinitionException(RedefinitionError.SchemaChanged);
                }
            }
            collectedChanges.addRemovedFields(oldFieldsList);
            result = ClassChange.SCHEMA_CHANGE;
        }

        // detect class-level changes
        if (newParserKlass.getFlags() != oldParserKlass.getFlags()) {
            if (jvmtiRestrictions) {
                throw new RedefinitionException(RedefinitionError.ClassModifiersChanged);
            }
            result = ClassChange.SCHEMA_CHANGE;
        }

        collectedChanges.addCompatibleFields(compatibleFields);

        // detect changes to superclass and implemented interfaces
        Klass superKlass = oldKlass.getSuperKlass();
        if (!newParserKlass.getSuperKlass().equals(oldParserKlass.getSuperKlass())) {
            if (jvmtiRestrictions) {
                throw new RedefinitionException(RedefinitionError.HierarchyChanged);
            }
            result = ClassChange.CLASS_HIERARCHY_CHANGED;
            superKlass = getLoadedKlass(newParserKlass.getSuperKlass(), oldKlass);
        }
        collectedChanges.addSuperKlass((ObjectKlass) superKlass);

        ObjectKlass[] newSuperInterfaces = oldKlass.getSuperInterfaces();

View on GitHub (pinned to a66e9ccd1d)

Solutions

  1. Emit replacement bytes with the identical class-level modifiers; restrict hotswap edits to method bodies and constant values.
  2. Apply modifier changes at restart, where the class is loaded anew.
  3. If live modifier changes are required, use Espresso's unrestricted redefinition mode.

Example fix

// before: `public class Foo` -> hotswap with `public final class Foo`
// after: keep `public class Foo` for the hotswap; add `final` on restart
Defensive patterns

Strategy: validation

Validate before calling

// compare class access flags (bytes offset 0..1 after constant pool via a parser)
int oldFlags = classFlagsOf(oldBytes);
int newFlags = classFlagsOf(newBytes);
if (oldFlags != newFlags) {
    throw new UnsupportedOperationException("class modifiers changed, restart required");
}

Try / catch

catch (RedefinitionException e) {
    if (e.getError() == RedefinitionError.ClassModifiersChanged) { /* restart or revert flags */ }
}

Prevention

When it happens

Trigger: Restricted hotswap where the replacement class file has different class-level flags: adding/removing final or abstract, changing visibility, or altering the interface bit.

Common situations: Making a class final (or un-finaling it to allow mocking/subclassing) and hotswapping; toggling abstract during a refactor; annotation-processing or AOP tools rewriting class flags.

Related errors


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