重复分配相同大小的字节数组,用池替换?

问题描述 投票:0回答:4

作为内存分析的一部分,我们发现了以下内容:

          percent          live          alloc'ed  stack class
 rank   self  accum     bytes objs     bytes  objs trace name
    3  3.98% 19.85%  24259392  808 3849949016 1129587 359697 byte[]
    4  3.98% 23.83%  24259392  808 3849949016 1129587 359698 byte[]

您会注意到许多对象已分配,但很少有对象保持活动状态。原因很简单 - 为生成的“客户端”的每个实例分配两个字节数组。客户端不可重复使用——每个客户端只能处理一个请求,然后就被丢弃。字节数组始终具有相同的大小 (30000)。

我们正在考虑迁移到字节数组池(Apache 的 GenericObjectPool),因为通常在任何给定时刻都有已知数量的活动客户端(因此池大小不应波动太大)。这样,我们可以节省内存分配和垃圾收集。问题是,池会导致严重的 CPU 命中吗?这个想法到底是个好主意吗?

java arrays memory-management
4个回答
2
投票

我认为有很好的与GC相关的理由来避免这种分配行为。根据分配时堆的大小和 eden 中的可用空间,简单地分配 30000 个元素 byte[] 可能会严重影响性能,因为它很容易大于 TLAB(因此分配不是一个障碍)指针事件)并且 eden 中甚至可能没有足够的空间可用,因此直接分配到 tenured 中,这本身可能会由于增加的完整 gc 活动而导致另一次命中(特别是如果由于碎片而使用 cms)。

话虽如此,fdreger 的评论也是完全正确的。多线程对象池是一个有点严峻的事情,可能会引起头痛。您提到它们仅处理单个请求,如果该请求仅由单个线程提供服务,则在请求末尾擦除的 ThreadLocal byte[] 可能是一个不错的选择。如果请求相对于典型的年轻GC周期来说是短暂的,那么年轻->老参考问题可能不是一个大问题(因为在GC期间处理任何给定请求的概率很小,即使你保证得到定期这样做)。


2
投票

池化可能对你没有多大帮助(如果有的话)——可能会让事情变得更糟,尽管它取决于许多因素(你使用什么 GC、对象的生存时间、可用内存等):

GC的时间主要取决于存活对象的数量。收集器(我假设您运行的是普通 Java JRE)不会访问死对象,也不会一一释放它们。它在复制活动对象后释放整个内存区域(这使内存保持整洁和紧凑)。 100 个死对象的收集速度可达 100000 个。另一方面,所有活动对象都必须被复制 - 因此,如果您有一个包含 100 个对象的池,并且在给定时间仅使用 50 个对象,则保留未使用的对象是会让你付出代价。

如果您的阵列目前的寿命往往短于获得终身使用(复制到老一代空间)所需的时间,那么还有另一个问题:您的池化阵列肯定会寿命足够长。这将产生一种情况,即从老年代到年轻代存在大量引用 - 并且 GC 会在考虑相反情况的情况下进行优化。

实际上,池化数组很可能会让你的 GC 比创建新数组慢;廉价物品通常会出现这种情况。

池化的另一个成本来自于跨线程同步对象并在使用后清理它们。两者都比听起来更棘手。

总结一下,除非您非常了解 GC 的内部结构并了解它在幕后的工作原理,并且分析器的结果表明管理所有阵列是一个瓶颈 - 不要池化。在大多数情况下,这是一个坏主意。


1
投票

如果您的情况下垃圾收集确实对性能造成影响(如果存活的对象不多,通常清理伊甸园空间并不需要太多时间),并且很容易插入对象池,请尝试并测量它。

这当然取决于您的应用程序的需求。


0
投票

只要您始终拥有对池的引用,池就会工作得更好,这样垃圾收集器就会简单地忽略池,并且只会声明一次(为了安全起见,您始终可以将其声明为静态)。虽然这将是持久内存,但我怀疑这会给您的应用程序带来问题。

© www.soinside.com 2019 - 2024. All rights reserved.