Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

HCR Redefinition of empty constructors gets ignored #19883

Closed
IBMJimmyk opened this issue Jul 19, 2024 · 17 comments · Fixed by #20073
Closed

HCR Redefinition of empty constructors gets ignored #19883

IBMJimmyk opened this issue Jul 19, 2024 · 17 comments · Fixed by #20073

Comments

@IBMJimmyk
Copy link
Contributor

If an object has an empty constructor, it gets skipped over and its super constructor is called instead. This is a problem if HCR kicks in and replaces the empty constructor with a non-empty constructor. I talked to @jdmpapin and he helped me track down this problem.

The test case looks like this:

import java.io.*;
import java.lang.instrument.*;

public class RedefEmptyInit {

    public static void main (String[] args) {
        RedefEmptyInit t = new RedefEmptyInit();

        System.out.println("Running warmup.");
        for (int i = 0; i < 100; i++) {
            t.runTest();
        }

        int result = t.runTest();
        System.out.printf("Before redefine Result: %d, Expected: 0\n", result);

        try {
            File classFile = new File("InitObject-Redef.class");
            byte[] byteArray = new byte[(int)classFile.length()];
            FileInputStream inputStream = new FileInputStream(classFile);
            inputStream.read(byteArray);
            RedefAgent.instru.redefineClasses(new ClassDefinition(InitObject.class, byteArray));
        } catch (Exception e) {
            System.out.println("***FAILED*** - Unable to set up testing.");
            return;
        }

        result = t.runTest();
        System.out.printf("After redefine Result: %d, Expected: 1\n", result);

        if (1 != result) {
            System.out.println("***FAILED*** - Unexpected result.");
        } else {
            System.out.println("PASSED");
        }
    }

    RedefEmptyInit() {
    }

    protected int runTest() {
        InitObject testObj = new InitObject();

        return testObj.intField;
    }
}

runTest creates a new object of type InitObject, reads and returns the value of intField.

public class InitObject {

    public int intField;

    InitObject() {
        //intField = 0;
    }

}

With the line commented out, the constructor is empty and the error will occur. If it is uncommented, the error with not happen. Either way, the value of intField is 0.

The redefined version looks like this:

public class InitObject {

    public int intField;

    InitObject() {
        intField = 1;
    }

}

The value of intField is now 1 after the redefinition.

What the test does is create an object of type InitObject and check that the value of intField is 0. It then redefines the InitObject class so that intField is now 1. It then creates a new object to confirm that.

A failed result looks like this:

java -javaagent:RedefAgent.jar -Xint RedefEmptyInit
Running warmup.
Before redefine Result: 0, Expected: 0
After redefine Result: 0, Expected: 1
***FAILED*** - Unexpected result.

java -javaagent:RedefAgent.jar -Xjit:"optlevel=warm,count=10,disableasynccompilation,limit={*runTest*}" -Xshareclasses:none -Xnoaot RedefEmptyInit
Running warmup.
Before redefine Result: 0, Expected: 0
After redefine Result: 0, Expected: 1
***FAILED*** - Unexpected result.

Both Xint and JIT enabled fail to pick up the redefined class so after the redefinition it still reads intField as 0.

In the JIT'd code, the compiled method looks like this:

n1n       BBStart <block_2>                                                                   [    0x7c8d9cb546a0] bci=[-1,0,42] rc=0 vc=0 vn=- li=- udi=- nc=0
n5n       treetop                                                                             [    0x7c8d9cb547e0] bci=[-1,0,42] rc=0 vc=0 vn=- li=- udi=- nc=1
n4n         new  jitNewObject[#91  helper Method] [flags 0x400 0x0 ]                          [    0x7c8d9cb54790] bci=[-1,0,42] rc=3 vc=0 vn=- li=- udi=- nc=1
n3n           loadaddr  InitObject[#404  Static] [flags 0x18307 0x0 ]                         [    0x7c8d9cb54740] bci=[-1,0,42] rc=1 vc=0 vn=- li=- udi=- nc=0
n6n       allocationFence on n4n (X==0 X>=0 X<=0 )                                            [    0x7c8d9cb54830] bci=[-1,0,42] rc=0 vc=0 vn=- li=- udi=- nc=0 flg=0x302
n8n       NULLCHK on n4n [#32]                                                                [    0x7c8d9cb548d0] bci=[-1,4,42] rc=0 vc=0 vn=- li=- udi=- nc=1
n7n         call  java/lang/Object.<init>()V[#406  special Method] [flags 0x500 0x0 ]         [    0x7c8d9cb54880] bci=[-1,4,42] rc=1 vc=0 vn=- li=- udi=- nc=1
n4n           ==>new
n9n       astore  <auto slot 1>[#407  Auto] [flags 0x7 0x0 ]                                  [    0x7c8d9cb54920] bci=[-1,7,42] rc=0 vc=0 vn=- li=- udi=- nc=1
n4n         ==>new
n12n      NULLCHK on n10n [#32]                                                               [    0x7c8d9cb54a10] bci=[-1,9,44] rc=0 vc=0 vn=- li=- udi=- nc=1
n11n        iloadi  InitObject.intField I[#408  Shadow +8] [flags 0x603 0x0 ]                 [    0x7c8d9cb549c0] bci=[-1,9,44] rc=2 vc=0 vn=- li=- udi=- nc=1
n10n          aload  <auto slot 1>[#407  Auto] [flags 0x7 0x0 ]                               [    0x7c8d9cb54970] bci=[-1,8,44] rc=1 vc=0 vn=- li=- udi=- nc=0
n13n      ireturn                                                                             [    0x7c8d9cb54a60] bci=[-1,12,44] rc=0 vc=0 vn=- li=- udi=- nc=1
n11n        ==>iloadi
n2n       BBEnd </block_2>                                                                    [    0x7c8d9cb546f0] bci=[-1,12,44] rc=0 vc=0 vn=- li=- udi=- nc=0

Note how the constructor is a call to java/lang/Object.<init>()V with no guard to check for redefinition.

If the line is uncommented in the pre-redefined InitObject class, the test will pick up the redefinition and pass:

java -javaagent:RedefAgent.jar -Xint RedefEmptyInit
Running warmup.
Before redefine Result: 0, Expected: 0
After redefine Result: 1, Expected: 1
PASSED

java -javaagent:RedefAgent.jar -Xjit:"optlevel=warm,count=10,disableasynccompilation,limit={*runTest*}" -Xshareclasses:none -Xnoaot RedefEmptyInit
Running warmup.
Before redefine Result: 0, Expected: 0
After redefine Result: 1, Expected: 1
PASSED

The cause of the problem appears to be forwarder methods.

lookupOptions |= (J9_LOOK_VIRTUAL | J9_LOOK_ALLOW_FWD | J9_LOOK_HANDLE_DEFAULT_METHOD_CONFLICTS);

If J9_LOOK_ALLOW_FWD is removed there, the test will also pass.

Enabling extended HCR will cause the test to pass under Xint but not with the JIT:

java -javaagent:RedefAgent.jar -XX:+EnableExtendedHCR -Xint RedefEmptyInit
Running warmup.
Before redefine Result: 0, Expected: 0
After redefine Result: 1, Expected: 1
PASSED

java -javaagent:RedefAgent.jar -XX:+EnableExtendedHCR -Xjit:"optlevel=warm,count=10,disableasynccompilation,limit={*runTest*}" -Xshareclasses:none -Xnoaot RedefEmptyInit
Running warmup.
Before redefine Result: 0, Expected: 0
After redefine Result: 0, Expected: 1
***FAILED*** - Unexpected result.

Running instructions:
Download: RedefEmptyInit.zip
It contains 5 files:

RedefAgent.jar - Agent to help with redefinition
RedefEmptyInit.java - Test case
InitObject.java - Class to be redefined
InitObject-Redef.class - Class file to use with redefinition
InitObject-Redef.java - Source code of class file to use for redefinition

Commands:

javac -cp RedefAgent.jar InitObject.java RedefEmptyInit.java

java -javaagent:RedefAgent.jar -Xint RedefEmptyInit
java -javaagent:RedefAgent.jar -Xjit:"optlevel=warm,count=10,disableasynccompilation,limit={*runTest*}" -Xshareclasses:none -Xnoaot RedefEmptyInit

This was run on ppcle JDK11 but the problem should be common.

@IBMJimmyk IBMJimmyk changed the title Redefinition of empty constructors get ignored. HCR Redefinition of empty constructors gets ignored Jul 19, 2024
@jdmpapin
Copy link
Contributor

Enabling extended HCR will cause the test to pass under Xint

I think this is because with extended HCR the redefinition will unresolve constant pool entries related to the redefined class

@gacholio
Copy link
Contributor

I suspect this issue is not limited to constructors - we use the forwarding option for all invokespecial targets. While it would be foolish to have forwarder methods in general (the forwarding constructors are added by the compiler, I believe), it's certainly not illegal.

Two obvious options occur:

  1. Remove the optimization - if the JIT is doing its job, this would only affect the interpreter.

  2. Re-resolve invokespecial constant pool entries after normal HCR.

@jdmpapin
Copy link
Contributor

(1) would fix the problem for both the interpreter and the JIT

(2) would fix the interpreter only, but the JIT would be straightforward. We'd just have to avoid J9_LOOK_ALLOW_FWD when jitCompileTimeResolve is true. (If we want we could still allow forwarding if HCR is disabled or we're in debug mode, since redefinition in debug mode will discard the entire code cache)

I don't really have a preference. Either way, the JIT can't correctly go on making use of this forwarding, at least under the defaults. So the difference is mainly re-resolution implementation effort vs. interpreter performance impact

@gacholio
Copy link
Contributor

gacholio commented Jul 29, 2024

I don't think your suggestion for (2) would work - there's no guarantee who resolves a CP entry (interp or JIT) so changing the resolve behaviour likely won't work reliably.

I think we should remove the optimization and do some perf testing. The JIT will presumably naturally optimize the chain of forwarder constructors, so this should only affect the interpreter.

@gacholio
Copy link
Contributor

I see the JIT currently uses the forwarder bit in the ROM method. If that can be removed in favour of general inlining of trivial methods, we can get rid of a bunch of code and reclaim the bit.

@jdmpapin
Copy link
Contributor

I think we should remove the optimization and do some perf testing. The JIT will presumably naturally optimize the chain of forwarder constructors, so this should only affect the interpreter.

Good plan

I see the JIT currently uses the forwarder bit in the ROM method. If that can be removed in favour of general inlining of trivial methods, we can get rid of a bunch of code and reclaim the bit.

The JIT won't be able to use the forwarder bit at all, at least in HCR mode. Since HCR is on by default, I think from a JIT point of view it would be fine to remove the entire forwarding mechanism

@jdmpapin
Copy link
Contributor

I don't think your suggestion for (2) would work - there's no guarantee who resolves a CP entry (interp or JIT) so changing the resolve behaviour likely won't work reliably.

Hopefully this is irrelevant, but I do think that (2) might work, and if not it could probably be made to work without too much trouble. The JIT calls jitResolveSpecialMethodRef() regardless of whether or not the CP entry is resolved. Then I don't think I see anything in there that short-circuits based on the state of the CP entry

@gacholio
Copy link
Contributor

If no one else picks this up, I'll remove the entire mechanism in the next week or so and get some builds for perf testing.

gacholio added a commit to gacholio/openj9 that referenced this issue Aug 6, 2024
@gacholio
Copy link
Contributor

gacholio commented Aug 6, 2024

@jdmpapin Please review the JIT changes above to make sure I haven't removed something important.

@jdmpapin
Copy link
Contributor

jdmpapin commented Aug 6, 2024

The commit deletes the beginning of a conditional:

if((subMethod != superMethod) && methodModifiersAreSafe  && ...)
   {
   if (traceIt)
      ...

I think we need to delete that entire case, all the way to

else if (superMethod != subMethod)

and then make that else if into just if

The first conditional is trying to analyze the bytecode to see when a method just forwards to the corresponding method in the base class. It's trying to avoid setting the bit that says that the base method is overridden and avoid invalidating nonoverridden runtime assumptions for it. But if we do that, then a later redefinition of the subclass could change the bytecode for this method and the JIT-compiled code would continue running the base implementation. The HCR assumptions would only guard against redefinition of the base class

gacholio added a commit to gacholio/openj9 that referenced this issue Aug 7, 2024
gacholio added a commit to gacholio/openj9 that referenced this issue Aug 7, 2024
gacholio added a commit to gacholio/openj9 that referenced this issue Aug 7, 2024
tajila pushed a commit to tajila/openj9 that referenced this issue Aug 7, 2024
gacholio added a commit to gacholio/openj9 that referenced this issue Aug 8, 2024
@gacholio
Copy link
Contributor

gacholio commented Aug 8, 2024

This is the current version of the change: https://github.com/gacholio/openj9/tree/forwarder

@jdmpapin
Copy link
Contributor

jdmpapin commented Aug 8, 2024

The JIT changes LGTM, though the comment on if (superMethod != subMethod) could stand to be updated to match, since now there won't have been any attempt to inspect the bytecode

gacholio added a commit to gacholio/openj9 that referenced this issue Aug 8, 2024
@gacholio
Copy link
Contributor

gacholio commented Aug 8, 2024

Done.

@gacholio
Copy link
Contributor

gacholio commented Aug 8, 2024

@IBMJimmyk Before sending this off for performance testing, can you please verify that this fixes the reported problem?

@IBMJimmyk
Copy link
Contributor Author

With the latest changes, my test now passes. Both -Xint and -Xjit.

java -javaagent:RedefAgent.jar -Xint RedefEmptyInit
Running warmup.
Before redefine Result: 0, Expected: 0
After redefine Result: 1, Expected: 1
PASSED

java -javaagent:RedefAgent.jar -Xjit:"optlevel=warm,count=10,disableasynccompilation,limit={*runTest*}" -Xshareclasses:none -Xnoaot RedefEmptyInit
Running warmup.
Before redefine Result: 0, Expected: 0
After redefine Result: 1, Expected: 1
PASSED

@gacholio
Copy link
Contributor

gacholio commented Aug 8, 2024

Thanks, I'll get this out for perf testing ASAP.

gacholio added a commit to gacholio/openj9 that referenced this issue Aug 19, 2024
@gacholio
Copy link
Contributor

@tajila tajila closed this as completed in b7372fe Aug 29, 2024
rmnattas pushed a commit to rmnattas/openj9 that referenced this issue Sep 5, 2024
luke-li-2003 pushed a commit to luke-li-2003/openj9 that referenced this issue Sep 23, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants