使用不可变对象的程序性能

Performance of programs using immutable objects

使用不可变对象编写程序会导致性能问题吗?如果一个给定的对象是不可变的,而我们需要以某种方式改变它的状态,我们必须将它映射到一个状态略有改变的新对象。因此,我们会发现自己处于创建大量占用内存的对象的情况,据我所知,这可能会给垃圾收集器带来问题。我所描述的事情是否正在发生,或者是否有我不知道的关于这个主题的某些方面?

当你重复修改一个可变对象时,它产生的垃圾可能比重复构造新的不可变对象来表示中间状态要少。但是使用不可变对象仍然不一定会带来性能问题有几个原因:

  • 在典型的应用程序中,与不可变对象不会受到影响甚至最终获胜的其他用途相比,这种情况只是偶尔发生。最值得注意的是:

    • getter 在返回不可变对象时不需要创建防御副本
    • 如果不可变,setter 可以通过引用存储传入的参数
    • 验证方法可以以简单(或幼稚)的方式实现,而不必处理 check-then-act 问题,因为不可变对象在检查和后续使用之间无法更改
    • 可以安全地共享不可变对象,以实际减少创建对象或使用内存的数量
  • 垃圾回收对性能的影响往往被高估了。我们经常使用不可变类型 java.lang.String,受益于上述优点。字符串也是最常用的哈希映射键之一。

    对于重复字符串操作的场景,有可变的伴侣class StringBuilder,但它的主要优点是关于分配对象的数量。字符串构造的问题是每个对象都必须创建包含字符的副本,因此在每一步构造新字符串时,重复连接字符等操作会导致二次时间复杂度。 StringBuilder 仍会在必要时在引擎盖下重新分配其缓冲区,但会出现非线性增长,这会产生重复串联的分摊线性时间复杂度。

    中所述,垃圾收集的成本主要取决于仍然存在的对象,因此临时对象通常不会对垃圾收集产生太大影响,除非您创建了过多的临时对象。但即使是后一种情况,也只有在您遇到实际性能问题并且公正的分析工具证明特定分配站点确实是罪魁祸首时才应解决。

  • 复杂的应用程序可能必须处理 undo/redo 或其他版本控制功能,无论如何都需要保留替代应用程序状态的副本。在这一点上,使用不可变对象实际上可能成为一个优势,因为您不需要复制在两个版本的应用程序状态之间没有变化的对象。更改后的应用程序状态可能包含 99% 与先前状态相同的对象。

  • 该主题(再次)流行的一个重要原因是不可变对象提供了实现高效和正确并行处理的最简单方法。无需获取锁来防止不一致的修改。如上所述,check-then-act的问题不用担心。此外,当应用程序状态可以表示为对复合不可变对象的单个引用时,对并发更新的检查减少为简单的引用比较。也与 .

    比较

因此,不可变对象有很多性能优势,如果有的话,它们可以弥补缺点。同时,可以通过临时处理操作的可变对象同时保持整体不可变行为的既定解决方案来解决潜在的瓶颈。