【问题标题】:Repetitive allocation of same-size byte arrays, replace with pools?重复分配相同大小的字节数组,替换为池?
【发布时间】:2011-03-14 18:23:32
【问题描述】:

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

实时分配的堆栈类百分比 rank self accum bytes objs bytes objs 跟踪名称 3 3.98% 19.85% 24259392 808 3849949016 1129587 359697 字节[] 4 3.98% 23.83% 24259392 808 3849949016 1129587 359698 字节[]

您会注意到分配了许多对象,但很少有对象仍然存在。这是出于一个简单的原因 - 为生成的“客户端”的每个实例分配两个字节数组。客户端不可重用——每个客户端只能处理一个请求,然后被丢弃。字节数组始终具有相同的大小 (30000)。

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

感谢您的帮助!

【问题讨论】:

  • 您不能使用合理大小的数组或重用部分客户端工作人员吗?
  • 是什么阻止您在完成“客户端”并重新使用它们(在池中)?
  • 客户端对象有许多不同的状态变量,这些变量在其生命周期中会发生变化。我们可以简单地重新初始化客户端并根据需要再次使用它,但鉴于客户端 90% 的内存使用是这两个字节数组,因此只关注它们并通过彻底改变客户端的行为来减少我们可能造成的混乱是有意义的.数组的大小是这样的,因为它们是用于高速网络连接的缓冲区。

标签: java bytearray memory-management


【解决方案1】:

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

话虽如此,fdreger 的 cmets 也是完全有效的。多线程对象池有点可怕,很可能会让人头疼。您提到他们仅处理单个请求,如果此请求仅由单个线程提供服务,那么在请求结束时擦除的 ThreadLocal 字节 [] 可能是一个不错的选择。如果请求相对于典型的年轻 gc 周期来说是短暂的,那么年轻-> 旧参考问题可能不是一个大问题(因为在 gc 期间处理任何给定请求的可能性很小,即使你保证得到定期)。

【讨论】:

  • 有两个线程使用缓冲区 - 一个用于写入(从网络读取之后),一个用于读取(来自更高级别的应用程序)。关于分配 30000 字节,您提出了一个有效的观点,您还有其他可能有帮助的想法吗?
  • 处理请求需要多长时间?它如何将字节 [] 从一个线程传递到另一个线程?如果您因频繁分配大型数组而遇到性能影响,那么一个简单的解决方法是通过一些 j.u.c 队列将 byte[] 传递回请求处理程序。每个报价都需要一个对象,因为它将被包装在一些内部节点实例中,但相对于频繁分配 30k 字节 [] 的成本而言,其成本确实很小。因此,首先使用一些字节 [] 为队列播种,然后在每个新请求上从队列中轮询(必要时创建)。
  • byte[] 直接从两个线程访问(它们在客户端的对象上同步)并且有两个指针 - 一个用于当前写入位置,一个用于读取位置(基本上,读取位置“追逐”写入位置)。我是否理解您基本上是在建议使用队列(例如 ConcurrentlyLinkedQueue)实现的池?
  • 是的,您可以使用一些 byte[] 数组并将指针保留到该结构中,但我认为这并不多。只需从队列中弹出一个即可使用它,并在完成后将其推回。理想情况下,您会重用每个 eden 集合中的所有 byte[] 实例,这样任何节点实例都不会超过一个集合,以防止任何(节点)泄漏到tenured。
  • 或者做一些更简单的事情,比如复制一个 j.u.c 类,并用你自己的 byte[] 包装类替换内部节点,作为一个节点。 javolution 集合类明确支持这种行为,遗憾的是 j.u.c 类也没有明确允许它。
【解决方案2】:

池化可能对您没有太大帮助 - 可能会使事情变得更糟,尽管它取决于许多因素(您使用的是什么 GC、对象的生存时间、可用内存量等。 ):

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

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

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

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

总结一下,除非您非常了解 GC 的内部结构并了解它在后台是如何工作的,并且从分析器获得的结果表明管理所有数组是一个瓶颈 - 不要汇集。在大多数情况下,这是一个坏主意。

【讨论】:

  • 感谢您的全面回复。我们将尝试尽快完成分析并返回答案。你能想到其他解决方案来减少分配这些字节数组的影响吗?
  • 我不知道你所说的“影响”是什么意思。申请有什么问题?有什么不好的症状需要治好吗?整体速度有问题吗?频繁的主要收藏?频繁的次要收藏?根据您的问题,答案会有所不同。第一个提示:如果您希望分配使用更少的内存,只需给它更少的内存。您可能会获得较小的吞吐量,但您会摆脱长时间的 GC 暂停。
  • 长时间的 GC 暂停是我们目前最大的问题,因为整个系统没有响应。此外,GC 会暂停对时钟的破坏性破坏(例如与超时相关的功能)。问题是——我们是否有一种方法可以在不降低与这些字节数组相关的吞吐量的情况下减少 GC 暂停?
  • 可能不是,这是一个折衷方案(在 C++ 中你会面临同样的问题——要么经常清理并使用更少的内存,要么清理大块并大量使用)。在你尝试更激进的东西之前,试着给你的程序尽可能少的内存。这类似于“所有活动客户端在内存使用高峰时同时需要的大量内存 + 20%”。这将强制进行非常频繁的次要收集,但它们应该非常快。这可能已经足够了。
【解决方案3】:

如果在你的情况下垃圾收集真的是性能损失(如果没有多少对象存活,通常清理伊甸园空间不会花费太多时间),并且很容易插入对象池,尝试一下,测量一下.

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

【讨论】:

  • +1 表示“尝试并测量”。优化很棘手,一盎司的实验数据值一磅的互联网意见。
  • 这是一个相当复杂的应用程序。您是否认为设置以下内容将服务于“尝试并衡量它”的目标:创建一个仅使用客户端的应用程序,使用当前解决方案和建议的解决方案使用 YourKit 之类的东西(计算 GC 等)运行它?
  • 您不需要分析器来计算 stw 暂停,只需使用 -XX:+PrintGCDetails -XX:+PrintGCDateStamps -XX:+PrintGCApplicationStoppedTime -XX:+PrintGCApplicationConcurrentTime -Xloggc:gc.log 然后解析日志文件以查看它暂停的频率。
【解决方案4】:

只要你总是有一个对它的引用,这个池就会工作得更好,这样垃圾收集器会简单地忽略这个池并且只会被声明一次(你总是可以将它声明为静态的,以确保安全) .虽然它会是持久内存,但我怀疑这会对您的应用程序造成问题。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-12-17
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2013-10-28
    • 2014-04-11
    • 2020-06-26
    相关资源
    最近更新 更多