llvm文档说:
然而,在实践中,使用积极的垃圾收集技术的地方和性能优势主导了任何低级别的损失。
那么,与手动管理内存相比,使用垃圾收集会导致性能提升的原因是什么呢? (除了代码编写时间的明显减少)仅仅执行堆压缩的好处是增加空间局部性和缓存利用率?或者是否有其他更有帮助的东西,比如一次删除所有内容?
答案 0 :(得分:2)
我只能代表Oracle(前Sun)和IBM JVM;它们的效率依赖于新创建的对象不太可能存活很长时间的事实。因此,将它们分隔到自己的区域允许该区域经常被压缩,因为很少有幸存者是廉价的操作。频繁的压缩意味着自由空间可以保持连续,因此对象创建也很便宜,因为没有自由链可以遍历而且没有内存碎片。
手动内存管理方案很少有效,因为这是一种相对复杂的处理方式,不太可能为每个应用程序重新创建。这些垃圾收集器已经发展并在更长的时间内进行了优化,并且比单个应用程序获得的更多努力。如果他们的表现不是很高,那就太令人惊讶和失望了。
答案 1 :(得分:2)
在现代处理器上,内存缓存是King。遭遇高速缓存未命中可能会使处理器停顿数百个cpu周期,等待慢速总线提供数据。
使缓存有效需要引用的位置。换句话说,如果下一次内存访问接近前一次,那么数据已经在缓存中的几率很高。
垃圾收集器可以帮助很好地完成这项工作。最大的胜利不是集合,它是重建对象图和重新组织数据结构的能力。压实。
想象一下典型的数据结构,一个指向对象的指针数组。这是慢慢建立的,比如从文件中读取一堆字符串并将它们转换为对象的字段值。分配的对象将在地址空间中进行分散拍摄。由工作对象分隔的数组指向的长寿命对象,如字符串。稍后迭代该数组会很慢。
直到垃圾收集器运行并重建数据结构。将所有指向的对象按顺序排列。
现在迭代集合非常快,因为访问元素N使得元素N + 1很可能随时可用。如果不在L1缓存中那么L2或L3的赔率非常高(如果你有的话)。
非常大的胜利,它是一个使垃圾收集与显式内存管理竞争的功能。显式类具有不支持移动对象的问题,因为它会使指针无效。
答案 2 :(得分:0)
我怀疑局部性有助于提高性能 - 不可否认,小型对象往往会在堆的同一区域同时创建(但这也适用于C),随着时间的推移,剩下的这些小对象将被压缩进入一个密切相关的堆区域,据说这可以让你比C风格的分配更有优势。但是,向我展示一个只使用这些小对象的程序,我将向您展示一个可以完成所有操作的程序。向我展示一个程序,它传递将在堆栈中使用的所有对象,我将向您展示一个速度尖叫的程序。
内存的取消分配是一种性能优势,短期因为它们不需要取消分配。但是,当垃圾收集器启动时,这种好处就会消失。通常情况下,收集发生在系统中没有其他任何事情发生时(理论上),因此成本实际上是无效的。
压缩堆也有助于分配,所有分配都可以来自堆的开头,而内存管理器不必走堆查找正确大小的下一个空闲块。但是,传统系统可以通过使用多个固定块堆来获得相同的速度(这意味着你总是从堆中分配所需的块大小,并且总是分配一个固定的块,所以走堆就是找到第一个空闲块,这可以使用位图删除
总而言之,除了基准测试之外,根本没有太大的好处。根据我的经验,GC可以并且会在错误的时间跳入并减慢你的速度,通常当系统内存被填满时,因为用户已经完成了加载需要大量内存分配的新页面...这又需要一个集合。
它也有使用大量内存的倾向 - “内存便宜”是GC语言的口头禅,因此编写程序就是考虑到这一点,这意味着内存分配更常见,特别是对于临时和中级对象。只需查看StringBuilder类就可以了解这一点。字符串可以使用它来“解决”,但许多其他对象仍然被放弃。任何使用大量内存的程序都会发现自己正在努力使用RAM IO - 所有内存都必须被带入要使用的CPU缓存中,使用的内存越多,你的CPU MM必须做的IO越多,那就可以了在错误的情况下杀死表现。
此外,当GC发生时,你也必须处理Finalized对象,这不像以前那么糟糕,但它仍然可以在终结者运行时暂停你的程序。
旧的Java GCs对于perf来说是可怕的,尽管许多研究已经使它们明显更好,但它们仍然不完美。编辑: 关于本地化的另一件事,想象创建一个数组并添加一些项目,然后进行大量分配,然后你想要向数组添加另一个项目 - 使用GC系统,添加的数组元素将不会被本地化,即使在压缩时,数组中的每个对象都将作为单个项存储在堆上。这就是为什么我认为本地化问题并不像它所做的那么大。现在,将它与分配有缓冲区的数组进行比较,并在缓冲区空间中分配对象。 可能需要重新分配和复制才能添加新项目,但阅读和修改它的速度非常快。
答案 3 :(得分:0)
尚未提及的一个因素是,特别是在多线程系统中,有时很难准确预测哪个对象最终会将最后一个存活的引用保存到某个其他对象。如果不必担心可能包含循环的对象图,则可以为此目的使用引用计数。在复制对象的引用之前,请增加其引用计数。在销毁对象的引用之前,减少其引用计数。它递减引用计数使其达到零,破坏对象以及引用。这种方法适用于只有一个CPU核心的计算机;如果在任何给定时间只有一个线程可以实际运行,那么如果两个线程试图同时调整同一个对象的引用计数,则不必担心会发生什么。遗憾的是,在具有多个CPU内核的系统中,任何想要调整引用计数的CPU都必须与所有其他CPU协调该操作,以确保两个CPU从不在同一时间点击计数器。这种协调在单个CPU中是“免费的”,但在多核系统中相对昂贵。
使用批处理模式垃圾收集器时,通常可以自由分配,复制和销毁对象引用,而无需CPU间协调。定期需要让所有CPU停止并运行垃圾收集周期,但要求所有CPU每隔几秒左右相互协调一次比要求它们在每一个上相互协调要便宜很多对象引用分配。