JDK-8350754 : Deprecate UseCompressedClassPointers
  • Type: CSR
  • Component: hotspot
  • Sub-Component: runtime
  • Priority: P4
  • Status: Finalized
  • Resolution: Unresolved
  • Fix Versions: 25
  • Submitted: 2025-02-26
  • Updated: 2025-04-01
Related Reports
CSR :  
Description
Summary
-------

The switch `UseCompressedClassPointers` is to be deprecated in preparation for the future removal of its negative mode (`-UseCompressedClassPointers`).

Problem
-------

Since the advent of compact object headers, headers can take three possible shapes:

- Containing raw 64-bit Klass pointers (`-UseCompressedClassPointers` `-UseCompactObjectHeaders`)

- Containing "compressed" aka "narrow" 32-bit Klass pointers (`+UseCompressedClassPointers` `-UseCompactObjectHeaders`). This is the default mode.

- Containing 22-bit narrow Klass pointers as part of the new compact object headers (`+UseCompressedClassPointers` `+UseCompactObjectHeaders`)

The first variant, using 64-bit Klass pointers with `-UseCompressedClassPointers`, serves almost no practical purpose. It mainly causes memory bloat, since it wastes 4 bytes per object, which can mean a significant increase in heap space. Moreover, maintaining this mode costs resources and accrues technical debt. In addition to that, Github source code analyses show (e.g. [1]) that it is often misunderstood and misapplied ("uses less memory," which is somewhat ironic).


Risk of deprecation
-------------------

There is a low but certainly nonzero risk involved with deprecating this mode.

### The number of loadable classes

That number is determined by the size of the class space, the number of loaders, and the Klass alignment granularity. At the moment, the following rough limits exist:

a) JVM called with `-UseCompressedClassPointers`: no limit

With UseCompressedClassPointers, the limit is determined by the class space size and the number of class loaders. Klass structures are variable-sized but typically ~700 bytes, with larger outliers being rare. Class loaders increase this to 1KB, since each class loader needs a separate metaspace chunk, the smallest size of which is 1KB. However, applications with millions of class loaders are very rare. Therefore:

b) JVM called with maxed out class space of 4GB: `+UseCompressedClassPointers -UseCompactObjectHeaders -XX:CompressedClassSpaceSize=4G` : about **4-6 million classes**

c) JVM called with default settings (1G default class space size and default settings `+UseCompressedClassPointers -UseCompactObjectHeaders`): about **1-1.5 million classes**

With Lilliput, we align Klass structures to 1KB boundaries (and use the alignment waste for non-Klass metadata); hence:

d) JVM called with maxed out class space `-XX:CompressedClassSpaceSize=4G` and compact object headers `+ UseCompactObjectHeaders` (which implies `+ UseCompressedClassPointers`): about **4 million classes**

e) JVM called with default class space size of 1GB and compact object headers `+UseCompactObjectHeaders`: about **1 million classes**

If we deprecate `-UseCompressedClassPointers` (a), we lose the ability to load unlimited classes. The maximum loadable classes would drop to 5-6 million with an explicitly maxed-out class space (b).

We consider that risk theoretical. We have never seen practical installations with these many classes, and if we disregard pathological leak scenarios, that would run into OOMEs eventually anyway. One interesting effect of loading this many classes would be that - since memory is needed from both class space and non-class metaspace in a ratio of 1:6..1:10 - maxing out 4GB of class space would use between 24..>40 GB of non-class metaspace in addition to the class space. This is a ridiculous number for class metadata alone.

Moreover, one would hit other limits before that, e.g., we would run out of code cache when the compiler attempts to compile this many classes. So, the classes would mostly stay uncompiled, which is not a practical solution for production scenarios.

We have a fallback plan if we run into problems with the number of classes limit. That plan (the Near/Far class idea, see mailthread [4]) has not yet been implemented but seems entirely practical. However, due to the increased complexity and overhead, we will only implement this plan when necessary. 


### Users misapplying -UseCompressedClassPointers 

Uses of -UseCompressedClassPointers are rare (Github source search yields about 700 hits, many of them forks of the same projects). Most of these uses seem to be misapplication based on misunderstanding the switch. These cases should remove the switch. Some uses mention crashes with +UseCompressedClassPointers. If these situations are reproducible in the mainline, they should be fixed.

### Users needlessly specifying +UseCompressedClassPointers 

If we deprecate the full switch and not just its negative form (see section "Solution" below), Apps that specify +UseCompressedClassPointers will see a deprecation warning. The fix would be to remove that switch.

### 32-bit platforms

32-bit platforms use raw 32-bit Klass* pointers with `-UseCompressedClassPointers` being hardwired. An idea exists to let 32-bit platforms use `+UseCompressedClassPointers` with a "fake class space" mode in which we treat the whole 4GB address space as class space. With this idea, we would use `+UseCompressedClassPointers` like other platforms, but Klass structures would continue to live in non-Klass metaspace as before (introducing a class space, even a small one, would risk address-space fragmentation on 32-bit).

However, for now, I would propose not to deprecate `-UseCompressedClassPointers` for 32-bit; the switch is not accepted on 32-bit platforms anyway since it is hard-coded. 

Note that 32-bit platforms are clearly on their way out. If JEP 503 [5] "Remove the 32-bit x86 port" is finished, the only remaining 32-bit platforms will be arm and the 32-bit zero JVMs. The 32-bit ports are in a sorry state and often broken for lengthy times (at the moment of writing this CSR, neither x86 nor 32-bit zero can even be built successfully). We need to decide on the fate of 32-bit platforms soon; as long as we don't do this, I hesitate to put a lot of work into them.


Solution
--------

Removing the `-UseCompressedClassPointers` mode leaves us with just the `+UseCompressedClassPointers` mode (the current default). Since that would make the switch pointless, the proposed solution is to deprecate the switch.

An alternative solution would be to deprecate/forbid the use of its negative form of `-UseCompressedClassPointers.` This would lessen the compatibility impact since the negative form of this switch is rarely used. In preceding discussions, however (see comments under [2]), this was not deemed necessary. The positive form (`+UseCompressedClassPointers`) is used much more frequently, pointlessly so, since `+UseCompressedClassPointers` is the default. However, deprecating the switch would yield warnings for the positive form, pointlessly so.


Specification
-------------

```
diff --git a/src/hotspot/share/runtime/arguments.cpp b/src/hotspot/share/runtime/arguments.cpp
index ab05cbeb891..173edb79387 100644
--- a/src/hotspot/share/runtime/arguments.cpp
+++ b/src/hotspot/share/runtime/arguments.cpp
@@ -534,6 +534,7 @@ static SpecialFlag const special_jvm_flags[] = {
   { "MetaspaceReclaimPolicy",       JDK_Version::undefined(), JDK_Version::jdk(21), JDK_Version::undefined() },
   { "ZGenerational",                JDK_Version::jdk(23), JDK_Version::jdk(24), JDK_Version::undefined() },
   { "ZMarkStackSpaceLimit",         JDK_Version::undefined(), JDK_Version::jdk(25), JDK_Version::undefined() },
+  { "UseCompressedClassPointers",   JDK_Version::jdk(25), JDK_Version::jdk(26), JDK_Version::undefined() },
 
 #ifdef ASSERT
   { "DummyObsoleteTestFlag",        JDK_Version::undefined(), JDK_Version::jdk(18), JDK_Version::undefined() },
```

## Further information

A discussion was held on hotspot-dev previously, see [2].
A JEP was created for this topic, which we will abandon in favour of this CSR [3]. The comment section of this JEP holds some valuable insights.

[1] https://github.com/jobar/ansidoop/blob/9618e31d65089b7586ac1c4f76de5bd748e59c4b/roles/spark-common/templates/spark-defaults.conf.j2#L9-L17

[2] https://mail.openjdk.org/pipermail/hotspot-dev/2025-February/101023.html

[3] https://bugs.openjdk.org/browse/JDK-8350272

[4] https://mail.openjdk.org/pipermail/lilliput-dev/2024-July/001809.html

[5] https://openjdk.org/jeps/503
Comments
Making UseCompactObjectHeaders a product option depends on this CSR getting approved and this work. What needs to be done? JDK-8350457
31-03-2025

Purely from a process standpoint, the CSR needs to be Finalized to request the second phase of CSR review: https://wiki.openjdk.org/display/csr/Main HTH
31-03-2025

[~rkennke] [~kvn] Just my gut feeling, but I would leave the code in for at least one release post 25. Let users use 25 and see if any major issues creep up. We have done a lot of due diligence here, but I still feel this deprecation can shake lose more unseen things. But if others disagree and favor a faster approach, I am fine, too. (side note, the CSR process does not impose a timeline, just an order of steps)
07-03-2025

[~kvn] yes, I think it is the normal process, but I did not see it reflected in the proposed change in this CSR.
05-03-2025

Lets target JDK 26 for Obsolete flag. May be file subtask of RFE JDK-8350753 and start looking on it now so we have more time to understand issues and how we can resolve it. Concentrate on ARM 32-bits first since x86 may go away before we start removing code.
04-03-2025

[~rkennke] Is not it normal Lifecycle for flags already? https://wiki.openjdk.org/display/HotSpot/Hotspot+Command-line+Flags%3A+Kinds%2C+Lifecycle+and+the+CSR+Process JDK 25 - Deprecate JDK 26 - Obsolete (accept flag but code is removed) JDK 27 - Expired
04-03-2025

Should we not shoot for obsoleting the flag in JDK26? Or is this too ambitious at this point (because of 32 bit support)?
04-03-2025

Moving to Provisional.
03-03-2025

And as you said, we may remove 32-bit x86 code before you start removing UseCompressedClassPointers related code - you need to wait next release after you deprecate flag.
01-03-2025

I looked on hotspot/cpu/arm code which check UseCompressedClassPointers. I think it is leftover from old 64 bit implementation: https://github.com/openjdk/jdk/blob/master/src/hotspot/cpu/arm/matcher_arm.hpp#L70 matcher_x86.hpp has the same check. I see some x86 C1 code not guarded by "#ifdef __LP64" but uses `cmpptr()`, `movptr()` which will do correct thing in 32-bit VM. We just need to go through all these places in 32-bit code and make sure we generate correct instructions in 32- and 64-bits VM when we remove the flag. It is not a lot of places to check.
01-03-2025

Hi [~kvn]! Thank you for looking at this. >> " unless they ran on JDK 11 or lower and used a heap size larger than 32GB (in these old JDKs, +UseCompressedClassPointers was tied to +UseCompressedOops)" > I don't think you need this statement since we are not going to backport this change. I thought that when a customer upgrades from JDK 11 to JDK 25, and his app uses more than 32GB of heap - his JDK 25 JVM will implicitly switch to `+UseCompressedClassPointers`. If his app relied on `-UseCompressedClassPointers` without realizing it, it may experience problems. But thinking about this some more, this is an issue with upgrading from JDK 11 to newer versions in general, so nothing new. E.g. JDK 17 would have had `+UseCompressedClassPointers` enabled by default even if the heap size > 32GB. So this "problem" has existed for years, and I have never heard of customers running into this. So this probably is a non-issue and I am just overthinking. I removed the statement. > I think you need e) case with +UseCompressedClassPointers +UseCompactObjectHeaders and default Thank you; added. >> I would propose not to deprecate -UseCompressedClassPointers for 32-bit; the switch is not accepted on 32-bit platforms anyway since it is hard-coded. > I thought UseCompressedClassPointers is not used for any code in 32-bit VM. The flags removal should not affect 32-bit ports. Or I am missing something? Yes, this is more of a topic when we remove the code behind `-UseCompressedClassPointers` later. 32-bit uses the code sections conditional to `-UseCompressedClassPointers`. If we remove the flag and these code sections, 32-bit stops functioning. So we need a solution for this. But we also want to shed complexity, this is one of the main purposes behind this deprecation; so just replacing `-UseCompressedClassPointers` with `#ifndef __LP64` works, but it is not attractive. OTOH using the remaining `+UseCompressedClassPointers` code paths for 32-bit will not work either. The solution for 32-bit I have in mind is: - We leave the object header as it is. With its 32-bit raw Klass* at offset 4. - but this can be seen as a "compressed Klass pointer" with encoding base = NULL and encoding shift = 0 -> "faking" narrow Klass pointers - but we don't want to setup a "class space" - we could, but 32-bit is vulnerable against address space fragmentation issues. So we want to leave Klass structures in normal metaspace (lots of small memory chunks, dispersed, works better in a small address space) - and then there is a lot of other stuff that depends on `+-UseCompressedClassPointers` - for each of these one needs to decide whether to use the old `-UseCompressedClassPointers` path for 32-bit or whether we can use the "fake narrow Klass" mode. It's an annoying topic, and one I would love to leave for later or maybe never. For now, with 32-bit, we just have to make sure that the implicit `-UseCompressedClassPointers` does not cause any warnings on stderr.
01-03-2025

> I would propose not to deprecate -UseCompressedClassPointers for 32-bit; the switch is not accepted on 32-bit platforms anyway since it is hard-coded. I thought `UseCompressedClassPointers` is not used for any code in 32-bit VM. The flags removal should not affect 32-bit ports. Or I am missing something?
28-02-2025

I think you need e) case with +UseCompressedClassPointers +UseCompactObjectHeaders and default CompressedClassSpaceSize
28-02-2025

> " unless they ran on JDK 11 or lower and used a heap size larger than 32GB (in these old JDKs, +UseCompressedClassPointers was tied to +UseCompressedOops)" I don't think you need this statement since we are not going to backport this change.
28-02-2025

Extended Risk section to contain 32-bit platform notes
28-02-2025

Hi [~kvn]: > can you add more explanation to d) ? I assume it is due to CompressedClassSpaceSize default value is smaller than 4Gb. What is value? Is it ergonomically set too? I extended the text. Note the point moved to (c). The gist of it is: class space size defaults to 1GB. > What about Java heap - we have to allocate classes mirrors there. jlClass is 16 byte IIRC. Not much of a memory drain (if it were, it would be an argument for deprecation, no?)
28-02-2025

> maxing out 4GB of class space would use between 24..>40 GB of non-class metaspace What about Java heap - we have to allocate classes mirrors there.
27-02-2025

[~stuefe] can you add more explanation to d) ? I assume it is due to `CompressedClassSpaceSize` default value is smaller than 4Gb. What is value? Is it ergonomically set too?
27-02-2025