RFR: 8302154: Hidden classes created by LambdaMetaFactory can't be unloaded [v2]

Volker Simonis simonis at openjdk.org
Fri Feb 17 20:03:54 UTC 2023


On Thu, 9 Feb 2023 18:11:18 GMT, Volker Simonis <simonis at openjdk.org> wrote:

>> Prior to [JDK-8239384](https://bugs.openjdk.org/browse/JDK-8239384)/[JDK-8238358](https://bugs.openjdk.org/browse/JDK-8238358) LambdaMetaFactory has created VM-anonymous classes which could easily be unloaded once they were not referenced any more. Starting with JDK 15 and the new "hidden class" based implementation, this is not the case any more, because the hidden classes will be strongly tied to their defining class loader. If this is the default application class loader, these hidden classes can never be unloaded which can easily lead to Metaspace exhaustion (see the [test case in the JBS issue](https://bugs.openjdk.org/secure/attachment/102601/LambdaClassLeak.java)). This is a regression compared to previous JDK versions which some of our applications have been affected from when migrating to JDK 17.
>> 
>> The reason why the newly created hidden classes are strongly linked to their defining class loader is not clear to me. JDK-8239384 mentions it as an "implementation detail":
>> 
>>> *4. the lambda proxy class has the strong relationship with the class loader (that will share the VM metaspace for its defining loader - implementation details)*
>> 
>> From my current understanding the strong link between a hidden class created by `LambdaMetaFactory` and its defining class loader is not strictly required. In order to prevent potential OOMs and fix the regression compared the JDK 14 and earlier I propose to create these hidden classes without the `STRONG` option.
>> 
>> I'll be happy to add the test case as JTreg test to this PR if you think that would be useful.
>
> Volker Simonis has updated the pull request incrementally with two additional commits since the last revision:
> 
>  - Remove assertions which insist on Lambda proxy classes being strongly linked to their class loader
>  - Removed unused import of STRONG und updated copyright year

Here are some numbers. I basically measured the overhead in Metaspace and native memory (i.e. `malloc()`) for 10_000 Lambda classes created with `LambdaMetafactory.metafactory()`. For the Metaspace numbers I took the "used" column for both "Class" and "Non-Class" areas of all class loaders (i.e. "Total Usage") reported by `jcmd VM.metaspace show-loaders`. This is basically the same like the "Total" "BlockSz" reported by `jcmd VM.classloader_stats`.

For native memory overhead I took the "malloc" numbers from the "Class" section in the output from `VM.native_memory`. I did run the test program with `-Xint -XX:+UseSerialGC -Xms512m -Xmx512m -XX:MetaspaceSize=100M -Xlog:gc -XX:NativeMemoryTracking=detail LambdaClassLeak 10000` to make sure I have no garbage collections except the explicit one triggered by me. 

The results in the first two columns are the difference between the numbers before and after creating the 10_000 Lambda classes and the results in the last two columns are the numbers after doing a `System.gc()` and potentially unloading the created Lambda classes (because they are all not referenced from the program any more). 

| JDK | Metaspace (kb)  | malloc (kb)  | - after GC -> | Metaspace (kb)  | malloc (kb) | 
|:---:| ---------------:| ------------:| ------------- | ---------------:| -----------:|
| 11  |          16_328 |        6_561 |               |               0 |         232 |
| 15  |          13_624 |          240 |               |          13_624 |          92 |
| 17  |          13_394 |         3_38 |               |          13_394 |          93 |
| tip |          13_451 |          337 |               |          13_451 |          93 |
| fix |          13_437 |        5_957 |               |               0 |          10 |

As you can see, with JDK 11, where Lambda classes are implemented by VM Anonymous classes, the 10_000 classes take up ~16mb of Metaspace and 6mb of native memory. The native memory is required for the `ClassLoaderData` structures because each VM Anonymous class lives in its own `ClassLoaderData`. After a GC, all the Metaspace and most of the native memory gets freed up.

Starting with JDK 15,  Lambda classes are now backed by the newly introduced "Hidden Classes". The Metaspace consumption slightly drops from ~16 to ~13mb (because the classes are a little smaller) and the native consumption drops significantly from ~6mb to 240kb. That's because the newly generated hidden classes are all strongly coupled to their defining class loader and therefor live in its `ClassLoaderData` section. While this saves ~6mb of native memory, it also prevents unloading of these classes.

These numbers haven't changed up to the current JDK development version. With the fix proposed in this PR, we basically trade about ~6mb of native memory (i.e. ~600 bytes per Lambda class) for the ability to easily unload such Lambda classes once they are not referenced any more.

I still think doing this change is reasonable because:
1. `LambdaMetafactory.metafactory()` does not mention that the classes is creates will not be unloadable. And they actually were unloadable up to JDK 11 while they were backed by VM anonymous classes. This is unexpected for users.
2. I'd argue that Metaspace is more "*valuable*" than native memory because it is limited by `-XX:MetaspaceSize` whereas native memory is only limited by the amount of available memory on the system. Also, growing Metaspace will trigger garbage collections which might be unexpected.
3. I think only "real" users of Lambda functions should pay the price for them and not users who don't need them any more.
4. Finally, the PR fixes a regression compared to the JDK 11 behavior. 

In general it seems to me that creating a new  `ClassLoaderData` for each hidden (or VM anonymous) class is a lot of overhead and was probably only done as "a hack" to simplify the implementation of class unloading for such classes. This might have been appropriate in the "early days" when we had just a few VM anonymous classes. But as the usage of `LambdaMetafactory.metafactory()` becomes more common it may make sense to revise the implementation such that for example all hidden classes defined by a class loader can live in a single `ClassLoaderData` structure. I think this should be technically possible although it would complicate the unloading logic (and should therefore be postponed to a later change).

-------------

PR: https://git.openjdk.org/jdk/pull/12493


More information about the core-libs-dev mailing list