Do Java Records actually save memory over a similar class declaration or are they more like syntactic sugar?
Asked Answered
E

4

13

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?

Exosmosis answered 14/4, 2020 at 13:7 Comment(9)
If I understood it correctly the compiler generates a final class extending Record with accessors, the instance variables, the needed constructor and toString, hashCode and equals methods. So I assume the memory used would be very similar. Of course the source code would use less memory ;)Offspring
Where do you think the memory savings would come from? They would obviously still have to store all the components.Pochard
@BrianGoetz That's understood. If you don't mind answering a subsequent question - I was wondering about the difference in bytecode representation and invokedynamic constants used in there. (Is there a way to find the value for all these constants within or outside JDK?). If there is a good amount of details to be understood here, I would be happy to create another Q&A here.Melodize
We use invokedynamic to lazily generate the implementations of Object methods (equals, hashCode) instead of generating them statically at compile time.Pochard
Wish switching on Strings would use the same invokedynamic trick. But now you can't even change String.hashCode() without breaking old binaries.Toast
@JohannesKuhn since String and HashMap are part of the same (JRE) library, they can use different algorithms under the hood while still delivering the specified value for hashCode(). 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…Spain
@Spain I don't see how this is relevant for switching on Strings..Toast
@JohannesKuhn you said “But now you can't even change String.hashCode() without breaking old binaries” (because of the way, switching on Strings has been implemented). Why would anyone want to change String.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 of String had been nailed down way earlier than thatSpain
Changing the specification is possible - sometimes a new method, sometimes methods are removed or the specification is changed to match a long standing implementation. But now changing String.hashCode() would result in a binary incompatibility. IMHO, it is over-specified. If the only guarantee that String.hashCode() made is that s.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
M
8

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.

Melodize answered 14/4, 2020 at 15:24 Comment(1)
This is also mentioned in the Peculiarities of Records.Melodize
O
2

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

Offspring answered 14/4, 2020 at 14:8 Comment(1)
Byte code does not necessarily reflect the actual code being run by the JVM .Capriole
W
2

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

Wordless answered 30/4, 2020 at 4:59 Comment(0)
A
2

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

Appointor answered 14/1, 2021 at 19:0 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.