Java中内存分配的典型速度是多少?

6
我正在分析一个Java应用程序,并发现对象分配的速度比我预期的慢得多。我运行了一个简单的基准测试,尝试确定小对象分配的总体速度,结果我发现在我的机器上分配一个小对象(三个浮点数的向量)需要约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纳秒/对象的性能,因此重用对象可以极大地提高性能。我真的不想重用对象,因为这会使符号有点混乱(许多方法需要接受一个“receptacle”参数而不是返回值)。

所以问题是:对象分配需要这么长时间是正常的吗?还是我的机器出了一些问题,一旦修复就可以在这方面获得更好的性能?对于其他人来说,小对象分配通常需要多长时间,有典型值吗?我正在使用客户端机器,目前没有使用任何编译标志。如果你的机器速度更快,你的JVM版本和操作系统是什么?

我知道个人的性能可能差异很大,但我只是想问一下我上面提到的数字是否在合理范围内。


内存带宽可能会占主导地位。池化可能会使垃圾回收机制效率低下,一旦您的对象分散开来,可能会出现局部性问题。为了获得最佳性能,您可能需要一个大数组(或内存映射缓冲区)和整数偏移量。这也适用于C和C++。 - Tom Hawtin - tackline
3个回答

5

当对象很小且没有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.

注意:没有发生垃圾回收。但是如果增加大小,程序需要等待在垃圾回收中复制内存。
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.

我猜测你的分配速度相对较慢是因为正在执行垃圾回收。解决这个问题的一种方法是增加应用程序可用的内存。(尽管这可能只会延迟成本)
如果我再次运行它,使用-verbosegc -XX:NewSize=1g
Average object allocation took 9.1 ns.

实际上,我后来发现年轻代空间不足是导致问题的原因。将对象提升到永久代似乎是很昂贵的 - 可能是因为在永久代上进行垃圾回收是很昂贵的。我的基准测试没有考虑到在某些用例中,被创建的对象将在年轻代中死亡。这是Java基准测试可以完全具有欺骗性的一个很好的例子。 - Gravity

2
我不知道你如何衡量分配时间。它可能至少相当于内联的程度。
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 compilation(即时编译)class loading(类加载);很可能,前几个单词被初始化(例如,类指针和哈希码要求生成一些随机数),并调用对象构造函数。它们可能需要同步等操作。
更重要的是,新分配的对象也许不在最近的一级缓存中,因此可能发生一些缓存未命中。
因此,尽管我不是Java专家,但我对您的措施并不感到惊讶。我相信分配新对象可以使您的代码更清晰、更易于维护,而不是试图重用旧对象。

同意,只有像Thread和direct ByteBuffer这样昂贵的对象才进行池化是一个好的实践。 - ClickerMonkey

1

是的。你所认为它应该做的事情和实际上它所做的事情之间的差距可能相当大。池化可能会有些混乱,但当分配和垃圾回收占执行时间的很大一部分时(这确实可能发生),从性能上讲,池化是一个很大的优势。

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

以下是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);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接