4 ms·
Java 24 to Reduce Object Header Size and Save Memory
- exabrial 2y agoI always kind of found it interesting that specifying the size of object fitters was part of the JVM specification. This sort of seems like an arbitrary implementation detail, since the programmer will never have to know anything about it.
- papercrane 2y agoThe JVM specification does not specify anything about how objects should be represented. The only time internal representation of objects is discussed in the JVM spec is section that says "The Java Virtual Machine does not mandate any particular internal structure for objects." In this case the JEP is scoped to just the hotspot runtime. Other implementations are free to represent objects however they want. https://docs.oracle.com/javase/specs/jvms/se23/html/jvms-2.html#jvms-2.7 https://docs.oracle.com/javase/specs/jvms/se23/html/jvms-2.h...
- exabrial 2y agoAh! that makes sense, thanks!
- deleted 2y ago[deleted]
- cesarb 2y agoPart of this complexity comes from what IMO was a mistake on Java's design (which was AFAIK copied by C#): the base Object class does too much. It has equality comparison, string conversion, object hashing, and a per-object re-entrant lock. Other than equality comparison (which is also bad because it contributes to the perennial confusion between identity equality and value equality), these need extra storage for each and every object in the system (string conversion contains the object hash code as part of its default output). Some tricks are used to avoid most of the space overhead for the per-object lock, at the cost of extra complexity.
- SpaghettiCthulu 2y agoThe default `toString` implementation isn't cached, is it?
- invalidname 2y agoIt is not but strings are cached (interned) in Java which is a different thing.
- layer8 2y agoString literals are interned, strings in general aren’t.
- layer8 2y agoNot the OP, but my beef with toString is that for some classes it is an essential part of the interface contract that requires a stable and documented string mapping (e.g. for value types like BigInteger or URI, and for String itself), whereas for other classes it just serves as a way to provide a debugging/logging representation that may change from one version to the next, and whose exact representation should not be relied on. These are really two separate purposes with a different interface contract. It would have been better for Object to have a toDebugString method, and to restrict implicit string conversion (concatenation) to classes implementing a StringConvertible interface with a corresponding separate toString method.
- ratmeadow 2y agoAstounding how so many languages and programmers don't make the clear distinction between "debug string", "canonical string", "human readable string", etc. There is no such thing as a totally generic "to string" function for any nontrivial program. The approach I'm most a fan of is functional languages where everything has a fixed canonical string representation (even cooler when you can convert the string directly back to code), and everything else you must explicitly create a function for.
- josefx 2y ago
- nwellnhof 2y ago> This is lightweight, by way of comparison: until quite recently, Python's header tax was 308 bytes Really? I thought that PyObject_HEAD only contains two machine words.
- masklinn 2y agoYou are correct, but PyObject_HEAD is not the full header for the average object. - the base requirement is a class pointer and a refcount, that's PyObject_HEAD (and PyObject) - then unless you have disabled this at compilation time, they have two pointers for the cycle-breaking part of the GC - a dict pointer (or as many instance value pointers as there are object members when using slots) - and a weakrefs pointer (except for slotted classes, unless you added it back) That is however only 6 words (48 bytes), or 4 (32) for slotted classes with no members or weakrefs. I believe the header is larger when running without GIL on 3.13 because PyObject has 2 more words (a local refcount, a gc bitset, and a mutex packed in a word with some padding, and a tid pointer). Still nowhere near 308 though, I've no idea where they got that. Maybe whoever wrote that article included the instance dict in their calculations? That would add 200~300 bytes. Or maybe they got mixed up between bits and bytes, calculated 308 bits somehow then wrote that up as bytes.
- deleted 2y ago[deleted]
- sctb 2y agoFor more information, there's a recent talk on Project Lilliput by Roman Kennke: https://www.youtube.com/watch?v=kHJ1moNLwao https://www.youtube.com/watch?v=kHJ1moNLwao.
- layer8 2y ago> This means that [with Compact Object Headers] the number of different class types we can load into a JVM process is [reduced to] around ~4 million [from previously 4 billion]. Comparing the class count of some of today’s Java projects (including dependencies) to two decades ago, I wonder if we won’t risk hitting that limit in another two decades or so, and then revert back to the bigger header size again. ;).
- cesarb 2y ago> Comparing the class count of some of today’s Java projects (including dependencies) [...] You are forgetting one important use case: defining classes dynamically. You have to count not only every class (including inner classes) of every dependency, but also all classes created at runtime through direct bytecode manipulation.
- layer8 2y agoYou are right, and I actually thought of that, but I suspect it’s usually still dominated by, or at most roughly on par with, static class count. Dynamic proxy classes, for example, are cached. And one probably shouldn’t dynamically create classes based on uncontrolled external input. The other thought I had is that if AI-generated code takes off, this could explode class count. On the other hand, AI could then also be instructed to refactor to minimal class count.
- masklinn 2y agoTBF 4 million classes is a very high amount, even in a huge project. It's not like dex files' limitation to 64Ki methods which you can very easily blow past if you have a large codebase or a few huge dependencies.