I’m hoping that Java 14 records actually use less memory than a similar data class.
Do they or is the memory using the same?
I’m hoping that Java 14 records actually use less memory than a similar data class.
Do they or is the memory using the same?
To add to the basic analysis performed by @lugiorgi and a similar noticeable difference that I could come up with analyzing the byte code, is in the implementation of toString
, equals
and hashcode
.
On one hand, the existing class with overridden Object
class APIs looking like
public class City {
private final Integer id;
private final String name;
// all-args, toString, getters, equals, and hashcode
}
produces the byte code as following
public java.lang.String toString();
Code:
0: aload_0
1: getfield #7 // Field id:Ljava/lang/Integer;
4: aload_0
5: getfield #13 // Field name:Ljava/lang/String;
8: invokedynamic #17, 0 // InvokeDynamic #0:makeConcatWithConstants:(Ljava/lang/Integer;Ljava/lang/String;)Ljava/lang/String;
13: areturn
public boolean equals(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: if_acmpne 7
5: iconst_1
6: ireturn
7: aload_1
8: ifnull 22
11: aload_0
12: invokevirtual #21 // Method java/lang/Object.getClass:()Ljava/lang/Class;
15: aload_1
16: invokevirtual #21 // Method java/lang/Object.getClass:()Ljava/lang/Class;
19: if_acmpeq 24
22: iconst_0
23: ireturn
24: aload_1
25: checkcast #8 // class edu/forty/bits/records/equals/City
28: astore_2
29: aload_0
30: getfield #7 // Field id:Ljava/lang/Integer;
33: aload_2
34: getfield #7 // Field id:Ljava/lang/Integer;
37: invokevirtual #25 // Method java/lang/Integer.equals:(Ljava/lang/Object;)Z
40: ifne 45
43: iconst_0
44: ireturn
45: aload_0
46: getfield #13 // Field name:Ljava/lang/String;
49: aload_2
50: getfield #13 // Field name:Ljava/lang/String;
53: invokevirtual #31 // Method java/lang/String.equals:(Ljava/lang/Object;)Z
56: ireturn
public int hashCode();
Code:
0: aload_0
1: getfield #7 // Field id:Ljava/lang/Integer;
4: invokevirtual #34 // Method java/lang/Integer.hashCode:()I
7: istore_1
8: bipush 31
10: iload_1
11: imul
12: aload_0
13: getfield #13 // Field name:Ljava/lang/String;
16: invokevirtual #38 // Method java/lang/String.hashCode:()I
19: iadd
20: istore_1
21: iload_1
22: ireturn
On the other hand the record representation for the same
record CityRecord(Integer id, String name) {}
produces the bytecode as less as
public java.lang.String toString();
Code:
0: aload_0
1: invokedynamic #19, 0 // InvokeDynamic #0:toString:(Ledu/forty/bits/records/equals/CityRecord;)Ljava/lang/String;
6: areturn
public final int hashCode();
Code:
0: aload_0
1: invokedynamic #23, 0 // InvokeDynamic #0:hashCode:(Ledu/forty/bits/records/equals/CityRecord;)I
6: ireturn
public final boolean equals(java.lang.Object);
Code:
0: aload_0
1: aload_1
2: invokedynamic #27, 0 // InvokeDynamic #0:equals:(Ledu/forty/bits/records/equals/CityRecord;Ljava/lang/Object;)Z
7: ireturn
Note: To what I could observe on the accessors and constructor byte code generated, they are alike for both the representation and hence excluded from the data here as well.
I did some quick and dirty testing with following
public record PersonRecord(String firstName, String lastName) {}
vs.
import java.util.Objects;
public final class PersonClass {
private final String firstName;
private final String lastName;
public PersonClass(String firstName, String lastName) {
this.firstName = firstName;
this.lastName = lastName;
}
public String firstName() {
return firstName;
}
public String lastName() {
return lastName;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
PersonClass that = (PersonClass) o;
return firstName.equals(that.firstName) &&
lastName.equals(that.lastName);
}
@Override
public int hashCode() {
return Objects.hash(firstName, lastName);
}
@Override
public String toString() {
return "PersonRecord[" +
"firstName=" + firstName +
", lastName=" + lastName +
"]";
}
}
The compiled record file amounts to 1.475 bytes, the class to 1.643 bytes. The size difference probably comes from different equals/toString/hashCode implementations.
Maybe someone can do some bytecode digging...
correct, I agree with [@lugiorgi] and [@Naman], the only difference in the generated bytecode between a record and the equivalent class is in the implementation of methods: toString
, equals
and hashCode
. Which in the case of a record class are implemented using an invoke dynamic (indy) instruction to the same bootstrap method at class: java.lang.runtime.ObjectMethods
(freshly added in the records project). The fact that these three methods, toString
, equals
and hashCode
, invoke the same bootstrap method saves more space in the class file than invoking 3 different bootstraps methods. And of course as already shown in the other answers, saves more space than generating the obvious bytecode
Every object in java has 64 bit of metadata, so an array of objects will consume more memory than an array of records, since the metadata will be attached only into the array reference, not in each record / struct . Moreover the advantage should be in the way the memory of records can be managed from Garbage Collector since it is fixed and contiguous. This is what I understand, if somebody could confirm or add extra information will be very useful. Thanks
© 2022 - 2024 — McMap. All rights reserved.
invokedynamic
to lazily generate the implementations of Object methods (equals, hashCode) instead of generating them statically at compile time. – PochardString
s would use the same invokedynamic trick. But now you can't even changeString.hashCode()
without breaking old binaries. – ToastString
andHashMap
are part of the same (JRE) library, they can use different algorithms under the hood while still delivering the specified value forhashCode()
. There were experiments with alternative hash codes in JDK 7 (and perhaps already in JDK 6), but they were dropped, I suppose, because the results were not worth it… – Spainswitch
ing on Strings.. – ToastString.hashCode()
without breaking old binaries” (because of the way, switching on Strings has been implemented). Why would anyone want to changeString.hashCode()
? To improve hashing, I guess. So I told you, that improving hashing still is possible (and had been attempted in a version that already supported switching on strings). Besides that, the hash code ofString
had been nailed down way earlier than that… – SpainString.hashCode()
would result in a binary incompatibility. IMHO, it is over-specified. If the only guarantee thatString.hashCode()
made is thats.hashCode()
has a constant value for the current JVM process, then it could not have baked into various class files. It maybe could be implemented using SMID instructions. And you don't need to care about keeping it the compatible with compact strings. – Toast