Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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?

like image 277
Clancy Merrick Avatar asked Apr 14 '20 13:04

Clancy Merrick


People also ask

Does Java have record type?

Like enum , record is also a special class type in Java. It is intended to be used in places where a class is created only to act as plain data carrier.

What is a Java record?

In Java, a record is a special type of class declaration aimed at reducing the boilerplate code.

Is Java record immutable?

Records are immutable data classes that require only the type and name of fields. The equals, hashCode, and toString methods, as well as the private, final fields and public constructor, are generated by the Java compiler.

Can Java records be extended?

Record classes are final so they cannot be extended.


4 Answers

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

like image 166
Salvatore Pannozzo Capodiferro Avatar answered Oct 25 '22 09:10

Salvatore Pannozzo Capodiferro


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.

like image 24
Naman Avatar answered Oct 25 '22 07:10

Naman


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

like image 44
lugiorgi Avatar answered Oct 25 '22 07:10

lugiorgi


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

like image 39
Vicente Romero Avatar answered Oct 25 '22 08:10

Vicente Romero