我们应该避免使用Java创建对象吗?


242

一位同事告诉我,在Java对象创建中,您可以执行的最昂贵的操作。因此,我只能得出结论:创建尽可能少的对象。

这似乎在某种程度上破坏了面向对象编程的目的。如果我们不创建对象,那么我们只是在编写一种长类C样式以进行优化?


39
“创建Java对象是您可以执行的最昂贵的操作”。介意分享该声明的来源吗?
松戈2012年

92
与-最昂贵的操作相比,是什么?与计算pi到900位数相比还是与增加int相比?
贾森克2012年

4
他们可能一直在谈论特定对象创建的特定部分吗?我在想苹果如何使用tableViewCells的队列。也许您的同事建议创建一些对象并由于与那些特定对象相关的一些开销而重用它们?只是想弄清楚为什么他们会提出这样的要求。
泰勒·德威特

3
这是我听说过的关于编程的最有趣的事情之一。)
Mert Akcakaya 2012年

22
算术也很昂贵。我们应该避免计算,哦,启动JVM确实很昂贵,因此我们不应该启动任何JVM :-)。
詹姆斯·安德森

Answers:


478

您的同事不知道他们在说什么。

您最昂贵的手术就是听他们的。他们浪费了您的时间,将您误导到已经过期十年(截至发布此答案的原始日期)的信息上,以及您不得不花时间在此处发布信息并研究互联网真相。

希望他们只是在无知地反省自己十多年前听到或读过的东西,而他们的情况再好不过了。我也将他们说的话当作可疑的东西,这对任何一种保持最新状态的人来说都是众所周知的谬论。

一切都是对象(除外primitives

除了基元(int, long, double,等)以外的所有东西都是Java中的对象。无法避免使用Java创建对象。

在大多数情况下,由于Java的内存分配策略,在Java中创建对象的速度比C ++快,并且与JVM中的所有其他功能相比,从所有实际目的出发,都可以认为它是“免费的”

早在1990年代末2000年代初,JVM实现确实在对象的实际分配中有一些性能开销。至少从2005年开始就没有这种情况了。

如果您调整-Xms以支持应用程序正常运行所需的所有内存,则在现代GC实现中,GC可能永远不必运行并清除大部分垃圾,因此寿命很短的程序可能根本就不会使用GC。

它不会尝试最大化可用空间,无论如何这都是一条红鲱鱼,它可以最大化运行时性能。如果这意味着JVM Heap几乎一直都是100%分配的,那就这样吧。可用的JVM堆内存无论如何都不会给您任何好处。

有人误以为GC将以一种有用的方式将内存释放回系统的其余部分,这完全是错误的!

JVM堆不会增长和收缩,因此JVM Heap中的可用内存会对系统的其余部分产生积极影响。-Xms在启动时分配所有指定的内容,其启发式方法是在JVM实例完全退出之前,不要将任何内存真正释放回OS与其他OS进程共享。-Xms=1GB -Xmx=1GB不管在给定时间实际创建了多少个对象,都会分配1GB的RAM。有一些设置允许释放一定百分比的堆内存,但是出于所有实际目的,JVM永远无法释放足够的内存来实现这一点因此没有其他进程可以回收此内存,因此JVM Heap的空闲也无法使系统的其余部分受益。2006年11月29日“接受”了此申请的RFE ,但是对此一无所获。权威人士不认为这是行为。

有一个误解,即创建许多小的短期对象会导致JVM长时间暂停,这现在也是错误的

实际上,当前的GC算法已经过优化,可以创建许多寿命很短的小对象,这基本上是每个程序中Java对象的99%启发式。在大多数情况下,尝试进行对象池化实际上会使JVM的性能变差。

今天唯一需要池化的对象是引用JVM 外部有限资源的对象。套接字,文件,数据库连接等,可以重复使用。常规对象的池化含义与允许您直接访问内存位置的语言的含义不同。对象缓存是一个不同的概念,并且可能不是某些人幼稚地称为“ 池化 ”的概念,这两个概念不是同一回事,因此不应混为一谈。

现代的GC算法不存在此问题,因为它们不按计划进行分配,而是在特定一代中需要空闲内存时进行分配。如果堆足够大,则不会发生足够长的释放以引起任何暂停。

面向对象的动态语言在计算敏感测试方面甚至击败了C。


274
+1:“您最昂贵的手术就是听他们的话……”。我听到一段时间以来最好的。
rjnilsson

21
@DeadMG,即使有累积的GC开销,Java 可以比C ++更快(例如,由于堆压缩,可以将某些数据结构的高速缓存未命中最小化)。
SK-logic

13
@ SK-logic:由于GC是不确定的,因此充其量很难真正证明这一点。至于最大程度地减少缓存未命中,当每个对象必须是另一个间接对象时,很难做到这一点,这会浪费缓存空间并增加执行时间。但是,我认为,只需在C ++中使用适当的分配器(如对象池或内存竞技场),就可以轻松匹配或击败垃圾收集器的性能。例如,您可以编写一个内存竞技场类,该类的执行速度将比_alloca摊销的要快。
DeadMG

33
现在,对象创建比以前便宜。它不是免费的。谁告诉你那是在说谎。关于面向对象语言击败C的链接是对真正尝试学习的人的夸张回应。
贾森克2012年

17
由于这些答案,我们最终得到了糟糕的代码。正确的答案是用Java创建对象既要创建Java对象又要对其进行初始化。第一部分是便宜,第二可以是非常昂贵的。人们new在热点地区使用关键字之前,应始终查看构造函数中发生的情况。我见过人们new ImageIcon(Image)paint()Swing对象的方法中使用它,这是非常昂贵的,并且使整个UI变得迟钝。因此,这不是一个黑白答案,请在使用new某个地方之前先考虑一下。
qwertzguy

94

底线:不要为了获得创建对象的捷径而牺牲设计。避免不必要地创建对象。如果可行,请进行设计以避免冗余操作(任何形式)。

与大多数答案相反-是的,对象分配确实有相关的成本。这是一种低成本,但是您应该避免创建不必要的对象。与应该避免代码中不必要的内容相同。大对象图使GC变慢,意味着执行时间更长,因为您可能将要进行更多的方法调用,触发更多的CPU缓存未命中,并增加在低RAM情况下将进程交换到磁盘的可能性。

在有人大声疾呼这是一个极端情况之前,我已经分析了一些应用程序,这些应用程序在优化之前创建了20 + MB的对象,以便处理约50行数据。这在测试中很好,直到您每分钟最多扩展一百个请求,然后突然每分钟创建2GB数据。如果您想以每秒20请求的速度执行操作,则需要创建400MB的对象,然后将其丢弃。对于一个体面的服务器来说,每秒20请求是很小的。


7
我可以补充一个例子:在某些情况下,它确实使得代码清晰度方面没有差别,但可以使性能或多或少很大的区别:从流读取时,例如,它是很常见的使用像while(something) { byte[] buffer = new byte[10240]; ... readIntoBuffer(buffer); ...这可能与...相比浪费byte[] buffer = new byte[10240]; while(something) { ... readIntoBuffer(buffer); ...
JimmyB 2012年

4
+1:不必要的对象创建(或者更确切地说是删除后的清理)有时肯定会很昂贵。Java中的3D图形/ OpenGL代码是我看到的一种优化方法,它可以最大程度地减少创建的对象数量,因为GC否则会对帧速率造成严重破坏。
狮子座2012年

1
哇!20+ MB可以处理50行数据?听起来很疯狂。无论如何,这些对象是长寿的吗?因为对于GC,情况就是如此。另一方面,如果您只是在讨论内存需求,那与垃圾回收或对象创建效率无关...
Andres F.

1
对象是短暂的(通常情况下少于0.5秒)。该垃圾量仍然会影响性能。
贾森克2012年

2
感谢您对现实的坚定支持,任何受过深思熟虑的建筑影响的人都可以与之建立联系。
JM Becker 2016年

60

实际上,由于Java语言(或任何其他托管语言)使内存管理策略成为可能,因此对象创建只不过是增加称为年轻代的内存块中的指针而已。它比必须搜索空闲内存的C快得多。

成本的另一部分是对象销毁,但是很难与C进行比较。集合的成本基于长期保存的对象数量,但是集合的频率取决于创建的对象数量...在最后,它仍然比C风格的内存管理要快得多。


7
+1即使垃圾收集器“可以正常工作”,每个Java程序员都应该了解分代垃圾收集器。
benzado 2012年

18
较新版本的Java可以进行转义分析,这意味着它可以为不对堆栈上的方法进行转义的对象分配内存,从而可以免费清理它们-垃圾收集器不必处理这些对象,当展开方法的堆栈框架(方法返回时)时,它们将被自动丢弃。
杰斯珀(Jesper)2012年

4
转义分析的下一步是仅在寄存器中为小对象“分配”内存(例如,一个Point对象可以容纳2个通用寄存器)。
约阿希姆·绍尔

6
@Joachim Sauer:就像在HotSpot VM的较新实现中那样。这称为标量替换。
someguy 2012年

1
@someguy:前一段时间我已经读过它,但是没有继续检查它是否已经完成。得知我们已经有了这个消息,这真是一个好消息。
约阿希姆·绍尔

38

其他张贴者正确地指出,在Java中对象创建非常快,并且在任何普通的Java应用程序中通常都不必担心它。

有一对夫妇的非常特殊的情况下是避免对象创建一个好主意。

  • 当您编写对延迟敏感的应用程序并希望避免GC暂停时。生成的对象越多,发生的GC越多,暂停的机会就越大。对于游戏,某些媒体应用程序,机械手控制,高频交易等,这可能是一个有效的考虑因素。解决方案是预先分配您需要的所有对象/数组,然后重新使用它们。有专门提供这种功能的库,例如Javolution。但是可以说,如果您真的关心低延迟,那么应该使用C / C ++ /汇编程序,而不是Java或C#:-)
  • 在某些情况下,避免使用盒装原语(Double,Integer等)可能是非常有益的微优化。由于未装箱的原语(double,int等)避免了每个对象的开销,因此它们是CPU密集型工作,例如数值处理等速度更快。通常,原语数组在Java中的性能很好,因此您希望将它们用于数字运算而不是数字运算任何其他种类的物体。
  • 受限制的内存情况下,由于每个对象的开销很小(取决于JVM的实现,通常为8-16字节),因此您希望减少创建的(活动)对象的数量。在这种情况下,您应该选择少量的大对象或数组来存储数据,而不要使用大量的小对象。

3
值得注意的是,转义分析将允许将短暂(有生命)的对象存储在堆栈中,可以免费释放这些对象ibm.com/developerworks/java/library/j-jtp09275/index.html
Richard Tingle

1
@Richard:重点-逃逸分析提供了一些非常好的优化。但是,您必须谨慎地依赖它:它不能保证在所有Java实现中和所有情况下都发生。因此,您通常需要进行基准测试来确定。
mikera 2013年

2
同样,100%正确的逃逸分析等效于停止问题。不要在复杂的情况下依赖逃逸分析,因为它至少在某些时候可能会给出错误的答案。
Jules

第一点和最后一点不适用于快速释放的小对象,它们死于eden(认为C语言中的堆栈分配,几乎是免费的),并且永远不会影响GC时间或内存分配。Java中的大多数对象分配都属于此类,因此基本上是免费的。第二点仍然有效。
比尔K

17

您的同事所说的话有道理。我谨此建议,对象创建的问题实际上是垃圾回收。在C ++中,程序员可以精确地控制内存的重新分配方式。该程序可以根据需要累积任意长或短的累积时间。此外,C ++程序可以使用与创建该线程不同的线程丢弃该线程。因此,当前正在工作的线程永远不必停止清理。

相反,Java虚拟机(JVM)会定期停止您的代码以回收未使用的内存。大多数Java开发人员从未注意到此暂停,因为该暂停通常很少且非常短。您积累的原始数据越多或JVM的约束越多,这些暂停就越频繁。您可以使用VisualVM之类的工具来可视化此过程。

在Java的最新版本中,可以对垃圾回收(GC)算法进行调整。通常,您想暂停的时间越短,虚拟机的开销(即协调GC进程所花费的CPU和内存)就越昂贵。

什么时候可能重要?每当您关心一致的亚毫秒级响应率时,您都会关心GC。用Java编写的自动交易系统会严重调整JVM,以最大程度地减少暂停时间。在系统必须始终保持高度响应的情况下,否则会编写Java的公司会转向C ++。

出于记录,我一般宽容避免对象!默认为面向对象的编程。仅当GC妨碍您时才调整此方法,然后才尝试将JVM调整为暂停较少的时间。关于Java性能调优的一本好书是Charlie Hunt和Binu John 撰写的Java Performance


10
大多数现代JVM支持的垃圾收集算法要比“停止世界”更好。垃圾回收所花费的摊销时间通常少于显式调用malloc / free所花费的时间。C ++内存管理是否也在单独的线程中运行?

10
Oracle的较新Java版本可以进行转义分析,这意味着不会对方法进行转义的对象将分配到堆栈上,这使它们的清理工作变得自由-当方法返回时,它们将自动释放。
Jesper 2012年

2
@ThorbjørnRavnAndersen:真的吗?您是真正的意思是停顿的GC,还是“通常是并行的但有时会暂停一点”的GC?我不明白GC如何永远都不会暂停程序,却又无法同时移动对象……从字面上看,这对我来说似乎是不可能的……
Mehrdad 2012年

2
@ThorbjørnRavnAndersen:它如何“停止世界”却从不“暂停JVM”?那没有道理……
梅尔哈德

2
@ThorbjørnRavnAndersen:不,我真的不;我只是不明白你在说什么。GC有时会暂停程序,在这种情况下,顾名思义,它不是“无暂停”,或者它永远不会暂停程序(因此,它是“无暂停”),在这种情况下,我不知道该怎么做对应于您的“它无法跟上它就会停止世界”的语句,因为这似乎意味着GC运行时程序已暂停。您介意说明哪种情况(是否不停顿吗?),以及如何做到这一点?
Mehrdad


9

GC已针对许多短期对象进行了调整

也就是说,如果您可以减少对象分配,则应该

一个示例是在循环中构建字符串,幼稚的方式是

String str = "";
while(someCondition){
    //...
    str+= appendingString;
}

String在每个+=操作上创建一个新对象(加上a StringBuilder和新的基础char数组)

您可以轻松地将其重写为:

StringBuilder strB = new StringBuilder();
while(someCondition){
    //...
    strB.append(appendingString);
}
String str = strB.toString();

这种模式(不变的结果和局部可变的中间值)也可以应用于其他事物

但除此之外,您应该拉起探查器以找到真正的瓶颈,而不是追逐幽灵


这种StringBuilder方法的最大优点是预先设置了大小StringBuilder因此它不必StringBuilder(int)构造函数重新分配基础数组。这使其成为单个分配,而不是1+N分配。

2
@JarrodRoberson StringBuilder将至少使当前容量增加一倍,换句话说,仅对log(n)分配,容量将成倍增长
棘手怪胎2012年

6

Joshua Bloch(Java平台创建者之一)在2001年的有效Java一书中写道:

除非维护池中的对象非常重,否则通过维护自己的对象池来避免创建对象是一个坏主意。证明对象池合理的对象的典型示例是数据库连接。建立连接的成本非常高,以至于可以重用这些对象。但是,一般而言,维护自己的对象池会使代码混乱,增加内存占用,并损害性能。现代JVM实现具有高度优化的垃圾收集器,这些垃圾收集器在轻量对象上的性能很容易超过此类对象池。


1
即使有了网络连接之类的东西,重要的也不是维护与连接关联的GC分配的对象,而是要维护GC管理的世界之外存在的一组连接。有时候,合并对象本身可能会有所帮助。其中大多数源于以下情况:对同一个对象的一对引用在语义上等效于对相同但又不同的对象的一对引用,但是仍然具有一些优点。例如,可以检查两个引用相同的五万个字符的字符串是否相等……
supercat

2
...比引用两个相同长度但截然不同的字符串要快得多。
2014年

5

这实际上取决于特定的应用程序,因此通常很难说。但是,如果对象创建实际上是应用程序中的性能瓶颈,我会感到非常惊讶。即使它们很慢,代码风格的好处也可能会超过性能(除非它实际上对用户而言是显而易见的)

无论如何,在分析了代码以确定实际的性能瓶颈而不是猜测之前,您甚至不必担心这些事情。在此之前,您应该为代码的可读性而不是性能做任何最好的事情。


在Java的早期版本中,垃圾收集器清理丢弃的对象会产生大量成本。Java的最新版本大大增强了GC选项,因此对象创建很少成为问题。通常,Web系统受到对外部系统的IO调用的限制,除非您在应用程序服务器上计算真正复杂的内容作为页面加载的一部分。
greg 2012年

3

我认为您的同事必须从不必要的对象创建的角度说过。我的意思是,如果您经常创建同一对象,则最好共享该对象。即使在对象创建很复杂并且占用更多内存的情况下,您也可能希望克隆该对象并避免创建复杂的对象创建过程(但这取决于您的要求)。我认为“对象创建成本很高”这一说法应结合上下文来考虑。

就JVM内存需求而言,请等待Java 8,甚至不需要指定-Xmx,元空间设置将满足JVM内存需求,并且它会自动增长。


如果人们在拒绝任何答案的同时也发表评论,那将是非常有建设性的。毕竟,我们所有人都在这里共享知识,仅仅在没有适当理由的情况下做出判断就不会有任何目的。
AKS

1
当对某个答案发表评论时,堆栈交换应具有某种机制来通知拒绝该答案的用户。
AKS

1

除了分配内存之外,创建类还需要更多。还有初始化,我不确定为什么所有答案都没有涵盖该部分。普通类包含一些变量,并进行某种形式的初始化,这不是免费的。根据类是什么,它可能会读取文件或执行其他任何数量的缓慢操作。

因此,在确定它是否免费之前,只需考虑类构造函数的作用。


2
出于这个原因,一个设计良好的类不应在其构造函数中费力。例如,您的文件读取类可以通过仅在启动时验证其目标文件是否存在并将所有实际的文件操作推迟到需要从文件中获取数据的第一个方法调用之前,降低其实例化成本。
GordonM 2012年

2
如果将额外费用移至“ if(firstTime)”,则比在循环中重新创建类要比重用该类花费更多。
亚历克斯(Alex)

我将要发布一个类似的答案,我相信这是正确的答案。这么多人对这些说法视而不见,因为创建Java对象很便宜,以至于他们没有意识到通常构造函数或进一步的初始化可能并不便宜。例如,Swing中的ImageIcon类,即使您将其传递给预加载的Image对象,其构造函数也非常昂贵。而且我不同意@ GordonM,JDK中的许多类都在构造函数中执行大部分init,并且我认为它使代码更精简,设计更好。
qwertzguy

1

实际上,Java的GC在以“突发”方式快速创建许多对象方面进行了非常优化。据我了解,他们使用顺序分配器(用于可变大小请求的最快和最简单的O(1)分配器)将这种“突发周期”插入到称为“ Eden空间”的内存空间中,并且仅当对象持久存在时在GC周期结束后,它们会被移到GC可以一次收集它们的地方。

话虽如此,如果您的性能需求变得足够关键(以实际的用户端要求衡量),则对象确实会产生开销,但就创建/分配而言,我不会这么想。它与引用的局部性以及Java中支持反射和动态分派等概念所需的所有对象的额外大小有关(Float大于float,通常比64位大4倍,具有对齐要求,并且Float根据我的理解,不一定保证数组是连续存储的)。

我见过的用Java开发的最引人注目的内容之一就是使我认为它是我的领域(VFX)的重量级竞争者,它是一种交互式多线程标准路径跟踪器(不使用辐照度缓存或BDPT或MLS或其他任何东西) CPU提供实时预览,可以很快地收敛到无噪点的图像。我曾与C ++专业人士一起工作,他们的职业生涯都是由花哨的分析器完成的,他们很难做到这一点。

但是我仔细查看了源代码,尽管它以微不足道的代价使用了很多对象,但路径跟踪器的最关键部分(BVH,三角形和材质)非常清楚并故意避免使用对象,而推荐使用大类型的原始类型(大多float[]int[]),这使得它使用少得多的存储器和保证空间局部性从一个得到float阵列到下一个英寸 我认为如果作者使用喜欢Float在那里,它的性能将付出相当大的代价。但是,我们谈论的是引擎最关键的部分,鉴于开发人员熟练地对其进行了优化,我可以肯定的是,他非常明智地对其进行了测量和应用,因为他乐于在其他地方使用对象。他令人印象深刻的实时路径跟踪器的花费微不足道。

一位同事告诉我,在Java对象创建中,您可以执行的最昂贵的操作。因此,我只能得出结论:创建尽可能少的对象。

即使在像我一样对性能至关重要的领域中,如果您在无关紧要的地方束手无策,也不会编写高效的产品。我什至可以断言,最关键的性能领域可能会对生产力提出更高的要求,因为我们需要所有额外的时间,从而可以通过浪费时间来解决那些真正重要的热点, 。就像上面的路径跟踪器示例一样,作者巧妙而明智地仅将这种优化应用于真正重要的地方,并且可能在事后测量之后,仍然很高兴地在其他地方使用了对象。


1
您的第一段在正确的轨道上。伊甸园和年轻空间是使用副本收集器,具有大约。死物费用为0。我强烈推荐这个演讲。顺带一提,您意识到这个问题来自2012年,对吧?
JimmyJames

@JimmyJames我很无聊,喜欢只筛选一些问题,包括老问题。我希望人们不要介意我的巫术!:-D
Dragon Energy

@JimmyJames装箱的类型不再需要额外的内存并且不能保证在数组中是连续的吗?我倾向于认为对象在Java中非常便宜,但一种情况可能相对昂贵,例如float[]vs. Float[],顺序处理一百万个对象可能比第二种要快得多。
Dragon Energy

1
当我第一次开始在这里发表文章时,我也是这样做的。当对旧问题的答案往往很少受到关注时,请不要感到惊讶。
JimmyJames

1
我很确定应该假定盒装类型比原始类型使用更多的空间。围绕它可能存在优化,但总的来说,我认为这正是您所描述的。上面的演示文稿中的Gil Tene还有另一个关于添加特殊种类的集合的提议的讨论,该集合将提供结构的一些好处,但仍然使用对象。这是一个有趣的想法,我不确定JSR的状态。成本很大程度上取决于时间,这就是您所暗示的。如果您可以避免让您的对象“转义”,则甚至可以为它们分配堆栈,并且永远不要触摸堆。
JimmyJames

0

正如人们所说的那样,对象创建在Java中并不是一个大代价(但是我敢打赌,它比大多数简单的操作(如加法等)还要大),并且您不应该避免过多使用它。

那仍然是一个代价,有时您可能会发现自己试图报废尽可能多的对象。但是仅在剖析表明这是一个问题之后。

这是有关该主题的精彩演讲:https : //www.cs.virginia.edu/kim/publicity/pldi09tutorials/memory-ficient-java-tutorial.pdf


0

我对此做了一个快速的微基准测试,并在github中提供了完整的源代码。我的结论是,创建对象是否昂贵并不成问题,但是连续创建对象时,GC会为您处理所有事情,这将使您的应用程序更快地触发GC进程。GC是一个非常昂贵的过程,最好在可能的情况下避免使用它,而不要尝试将其插入。


这似乎并没有提供任何对之前15个答案中提出和解释的观点的实质性建议。几乎没有一个内容值得颠簸这是2年多前提出,并已经得到了一个问题,很完整的答案
蚊蚋
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.