Skip to content

Conversation

@vicente-romero-oracle
Copy link
Contributor

@vicente-romero-oracle vicente-romero-oracle commented Oct 29, 2025

Javac is throwing an OOME for the code like:

class Test {
    interface WithMixin01<T> {}
    // ...
    // 20 interfaces here
    interface WithMixin20<T> {}

    interface ClientA extends
            WithMixin02<ClientA>,
            WithMixin01<ClientA>,
            WithMixin03<ClientA>,
            WithMixin04<ClientA>,
            WithMixin05<ClientA>,
            WithMixin06<ClientA>,
            WithMixin07<ClientA>,
            WithMixin08<ClientA>,
            WithMixin09<ClientA>,
            WithMixin10<ClientA>,
            WithMixin11<ClientA>,
            WithMixin12<ClientA>,
            WithMixin13<ClientA>,
            WithMixin14<ClientA>,
            WithMixin15<ClientA>,
            WithMixin16<ClientA>,
            WithMixin17<ClientA>,
            WithMixin18<ClientA>,
            WithMixin19<ClientA>,
            WithMixin20<ClientA> {
    }

    interface ClientB extends
            WithMixin01<ClientB>,
            WithMixin02<ClientB>,
            WithMixin03<ClientB>,
            WithMixin04<ClientB>,
            WithMixin05<ClientB>,
            WithMixin06<ClientB>,
            WithMixin07<ClientB>,
            WithMixin08<ClientB>,
            WithMixin09<ClientB>,
            WithMixin10<ClientB>,
            WithMixin11<ClientB>,
            WithMixin12<ClientB>,
            WithMixin13<ClientB>,
            WithMixin14<ClientB>,
            WithMixin15<ClientB>,
            WithMixin16<ClientB>,
            WithMixin17<ClientB>,
            WithMixin18<ClientB>,
            WithMixin19<ClientB>,
            WithMixin20<ClientB> {
    }

    public static void main(String... args) {
        ClientA a = null;
        ClientB b = null;
        String selector = "a";
        Object o = switch (selector) {
            case "a" -> a;
            case "b" -> b;
            default -> null;
        };
    }
}

the reason is that after JDK-8353565 we are using Types::lub when joining jump chains in Code. The result of the lub will be erased anyways so the proposal here is to cut in the result given by lub types that will be erased anyways. The reason for the slow lub invocation here is that in order to obtain the lub in this case, the lub method is invoked several times. Each invocation, minus the first one, with very complex types which makes them very slooooow.

TIA


Progress

  • Change must be properly reviewed (1 review required, with at least 1 Reviewer)
  • Change must not contain extraneous whitespace
  • Commit message must refer to an issue

Warning

 ⚠️ Found leading lowercase letter in issue title for 8369654: javac OutOfMemoryError for complex intersection type

Issue

  • JDK-8369654: javac OutOfMemoryError for complex intersection type (Bug - P4)

Reviewers

Reviewing

Using git

Checkout this PR locally:
$ git fetch https://git.openjdk.org/jdk.git pull/28050/head:pull/28050
$ git checkout pull/28050

Update a local copy of the PR:
$ git checkout pull/28050
$ git pull https://git.openjdk.org/jdk.git pull/28050/head

Using Skara CLI tools

Checkout this PR locally:
$ git pr checkout 28050

View PR using the GUI difftool:
$ git pr show -t 28050

Using diff file

Download this PR as a diff file:
https://git.openjdk.org/jdk/pull/28050.diff

Using Webrev

Link to Webrev Comment

@bridgekeeper
Copy link

bridgekeeper bot commented Oct 29, 2025

👋 Welcome back vromero! A progress list of the required criteria for merging this PR into master will be added to the body of your pull request. There are additional pull request commands available for use with this pull request.

@openjdk
Copy link

openjdk bot commented Oct 29, 2025

@vicente-romero-oracle This change now passes all automated pre-integration checks.

ℹ️ This project also has non-automated pre-integration requirements. Please see the file CONTRIBUTING.md for details.

After integration, the commit message for the final commit will be:

8369654: javac OutOfMemoryError for complex intersection type

Reviewed-by: liach, mcimadamore

You can use pull request commands such as /summary, /contributor and /issue to adjust it as needed.

At the time when this comment was updated there had been 12 new commits pushed to the master branch:

As there are no conflicts, your changes will automatically be rebased on top of these commits when integrating. If you prefer to avoid this automatic rebasing, please check the documentation for the /integrate command for further details.

➡️ To integrate this PR with the above commit message to the master branch, type /integrate in a new comment.

@openjdk
Copy link

openjdk bot commented Oct 29, 2025

@vicente-romero-oracle The following label will be automatically applied to this pull request:

  • compiler

When this pull request is ready to be reviewed, an "RFR" email will be sent to the corresponding mailing list. If you would like to change these labels, use the /label pull request command.

@openjdk openjdk bot added the rfr Pull request is ready for review label Oct 29, 2025
@mlbridge
Copy link

mlbridge bot commented Oct 29, 2025

List<Type> mec = closureMin(cl);
List<Type> mec = null;
if (lubWillBeErased && !cl.isEmpty()) {
cl = List.of(cl.head);
Copy link
Contributor Author

@vicente-romero-oracle vicente-romero-oracle Oct 29, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we don't need to deal with the whole list of types from here as it will be erased

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems a bit unsound because we're just picking the first erased supertype. Maybe it's ok because all closures are sorted by subtyping (so all supertypes come after subtypes).

Another possible approach would be not to bother with lub, and to have a simpler routine in Code that:

  • obtains all the erased supers of both types
  • "minimize" them, to find the smallest set of common erased supertypes
  • pick randomly one element in the minimized set

(because lub is a bit of an overkill is then we're going to erase the result anyway). Perhaps it would be good to write both versions, and then decide on which one seems minimally invasive. Of course it's a tricky trade off -- on the one hand by doubling down on lub, we can reuse some of the machinery there, but we also pull in more than we should.

@lahodaj
Copy link
Contributor

lahodaj commented Oct 30, 2025

I am not the expert on types, and leveraging the fact that we will erase the type later is clever.

But, when I was trying to debug this, it seemed the reason is that we are repeatedly and recursively asking lub(ClientB, ClientA), from different paths (e.g. from WithMixin02<ClientA>, WithMixin01<ClientB>, which will then ask the same question based on different mixins one level deeper in the recursion). Caching the results helps in this case[1], and I wonder if that could be a viable solution (i.e. is this mostly about the recursion?).

I also tried var v = java.util.List.of(a, b); - that takes a long time as well, and without the cache, it takes a long time in lub. But the cache is not enough for that case, as with the cache, it still spends a lot of time elsewhere.

[1]

diff --git a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java
index 0c155caa56d..3da5c0703bd 100644
--- a/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java
+++ b/src/jdk.compiler/share/classes/com/sun/tools/javac/code/Types.java
@@ -3984,6 +3984,45 @@ public Type lub(List<Type> ts) {
      * does not exist return the type of null (bottom).
      */
     public Type lub(Type... ts) {
+        TypeListKey key = new TypeListKey(this, ts);
+        Type result = lubCache.get(key);
+
+        if (result == null) {
+            lubCache.put(key, result = computeLub(ts));
+        }
+
+        return result;
+    }
+    private record TypeListKey(Types thisTypes, Type... types) {
+        @Override
+        public int hashCode() {
+            int hashCode = 0;
+            for (Type t : types) {
+                hashCode = 127 * hashCode + thisTypes.hashCode(t);
+            }
+            return hashCode;
+        }
+        @Override
+        public boolean equals(Object obj) {
+            if (!(obj instanceof TypeListKey other)) {
+                return false;
+            }
+            if (thisTypes != other.thisTypes) {
+                return false;
+            }
+            if (types.length != other.types.length) {
+                return false;
+            }
+            for (int i = 0; i < types.length; i++) {
+                if (!thisTypes.isSameType(types[i], other.types[i])) {
+                    return false;
+                }
+            }
+            return true;
+        }
+    }
+    private Map<TypeListKey, Type> lubCache = new HashMap<>();
+    private Type computeLub(Type... ts) {
         final int UNKNOWN_BOUND = 0;
         final int ARRAY_BOUND = 1;
         final int CLASS_BOUND = 2;

@vicente-romero-oracle
Copy link
Contributor Author

vicente-romero-oracle commented Oct 30, 2025

I am not the expert on types, and leveraging the fact that we will erase the type later is clever.

correct, that's the reason, the recursion, but in general I don't think that you can cache the lub results based on the input. As the output will depend not only on the input. See the cache already existing for the Types::merge method. That method is invoked by Types::lub and it itself invokes lub or not. So if Types::merge finds that it itself invoked Types::lub for its current input, then it will refrain from calling Typess::lub again for the same input as this would be an infinite recursion situation. So depending on what is on this merge cache the output of Types::lub could be different for the same input

@vicente-romero-oracle
Copy link
Contributor Author

vicente-romero-oracle commented Oct 30, 2025

some data points to have an idea of the number of times we are invoking lub for the test case included in the description above. Assuming that we run it with different definitions of ClientA and ClientB changing the number of super types so for # of SuperTypes == 1 we would have:

interface ClientA extends WithMixin01<ClientA> {}
interface ClientB extends WithMixin01<ClientB> {}

for 2 supertypes we would have:

interface ClientA extends WithMixin01<ClientA>, WithMixin02<ClientA> {}
interface ClientB extends WithMixin01<ClientB>, WithMixin02<ClientB> {}

and so on. I got the table below:

# of SuperTypes (x) # of Lub Invocations (y)
1 1
2 5
3 16
4 65
5 326
6 1957
7 13700
8 109601
9 986410
10

which is exponential on # of SuperTypes and steeper that e^x for # of SuperTypes > 3

I think this is:
f(x) == 1 for x == 1
f(x) == 5 for x == 2
f(x) == x * f(x - 1) + 1 for x >= 3

e^x < f(x) < 3^(x+3) for x > 3

PS: System out of resources for # of SuperTypes = 10

@vicente-romero-oracle
Copy link
Contributor Author

vicente-romero-oracle commented Oct 31, 2025

Note for reviewers: I think that the current solution is incomplete and still incorrect. We need something more complex for the case when both types are non primitive arrays

@mcimadamore
Copy link
Contributor

Note for reviewers: I think that the current solution is incomplete and still incorrect. We need something more complex for the case when both types are non primitive arrays

Also, for context -- yesterday @vicente-romero-oracle and I explored a possible solution that doesn't need go through lub -- after all here we're after finding a common erased supertype between the two types under test. While finding common erased super types is a part of lub (see JLS), lub also does a lot more stuff. In principle the code we need might be as simple as:

List<Type> ec = intersect(erasedSupertypes(t1), erasedSupertypes(t2));
return ec.head

But this simple solution seems to have issues, so additional investigation is required.

@lahodaj am I correct in assuming that the types to be merged in Code are always either class types or array types? E.g. no type variables, intersection types, or other "weird" stuff?

@lahodaj
Copy link
Contributor

lahodaj commented Oct 31, 2025

Yes, I think the types here should not be weird (even primitive types are probably handled by the isSubtype before going to lub). If needed, the types could be passed through erasure, to be sure.

buf.append(erasure(sup));
}

public List<Type> erasedSupertypes(Type t) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no code changes just made it public plus indentation change

}
return arraySuperType;
private Type arraySuperType;
public Type arraySuperType() {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no code changes just made it public plus indentation change

}
}

private Type allArray(Type... ts) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this code has been extracted from lub so there will be some duplicity here

@vicente-romero-oracle
Copy link
Contributor Author

I have updated the PR improving the array support, basically the previous approach was arriving to a wrong supertype for the case of two reference arrays. There is now some code that is common to a portion of lub which we could probably extract to a common method in Types and just reuse it in Code but not without some massage anyways that could slow the general lub a bit so probably not worthy as lub is a very established code that I don't think will change in the near future. Thanks for all the comments so far

}

private List<Type> getErasedSuperTypes(Type t) {
if (t.hasTag(TYPEVAR)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this can use Types.skipTypeVars?

Copy link
Member

@liach liach left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This iteration is much cleaner and more sensible than the initial version.

@openjdk openjdk bot added the ready Pull request is ready to be integrated label Oct 31, 2025
Copy link
Contributor

@mcimadamore mcimadamore left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Very good -- thanks!

@vicente-romero-oracle
Copy link
Contributor Author

Very good -- thanks!

thanks for the suggestions!

@vicente-romero-oracle
Copy link
Contributor Author

/integrate

@openjdk
Copy link

openjdk bot commented Nov 3, 2025

Going to push as commit 9f97200.
Since your change was applied there have been 24 commits pushed to the master branch:

Your commit was automatically rebased without conflicts.

@openjdk openjdk bot added the integrated Pull request has been integrated label Nov 3, 2025
@openjdk openjdk bot closed this Nov 3, 2025
@openjdk openjdk bot removed ready Pull request is ready to be integrated rfr Pull request is ready for review labels Nov 3, 2025
@openjdk
Copy link

openjdk bot commented Nov 3, 2025

@vicente-romero-oracle Pushed as commit 9f97200.

💡 You may see a message that your pull request was closed with unmerged commits. This can be safely ignored.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

compiler [email protected] integrated Pull request has been integrated

Development

Successfully merging this pull request may close these issues.

5 participants