【问题标题】:What is the typical speed of a memory allocation in Java?Java中内存分配的典型速度是多少?
【发布时间】:2012-01-28 17:57:00
【问题描述】:

我正在分析一个 Java 应用程序,发现对象分配的发生速度比我预期的要慢得多。我运行了一个简单的基准测试来尝试确定小对象分配的整体速度,我发现在我的机器上分配一个小对象(3 个浮点数的向量)似乎需要大约 200 纳秒。我在(双核)2.0 GHz 处理器上运行,所以这大约是 400 个 CPU 周期。我想问这里那些曾经分析过 Java 应用程序的人,这种速度是否可以预期。这对我来说似乎有点残忍和不寻常。毕竟,我认为像 Java 这样可以压缩堆并重新定位对象的语言的对象分配如下所示:

int obj_addr = heap_ptr;
heap_ptr += some_constant_size_of_object
return obj_addr;

....这是几行组装。至于垃圾收集,我没有分配或丢弃足够的对象来发挥作用。当我通过重用对象来优化我的代码时,我获得了大约 15 纳秒/我需要处理的对象的性能,而不是每个我需要处理的对象 200 ns,因此重用对象极大地提高了性能。我真的不想重用对象,因为这会使符号有点毛茸茸(许多方法需要接受 receptacle 参数而不是返回值)。

所以问题是:对象分配需要这么长时间是否正常?或者我的机器上可能有问题,一旦修复,可能会让我在这方面有更好的表现?小对象分配通常需要多长时间,是否有典型值?我正在使用客户端机器,目前没有使用任何编译标志。如果你的机器速度更快,你机器的 JVM 版本和操作系统是什么?

我意识到,在性能方面,个人里程可能会有很大差异,但我只是想问一下我上面提到的数字是否看起来正确。

【问题讨论】:

  • 内存带宽可能会占主导地位。 / 池化会使 GC 效率低下,一旦你的对象被分散,你可能会遇到局部性问题。可能您想要获得最佳性能的是一个具有整数偏移量的大数组(或内存映射缓冲区)。 (这也适用于 C 和 C++。)

标签: java performance memory-management


【解决方案1】:

我不知道您如何衡量分配时间。它可能是内联的,至少相当于

intptr_t obj_addr = heap_ptr;
heap_ptr += CONSTANT_SIZE;
if (heap_ptr > young_region_limit) 
    call_the_garbage_collector ();
return obj_addr;

但它比这更复杂,因为你必须填写obj_addr;然后,可能会发生一些JIT compilationclass loading;很可能,前几个词被初始化(例如类指针和哈希码,这可能涉及一些随机数生成......),并调用对象构造函数。它们可能需要同步等。

更重要的是,新分配的对象可能不在最近的一级缓存中,因此可能会发生一些缓存未命中。

因此,虽然我不是 Java 专家,但我并不对您的措施感到惊讶。我确实相信分配新对象比尝试重用旧对象更能让您的代码更简洁、更易于维护。

【讨论】:

  • 同意,最好只将昂贵的对象池化,如 Thread 和直接 ByteBuffer。
【解决方案2】:

是的。你认为它应该做的事情和它实际做的事情之间的差异可能非常大。池化可能很混乱,但是当分配和垃圾收集占执行时间的很大一部分时(当然可以),池化在性能方面是一个巨大的胜利。

要池化的对象是您在分配过程中最常通过堆栈样本找到的对象。

这是 C++ 中此类示例的外观。在 Java 中,细节不同,但思路是一样的:

... blah blah system stuff ...
MSVCRTD! 102129f9()
MSVCRTD! 1021297f()
operator new() line 373 + 22 bytes
operator new() line 65 + 19 bytes
COpReq::Handler() line 139 + 17 bytes <----- here is the line that's doing it
doit() line 346 + 12 bytes
main() line 367
mainCRTStartup() line 338 + 17 bytes
KERNEL32! 7c817077()
                              V------ and that line shows what's being allocated
        COperation* pOp = new COperation(iNextOp++, jobid);

【讨论】:

    【解决方案3】:

    在对象很小且没有 GC 成本的情况下,创建对象非常快。

    final int batch = 1000 * 1000;
    
    Double[] doubles = new Double[batch];
    long start = System.nanoTime();
    
        for (int j = 0; j < batch; j++)
            doubles[j] = (double) j;
    
    long time = System.nanoTime() - start;
    System.out.printf("Average object allocation took %.1f ns.%n", (double) time/batch);
    

    -verbosegc打印

    Average object allocation took 13.0 ns.
    

    注意:没有发生 GC。但是增加大小,程序需要等待在 GC 中复制内存。

    final int batch = 10 *1000 * 1000;
    

    打印

    [GC 96704K->94774K(370496K), 0.0862160 secs]
    [GC 191478K->187990K(467200K), 0.4135520 secs]
    [Full GC 187990K->187974K(618048K), 0.2339020 secs]
    Average object allocation took 78.6 ns.
    

    我怀疑您的分配速度相对较慢,因为您正在执行 GC。解决此问题的一种方法是增加应用程序可用的内存。 (虽然这可能只是延迟成本)

    如果我用-verbosegc -XX:NewSize=1g 再次运行它

    Average object allocation took 9.1 ns.
    

    【讨论】:

    • 实际上,我发现年轻一代的空间不足是导致问题的原因。将对象提升到永久代似乎代价高昂——也许是因为 GC 代价高昂,是的。我的基准测试没有考虑到在我的某些用例中,正在创建的对象将在年轻一代中消亡。一个很好的例子,说明 Java 基准测试如何完全具有欺骗性。
    猜你喜欢
    • 2011-05-17
    • 2011-07-31
    • 2011-03-03
    • 2019-11-24
    • 1970-01-01
    • 1970-01-01
    • 2012-08-28
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多