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:
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.
- 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.

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:
HostClassName$$Lambda$ part of the class name. 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$
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.