Openj9: Store anonymous classes to shared cache

Created on 3 May 2019  路  29Comments  路  Source: eclipse/openj9

This issue is to look at storing anonymous classes to the shared cache.

Anonymous class is defined directly through Unsafe.defineAnonymousClass(), it does not use loadClass()/findClass().
Discussed with Dan, the current plan is to store the anonymous class in the shared cache as orphan. (I guess we are doing the same for proxy classes in Java 8).
After loading the anonymous class, we compare it against the potential match in the shared cache. If a match is found, the one in the shared cache is returned. As long as the class is in the cache, there can be AOTs.
A few points to look at:

  1. The name of anonymous class is something like HostClassName$$Lambda$1/0xromaddress. 0xromaddress can be different from run to run. We might need to unmingle the 0xromaddress from the class name. The 0xromaddress is also in the constant pool, we might need to take care of that when doing class comparison.
  2. Trigger the SCC call for anonymous classes
  3. All unsafe classes are currently not shared. Anonymous class is defined using Unsafe.defineAnonymousClass().
  4. Make sure compareROMClassForEquality() correctly returns the anonymous class in shared cache if the class has not changed on disk.
  5. Add test.
vm enhancement

All 29 comments

FYI: @DanHeidinga @pshipton

Turn on J9CLASSLOADER_SHARED_CLASSES_ENABLED for vm->anonClassLoader

I'm not sure this will matter as we don't call the anonClassLoader's defineClass method. There is code that checks for the anonClassLoader and skips the SCC calls which will need to be updated as part of this work

Related to item 3 (unsafe classes are currently not shared), https://github.com/eclipse/openj9/issues/4985 - Proxy ROMClasses are not stored in the Java 11 shared cache

There is code that checks for the anonClassLoader and skips the SCC calls which will need to be updated as part of this work

Yes. I will update to say "trigger the SCC call for anonymous classes." Currently if J9CLASSLOADER_SHARED_CLASSES_ENABLED in not there, the SCC call is skipped. There may be other conditions that needs to be changed to trigger the SCC call. We will know more when we get into the details.

  1. All unsafe classes are currently not shared

This will be changed by this item. If the proxy classes are defined through Unsafe.defineAnonymousClass(), this item might also solve #4985

FYI: @sogutbera

The name of anonymous class is something like HostClassName$$Lambda$1/0xromaddress. 0xromaddress can be different from run to run.

@sogutbera find out that the class name is actually HostClassName$$Lambda$<number>/0xromaddress, and the part <number> can also be different from run to run. So we need to unmingle <number>/0xromaddress

    public class Test1 {

            public static void main(String[] args){

                    Test2 tobj = new Test2();
                    tobj.func();

                    int x = 99;
                    Comparator<String> stringComparatorLambda = (String o1, String o2) -> { return o1.compareTo(o2)+o2.compareTo(o1)+15+x; };
                    int lambdaComparison = stringComparatorLambda.compare("world", "hello");
                    System.out.println(lambdaComparison);

                    int y = 12345;
                    Comparator<String> stringComparatorLambda2 = (String o1, String o2) -> { return 2+y+o2.compareTo(o1); };
                    int lambdaComparison2 = stringComparatorLambda2.compare("###", "!!!");
                    System.out.println(lambdaComparison2);
            }
    }

    public class Test2 {

            public void func(){
                    Comparator<String> strComparator = (String o1, String o2) -> o1.compareTo(o2);
                    int strComparison = strComparator.compare("coffee", "cup");
                    System.out.println(strComparison);


                    int x = 9;
                    Comparator<String> strComparator2= (String o1, String o2) -> o1.compareTo(o2)+o2.compareTo(o1)+15+x;                      
                    int strComparison2 = strComparator2.compare("world", "hello");
                    System.out.println(strComparison2);                                                                                       
            }
    }

So when I run this code and print the names of the anonymous classes I get this output:
Anonymous Class Name: Test2$$Lambda$1/00000000D4305E00
Anonymous Class Length: 32
-6
Anonymous Class Name: Test2$$Lambda$2/00000000D4306D20
Anonymous Class Length: 32
24
Anonymous Class Name: Test1$$Lambda$3/00000000D4493300
Anonymous Class Length: 32
114
Anonymous Class Name: Test1$$Lambda$4/00000000D4493E90
Anonymous Class Length: 32
12345

And when I change the Test2 file with this:

    public class Test2 {

            public void func(){

            }
    }

I get this output:
Anonymous Class Name: Test1$$Lambda$1/00000000D0306430
Anonymous Class Length: 32
114
Anonymous Class Name: Test1$$Lambda$2/00000000D0306850
Anonymous Class Length: 32
12345

So the number we are seeing in the class name in the format HostClassName$$Lambda$"number"/0xromaddress is changing from run to run. It is starting from 1 and increasing by one at each lambda expression.

I suspect that there's a reasonable limit to the numbers of lambdas likely to be generated in a class so matching on the "HostClassName$$Lambda$" is a good first heuristic.

Another may be to capture some limited amount of the stacktrace as the classpath equivalent for this class. Provided the capture / compare of the stacktrace is faster than the repeated compare of the romclasses, we'd come out ahead.

@sogutbera is currently using "HostClassName$$Lambda$0/00000000" as the class name. It is used in the rom class comparison, and also as the hashkey to locate the anonymous class in the shared cache. This is functionally equivalent to using "HostClassName$$Lambda$"

To solve the repeated comparison of the romclasses, another possibility might be to take advantage of the first field romSize in struct J9ROMClass. (Not sure if we are already comparing this field in compareROMClassForEquality(), but Bera can confirm). I guess romSize can eliminate most anonymous classes under the same host class without going through a full class comparison.

The romSize is affected by UTF8 sharing. i.e. two ROM classes can be equivalent but be different sizes because they have different shared UTF8's.

A simple solution, although it could cost a little more space, is to disable UTF8 sharing for anonymous classes.

_interningEnabled = J9_ARE_ALL_BITS_SET(_bcuFlags, BCU_ENABLE_INVARIANT_INTERNING) && J9_ARE_NO_BITS_SET(_findClassFlags, J9_FINDCLASS_FLAG_ANON);
This is how the _interningEnabled variable is initialized so for anonymous classes we don't need to worry about that.

Bera is changing the code to compare romSize first for anonymous classes. We will continue to full class comparison only when romSize is the same.

I changed the class name to "HostClassName$$Lambda$0/00000000" and recovered it when shared classes are not available. Also modified the size variables since the number before the '/' could have more than 1 digit. Also in compareROMClassForEquality, if the class being compared is anonymous, I check if the ROM sizes match at first and return false if they don't so we don't do the other checks. I am running a build now.

When I run a Java code with 10 lambda expressions with shared classes enabled, this is the output I get:

Modified Class Name: Test1$$Lambda$0/0000000000000000
92
Modified Class Name: Test1$$Lambda$0/0000000000000000
92
Modified Class Name: Test1$$Lambda$0/0000000000000000
92
Modified Class Name: Test1$$Lambda$0/0000000000000000
92
Modified Class Name: Test1$$Lambda$0/0000000000000000
92
Modified Class Name: Test1$$Lambda$0/0000000000000000
92
Modified Class Name: Test1$$Lambda$0/0000000000000000
92
Modified Class Name: Test1$$Lambda$0/0000000000000000
92
Modified Class Name: Test1$$Lambda$0/0000000000000000
92
Modified Class Name: Test1$$Lambda$0/0000000000000000
12283

When I run with shared classes disabled, this is the output:

Modified Class Name: Test1$$Lambda$0/0000000000000000
Recovered Class Name: Test1$$Lambda$1/0000000000000000
92
Modified Class Name: Test1$$Lambda$0/0000000000000000
Recovered Class Name: Test1$$Lambda$2/0000000000000000
92
Modified Class Name: Test1$$Lambda$0/0000000000000000
Recovered Class Name: Test1$$Lambda$3/0000000000000000
92
Modified Class Name: Test1$$Lambda$0/0000000000000000
Recovered Class Name: Test1$$Lambda$4/0000000000000000
92
Modified Class Name: Test1$$Lambda$0/0000000000000000
Recovered Class Name: Test1$$Lambda$5/0000000000000000
92
Modified Class Name: Test1$$Lambda$0/0000000000000000
Recovered Class Name: Test1$$Lambda$6/0000000000000000
92
Modified Class Name: Test1$$Lambda$0/0000000000000000
Recovered Class Name: Test1$$Lambda$7/0000000000000000
92
Modified Class Name: Test1$$Lambda$0/0000000000000000
Recovered Class Name: Test1$$Lambda$8/0000000000000000
92
Modified Class Name: Test1$$Lambda$0/0000000000000000
Recovered Class Name: Test1$$Lambda$9/0000000000000000
92
Modified Class Name: Test1$$Lambda$0/0000000000000000
Recovered Class Name: Test1$$Lambda$10/0000000000000000
12283

So I think it looks correct. I also checked if it compares the rom sizes correctly and it seems correct.

I suggested Bera to test his change on some real world application.

I tested my changes on Eclipse and got an error which said "j0/0000000000000000.source", Hang said the 0/0000000000000000 seemed like the class name so I tested changing the class name and the class name in the error also changed. So now instead of changing the class name I will work on changing compareROMClassForEquality to check just for the "HostClassName$$Lambda$" part of the class name.

Screen Shot 2019-06-10 at 6 20 58 PM (3)

romSize's will be different when one class is named HostClassName$$Lambda$10 and another HostClassName$$Lambda$9 since first has longer length. To solve this issue we thought that instead of checking if the romSize's are equal, we could check if the difference between them is equal or less than sizeof(U_64) which would allow up to 4 digits difference.

So the required changes should be:

  1. Change compareROMClassForEquality() to check just for the HostClassName$$Lambda$ part of the class name.
  2. romClass comparison should directly return false if romSize difference > sizeof(U_64).
  3. Use HostClassName$$Lambda$ as the hashkey to store and find the anonymous class in the shared cache.

The Eclipse error was because I was also changing the names of anonymous classes that were not lambda classes, fixed it now and Eclipse seems to work. Adding tests now.

The Eclipse error was because I was also changing the names of anonymous classes that were not lambda classes, fixed it now and Eclipse seems to work.

If the solution using HostClassName$$Lambda$0/00000000 works, then there is no need to switch to the alternative solution mentioned in https://github.com/eclipse/openj9/issues/5665#issuecomment-501021868

The solution using HostClassName$$Lambda$0/00000000 doesn't work because the class file size doesn't change when I change the class name so for example if the same class has the name HostClassName$$Lambda$12 in one run and HostClassName$$Lambda$9 in another run they don't match because even though I have replaced the numbers with 0, the class file sizes remains the same. So I am going back to the alternative solution mentioned in Hang's comment

Moving out as it won't make 0.15

Lambda classes also have some kind of method name which we compare in compareROMClassForEquality, it's in the form of lambda$$ and sometimes has a '(' at the end, the function name is the name of the function the class is in, and the number is set at compile time and starts from 0 and increases by 1 in each file so it is not global. For example I have a code which has 4 cases based on the input I give, I run only the 4th case which has one lambda class at first, then it calls another function named "func" in another file which has 2 lambda classes, and one last lambda class. The names are "lambda$main$14(", "lambda$func$0", "lambda$func$1", "lambda$main$15" respectively. The number starts from 14 because there are 14 other lambda classes before the 4th case so this shows that it is set in compile time.

public class Test1 {

    public static String str1 = new String("testing123");

    public static void main(String[] args){
        int input = Integer.parseInt(args[0]);

        if(0 == input) {
            Comparator<String> stringComparatorLambda = (String o1, String o2) -> {
                return o1.compareTo(o2) + o2.compareTo(o1) + 15;
            };
            int lambdaComparison = stringComparatorLambda.compare("world", "hello");
            System.out.println(lambdaComparison);

            Comparator<String> stringComparatorLambda2 = (String o1, String o2) -> {
                return o1.compareTo(o2) + 42;
            };
            int lambdaComparison2 = stringComparatorLambda2.compare("qqqsdasfds", "dsfkopvwqp");
            System.out.println(lambdaComparison2);
        }
        else if(1 == input){
            int x = 99;
            Comparator<String> stringComparatorLambda = (String o1, String o2) -> {
                return str1.compareTo(o1) + o1.compareTo(o2) + o2.compareTo(o1) + 15 + x;
            };
            int lambdaComparison = stringComparatorLambda.compare("world", "hello");
            System.out.println(lambdaComparison);

            Test2 tobj = new Test2();
            tobj.func();

            Comparator<String> stringComparatorLambda2 = (String o1, String o2) -> {
                return str1.compareTo(o1) + o1.compareTo(o2) + o2.compareTo(o1) + 42 + x;
            };
            int lambdaComparison2 = stringComparatorLambda2.compare("qqqsdasfds", "dsfkopvwqp");
            System.out.println(lambdaComparison2);
        }
    }
}

This is Test1 and it's calling Test2 below.

public class Test2 {

    public void func(){
        Comparator<String> strComparator = (String o1, String o2) -> o1.compareTo(o2);
        int strComparison = strComparator.compare("coffee", "cup");
        System.out.println(strComparison);

        Comparator<String> strComparator2 = (String o1, String o2) -> o2.compareTo(o1) + 1523;
        int strComparison2 = strComparator2.compare("a_cup_of", "tea");
        System.out.println(strComparison2);
    }
}

This is the output of Test1 when the input is 1:

111
-6
Current Name: lambda$func$1
Compared Name: lambda$func$0
1542
Current Name: lambda$main$3
Compared Name: lambda$main$2
144

The names are only being printed when they are compared so the first 2 names are when the second lambda class in Test2 is being compared to the first one in Test2 and the last 2 names are when the last lambda class in Test1 is being compared to the first one in Test1.

I guess we do not need to handle the differences in method names. The only thing that we ignore in compareROMClassForEquality() is $<number>/0xromaddress in the class name.

Was this page helpful?
0 / 5 - 0 ratings