我正在接管前一位开发人员的一些申请。当我通过Eclipse运行应用程序时,我看到内存使用情况和堆大小增加了很多。经过进一步调查,我发现他们正在循环中创建一个对象以及其他东西。
我开始经历一些清理工作。但是我经历的越多,我就越想问“这实际上会做什么吗?”
例如,不是在上面提到的循环之外声明变量而只是在循环中设置它的值......它们在循环中创建了对象。我的意思是:
for(int i=0; i < arrayOfStuff.size(); i++) {
String something = (String) arrayOfStuff.get(i);
...
}
与
String something = null;
for(int i=0; i < arrayOfStuff.size(); i++) {
something = (String) arrayOfStuff.get(i);
}
我是不是说底部循环更好?也许我错了。
另外,在上面的第二个循环之后,我将“something”设置为null?这会清除一些记忆吗?
在任何一种情况下,我可以遵循哪些良好的内存管理最佳实践,这将有助于在我的应用程序中保持较低的内存使用率?
更新
到目前为止,我感谢每个人的反馈。但是,我并没有真正询问上面的循环(尽管根据你的建议,我确实回到了第一个循环)。我正在努力获得一些我可以留意的最佳实践。 “当你完成使用Collection时,清除它”的界限。我真的需要确保这些应用程序不会占用太多内存。
答案 0 :(得分:37)
不要试图超越虚拟机。第一个循环是建议的最佳实践,包括性能和可维护性。在循环之后将引用设置回null将不保证立即释放内存。当您使用最小范围时,GC将发挥最佳作用。
详细介绍这些内容的书籍(从用户的角度来看)是Effective Java 2和Implementation Patterns。
如果您想了解有关性能和VM内部的更多信息,您需要查看来自Brian Goetz的讲座或阅读书籍。
答案 1 :(得分:9)
除something
的范围外,这两个循环是等效的;有关详细信息,请参阅this question。
一般最佳做法?嗯,让我们看看:除非你有充分的理由,否则不要在静态变量中存储大量数据。完成后,从集合中删除大对象。哦,是的,“措施,不要猜。”使用分析器查看内存的分配位置。
答案 2 :(得分:8)
两个代码示例中都没有创建对象。您只需将对象引用设置为arrayOfStuff中已有的字符串。记忆力没有区别。
答案 3 :(得分:5)
两个循环将使用基本相同的内存量,任何差异都可以忽略不计。 “String something”仅创建对象的引用,而不是自身的新对象,因此使用的任何额外内存都很小。此外,编译器/与JVM结合可能会优化生成的代码。
对于内存管理实践,您应该尝试更好地分析内存,以了解实际存在的瓶颈。特别注意指向大块内存的静态引用,因为它永远不会被收集。
您还可以查看弱引用和其他专门的内存管理类。
最后,请记住,如果应用程序占用内存,可能有原因....
更新内存管理的关键是数据结构,以及您需要/何时需要多少性能。权衡通常在内存和CPU周期之间进行。
例如,缓存可以占用大量内存,特别是为了提高性能,因为您试图避免昂贵的操作。
因此,请仔细考虑您的数据结构,并确保您不会将内容保留在内存中的时间超过必须的时间。如果它是一个Web应用程序,请避免将大量数据存储到会话变量中,避免对大量内存池进行静态引用等。
答案 4 :(得分:5)
JVM最擅长释放短期对象。尽量不要分配不需要的对象。但在了解工作负载,对象生存期和对象大小之前,无法优化内存使用情况。分析器可以告诉你这个。
最后,你必须避免做的第一件事:永远不要使用终结器。终结器会干扰垃圾收集,因为对象不能只是被释放,而是必须排队等待最终确定,这可能会也可能不会发生。最好不要使用终结器。
至于你在Eclipse中看到的内存使用情况,它并不一定相关。 GC将根据可用内存量来完成其工作。如果您有大量可用内存,则在关闭应用程序之前可能看不到单个GC。如果您发现您的应用程序内存不足,那么只有真正的分析器可以告诉您泄漏或效率低下的位置。
答案 5 :(得分:4)
第一个循环更好。因为
但是从记忆的角度来看,这是无关紧要的。
如果你有内存问题,那么你应该分析它的消费地点。
答案 6 :(得分:4)
在我看来,你应该避免像这样的微优化。它们耗费了大量的脑循环,但大部分时间都没有什么影响。
您的应用程序可能有一些中央数据结构。那些是你应该担心的。例如,如果填充它们,则使用良好的大小估计来预分配它们,以避免重复调整底层结构的大小。这尤其适用于StringBuffer
,ArrayList
,HashMap
等。设计好您对这些结构的访问权限,这样您就不必复制很多。
使用适当的算法访问数据结构。在最低级别,如您提到的循环,使用Iterator
,或者至少避免一直调用.size()
。 (是的,你每次都要询问列表的大小,大部分时间都不会改变。)顺便说一句,我经常看到Map
的类似错误。人们迭代keySet()
和get
每个值,而不是首先迭代entrySet()
。内存管理器会感谢你额外的CPU周期。
答案 7 :(得分:2)
正如上面提到的一张海报,使用分析器来测量程序某些部分的内存(和/或cpu)使用情况,而不是试图猜测它。您可能会对所发现的内容感到惊讶!
此外还有一个额外的好处。您将更了解您的编程语言和应用程序。
我使用VisualVM进行性能分析并大力推荐它。它带有jdk / jre发行版。
答案 8 :(得分:1)
嗯,第一个循环实际上更好,因为某些东西的范围更小。关于内存管理 - 它没有太大区别。
当您将对象存储在集合中但忘记删除它们时,大多数Java内存问题都会出现。否则GC会使他的工作非常好。
答案 9 :(得分:1)
第一个例子很好。除了每次循环的堆栈变量分配和释放(非常便宜和快速)之外,没有任何内存分配。
原因是所有被“分配”的都是一个引用,它是一个4字节的堆栈变量(无论如何在大多数32位系统上)。堆栈变量通过添加到表示堆栈顶部的内存地址来“分配”,因此非常快速且便宜。
你需要注意的是:
这样的循环for (int i = 0; i < some_large_num; i++)
{
String something = new String();
//do stuff with something
}
因为那实际上正在进行内存分配。
答案 10 :(得分:1)
如果您还没有,我建议安装Eclipse Test & Performance Tools Platform(TPTP)。如果要转储并检查堆,请查看SDK jmap and jhat工具。另请参阅Monitoring and Managing Java SE 6 Platform Applications。
答案 11 :(得分:0)
“我不正确地说底部循环更好吗?”,答案是否定的,不仅更好,在同样的情况下是必要的......变量定义(不是内容),是在内存堆中进行的,并且是有限的,在第一个例子中,每个循环在这个内存中创建一个实例,如果“arrayOfStuff”的大小很大,可能会出现“Out of memory error:java heap space”....
答案 12 :(得分:0)
据我了解你所看到的,底部循环并不是更好。原因是,即使您尝试重用单个引用(例如),事实是对象(Ex - arrayOfStuff.get(i))仍然从List(arrayOfStuff)引用。要使对象符合收集条件,不应从任何地方引用它们。如果您确定此后的List的生命周期,您可以决定在单独的循环中从中删除/释放对象。
可以从静态角度进行优化(即,不从任何其他线程对此List进行修改),最好避免重复调用size()。也就是说,如果你不期望改变大小,那么为什么要一次又一次地计算它呢?毕竟它不是array.length,它是list.size()。