【问题标题】:Do Java Records actually save memory over a similar class declaration or are they more like syntactic sugar?Java Records 是否真的比类似的类声明节省了内存,或者它们更像是语法糖?
【发布时间】:2020-07-27 05:28:42
【问题描述】:

我希望 Java 14 记录实际上比类似的数据类使用更少的内存。

他们或是否使用相同的内存?

【问题讨论】:

  • 如果我理解正确,编译器会生成一个扩展 Record 的最终类,其中包含访问器、实例变量、所需的构造函数以及 toString、hashCode 和 equals 方法。所以我假设使用的内存会非常相似。当然源代码会使用更少的内存;)
  • 您认为节省的内存来自哪里?他们显然仍然需要存储所有组件。
  • 我们使用invokedynamic 懒惰地生成Object 方法(equals、hashCode)的实现,而不是在编译时静态生成它们。
  • @JohannesKuhn 因为StringHashMap 是同一个(JRE)库的一部分,它们可以在底层使用不同的算法,同时仍然为hashCode() 提供指定的值。在 JDK 7 中(可能已经在 J​​DK 6 中)进行了替代哈希码的实验,但我想它们被放弃了,因为结果不值得……
  • @JohannesKuhn 你说“但是现在你甚至不能在不破坏旧二进制文件的情况下更改String.hashCode()”(因为这种方式,已经实现了打开字符串)。为什么有人要更改String.hashCode()?我猜是为了改进散列。所以我告诉过你,改进散列仍然是可能的(并且已经在已经支持切换字符串的版本中尝试过)。除此之外,Stringhad been nailed down way earlier than that的哈希码……

标签: java memory-footprint java-14 java-record


【解决方案1】:

除了执行by @lugiorgi 的基本分析之外,我还可以在分析字节码时得出类似的显着差异,即toStringequalshashcode 的实现。

一方面,具有覆盖 Object 类 API 的现有类看起来像

public class City {
    private final Integer id;
    private final String name;
    // all-args, toString, getters, equals, and hashcode
}

产生如下字节码

 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

另一方面,相同的记录表示

record CityRecord(Integer id, String name) {}

产生的字节码少于

 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

注意:根据我在生成的访问器和构造器字节码上观察到的情况,它们在表示上都是相似的,因此也被排除在此处的数据之外。

【讨论】:

【解决方案2】:

我做了一些快速而肮脏的测试

public record PersonRecord(String firstName, String lastName) {}

对比

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 +
                "]";
    }
}

编译后的记录文件为1.475字节,类为1.643字节。大小差异可能来自不同的 equals/toString/hashCode 实现。

也许有人可以做一些字节码挖掘...

【讨论】:

    【解决方案3】:

    正确,我同意 [@lugiorgi] 和 [@Naman],记录和等效类之间生成的字节码的唯一区别在于方法的实现:toStringequalshashCode .在记录类的情况下,使用调用动态(indy)指令实现类中相同的引导方法:java.lang.runtime.ObjectMethods(新添加到记录项目中)。事实上,toStringequalshashCode 这三个方法调用相同的引导方法比调用 3 个不同的引导方法在类文件中节省了更多空间。当然,正如其他答案中已经显示的那样,比生成明显的字节码节省更多空间

    【讨论】:

      【解决方案4】:

      java 中的每个对象都有 64 位元数据,因此对象数组将比记录数组消耗更多内存,因为元数据将仅附加到数组引用中,而不是附加到每个记录/结构中。此外,优势应该在于可以从垃圾收集器管理记录内存的方式,因为它是固定且连续的。这就是我的理解,如果有人可以确认或添加额外信息将非常有用。谢谢

      【讨论】:

        猜你喜欢
        • 2016-01-26
        • 1970-01-01
        • 2016-05-11
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2014-05-12
        • 1970-01-01
        • 2012-07-06
        相关资源
        最近更新 更多