Java медленнее с большой кучей, без чрезмерного gc-ing

Оригинальный вопрос

Я запускаю программу Java, скажем program.jar, с "маленькой" начальной кучей (1gb) и "большой" начальной кучей (16gb). Когда я запускаю его с небольшой начальной кучей, то есть

java -jar -Xms1g -Xmx16g program.jar

программа заканчивается через 12 секунд (усредняется по нескольким прогонам). Напротив, когда я запускаю его с большой начальной кучей, то есть

java -jar -Xms16g -Xmx16g program.jar

программа заканчивается через 30 секунд (усредняется по нескольким прогонам).

Я понимаю из других вопросов в SO, что, как правило, большие кучи могут привести к чрезмерному сбору мусора, тем самым замедляя работу программы:

Однако, когда я запускаю program.jar с флагом -verbose:gc, никакой gc-активности не сообщается вообще с большой начальной кучей. При небольшой начальной куче, только во время фазы инициализации программы есть только некоторая gc-активность, прежде чем я начну измерять время. Излишняя сборка мусора, по-видимому, не разъясняет мои наблюдения.

Чтобы сделать его более запутанным (для меня как минимум), у меня есть функционально эквивалентная программа, скажем program2.jar, которая имеет такое же поведение ввода-вывода, что и program.jar. Основное различие заключается в том, что program2.jar использует менее эффективные структуры данных, чем program.jar, по крайней мере в терминах памяти (независимо от того, насколько program2.jar также менее эффективен с точки зрения времени, на самом деле это то, что я пытаюсь определить). Но независимо от того, запускаю ли program2.jar небольшую начальную кучу или большую начальную кучу, она всегда заканчивается примерно через 22 секунды (в том числе около 2-3 секунд gc-ing).

Итак, это мой вопрос: (как) могут большие кучи замедлять программы, чрезмерное игнорирование?

(Этот вопрос может показаться похожим на вопрос Георга в Java медленнее с большой кучей, но его проблема оказалась не связанной с кучей. В моем случае, Я чувствую, что это должно иметь какое-то отношение к куче, поскольку это единственное различие между двумя прогонами program.jar.)

Вот некоторые подробности, которые могут иметь значение. Я использую Java 7, OpenJDK:

> java -version
java version "1.7.0_75"
OpenJDK Runtime Environment (rhel-2.5.4.0.el6_6-x86_64 u75-b13)
OpenJDK 64-Bit Server VM (build 24.75-b04, mixed mode)

Моя машина имеет два процессора E5-2690V3 (http://ark.intel.com/products/81713) в двух сокетах (Hyper-Threading и Turbo Boost отключены) и имеет достаточная память (64 ГБ), около половины которой бесплатно перед запуском программы:

> free
             total       used       free     shared    buffers     cached
Mem:      65588960   31751316   33837644         20     154616   23995164
-/+ buffers/cache:    7601536   57987424
Swap:      1023996      11484    1012512

Наконец, программа имеет несколько потоков (около 70).

Изменить 1

В ответ на Bruno Reis и kdgregory

Я собрал дополнительную статистику. Это для program.jar с небольшой начальной кучей:

Command being timed: "java -Xms1g -Xmx16g -verbose:gc -jar program.jar"
User time (seconds): 339.11
System time (seconds): 29.86
Percent of CPU this job got: 701%
Elapsed (wall clock) time (h:mm:ss or m:ss): 0:52.61
Average shared text size (kbytes): 0
Average unshared data size (kbytes): 0
Average stack size (kbytes): 0
Average total size (kbytes): 0
Maximum resident set size (kbytes): 12192224
Average resident set size (kbytes): 0
Major (requiring I/O) page faults: 1
Minor (reclaiming a frame) page faults: 771372
Voluntary context switches: 7446
Involuntary context switches: 27788
Swaps: 0
File system inputs: 10216
File system outputs: 120
Socket messages sent: 0
Socket messages received: 0
Signals delivered: 0
Page size (bytes): 4096
Exit status: 0

Это для program.jar с большой начальной кучей:

Command being timed: "java -Xms16g -Xmx16g -verbose:gc -jar program.jar"
User time (seconds): 769.13
System time (seconds): 28.04
Percent of CPU this job got: 1101%
Elapsed (wall clock) time (h:mm:ss or m:ss): 1:12.34
Average shared text size (kbytes): 0
Average unshared data size (kbytes): 0
Average stack size (kbytes): 0
Average total size (kbytes): 0
Maximum resident set size (kbytes): 10974528
Average resident set size (kbytes): 0
Major (requiring I/O) page faults: 16
Minor (reclaiming a frame) page faults: 687727
Voluntary context switches: 6769
Involuntary context switches: 68465
Swaps: 0
File system inputs: 2032
File system outputs: 160
Socket messages sent: 0
Socket messages received: 0
Signals delivered: 0
Page size (bytes): 4096
Exit status: 0

(Описанные здесь времена настенных часов отличаются от тех, которые указаны в моем первоначальном сообщении из-за ранее не прошедшей этап инициализации.)

Изменить 2

В ответ на начальный комментарий и comment

Я собрал некоторую статистику по кэшам. Это для program.jar с небольшой начальной кучей (усредненной на 30 прогонов):

  2719852136 cache-references                                              ( +-  1.56% ) [42.11%]
  1931377514 cache-misses              #   71.010 % of all cache refs      ( +-  0.07% ) [42.11%]
 56748034419 L1-dcache-loads                                               ( +-  1.34% ) [42.12%]
 16334611643 L1-dcache-load-misses     #   28.78% of all L1-dcache hits    ( +-  1.70% ) [42.12%]
 24886806040 L1-dcache-stores                                              ( +-  1.47% ) [42.12%]
  2438414068 L1-dcache-store-misses                                        ( +-  0.19% ) [42.13%]
           0 L1-dcache-prefetch-misses                                    [42.13%]
    23243029 L1-icache-load-misses                                         ( +-  0.66% ) [42.14%]
  2424355365 LLC-loads                                                     ( +-  1.73% ) [42.15%]
   278916135 LLC-stores                                                    ( +-  0.30% ) [42.16%]
   515064030 LLC-prefetches                                                ( +-  0.33% ) [10.54%]
 63395541507 dTLB-loads                                                    ( +-  0.17% ) [15.82%]
  7402222750 dTLB-load-misses          #   11.68% of all dTLB cache hits   ( +-  1.87% ) [21.08%]
 20945323550 dTLB-stores                                                   ( +-  0.69% ) [26.34%]
   294311496 dTLB-store-misses                                             ( +-  0.16% ) [31.60%]
    17012236 iTLB-loads                                                    ( +-  2.10% ) [36.86%]
      473238 iTLB-load-misses          #    2.78% of all iTLB cache hits   ( +-  2.88% ) [42.12%]
 29390940710 branch-loads                                                  ( +-  0.18% ) [42.11%]
    19502228 branch-load-misses                                            ( +-  0.57% ) [42.11%]

53.771209341 seconds time elapsed                                          ( +-  0.42% )

Это для program.jar с большой начальной кучей (усредненный более 30 прогонов):

 10465831994 cache-references                                              ( +-  3.00% ) [42.10%]
  1921281060 cache-misses              #   18.358 % of all cache refs      ( +-  0.03% ) [42.10%]
 51072650956 L1-dcache-loads                                               ( +-  2.14% ) [42.10%]
 24282459597 L1-dcache-load-misses     #   47.54% of all L1-dcache hits    ( +-  0.16% ) [42.10%]
 21447495598 L1-dcache-stores                                              ( +-  2.46% ) [42.10%]
  2441970496 L1-dcache-store-misses                                        ( +-  0.13% ) [42.10%]
           0 L1-dcache-prefetch-misses                                    [42.11%]
    24953833 L1-icache-load-misses                                         ( +-  0.78% ) [42.12%]
 10234572163 LLC-loads                                                     ( +-  3.09% ) [42.13%]
   240843257 LLC-stores                                                    ( +-  0.17% ) [42.14%]
   462484975 LLC-prefetches                                                ( +-  0.22% ) [10.53%]
 62564723493 dTLB-loads                                                    ( +-  0.10% ) [15.80%]
 12686305321 dTLB-load-misses          #   20.28% of all dTLB cache hits   ( +-  0.01% ) [21.06%]
 19201170089 dTLB-stores                                                   ( +-  1.11% ) [26.33%]
   279236455 dTLB-store-misses                                             ( +-  0.10% ) [31.59%]
    16259758 iTLB-loads                                                    ( +-  4.65% ) [36.85%]
      466127 iTLB-load-misses          #    2.87% of all iTLB cache hits   ( +-  6.68% ) [42.11%]
 28098428012 branch-loads                                                  ( +-  0.13% ) [42.10%]
    18707911 branch-load-misses                                            ( +-  0.82% ) [42.10%]

73.576058909 seconds time elapsed                                          ( +-  0.54% )

Сравнивая абсолютные числа, большая начальная куча приводит к примерно на 50% больше L1-dcache-load-misses и 70% больше dTLB-промахов. Я сделал подсчет конвертов для пропусков dTLB-нагрузки, предполагая 100 циклов/пропусков (источник: Wikipedia) на моей 2,6-гигабайтной машине, что дает 484-секундную задержку для большой начальной кучи по сравнению с 284-секундной задержкой с маленькой. Я не знаю, как перевести это число обратно на затухающую задержку (возможно, не просто делить на количество ядер?), Но порядок величины кажется правдоподобным.

После сбора этих статистических данных я также различал вывод -XX:+PrintFlagsFinal для большой и малой начальной кучи (на основе одного прогона для каждого из этих двух случаев):

<     uintx InitialHeapSize                          := 17179869184     {product}           
---
>     uintx InitialHeapSize                          := 1073741824      {product}  

Таким образом, никакие другие флаги не отображаются в -Xms. Здесь также вывод -XX:+PrintGCDetails для program.jar с небольшой начальной кучей:

[GC [PSYoungGen: 239882K->33488K(306176K)] 764170K->983760K(1271808K), 0.0840630 secs] [Times: user=0.70 sys=0.66, real=0.09 secs]
[Full GC [PSYoungGen: 33488K->0K(306176K)] [ParOldGen: 950272K->753959K(1508352K)] 983760K->753959K(1814528K) [PSPermGen: 2994K->2993K(21504K)], 0.0560900 secs] [Times: user=0.20 sys=0.03, real=0.05 secs]
[GC [PSYoungGen: 234744K->33056K(306176K)] 988704K->983623K(1814528K), 0.0416120 secs] [Times: user=0.69 sys=0.03, real=0.04 secs]
[GC [PSYoungGen: 264198K->33056K(306176K)] 1214765K->1212999K(1814528K), 0.0489600 secs] [Times: user=0.61 sys=0.23, real=0.05 secs]
[Full GC [PSYoungGen: 33056K->0K(306176K)] [ParOldGen: 1179943K->1212700K(2118656K)] 1212999K->1212700K(2424832K) [PSPermGen: 2993K->2993K(21504K)], 0.1589640 secs] [Times: user=2.27 sys=0.10, real=0.16 secs]
[GC [PSYoungGen: 230538K->33056K(431616K)] 1443238K->1442364K(2550272K), 0.0523620 secs] [Times: user=0.69 sys=0.23, real=0.05 secs]
[GC [PSYoungGen: 427431K->33152K(557568K)] 1836740K->1835676K(2676224K), 0.0774750 secs] [Times: user=0.64 sys=0.72, real=0.08 secs]
[Full GC [PSYoungGen: 33152K->0K(557568K)] [ParOldGen: 1802524K->1835328K(2897920K)] 1835676K->1835328K(3455488K) [PSPermGen: 2993K->2993K(21504K)], 0.2019870 secs] [Times: user=2.74 sys=0.13, real=0.20 secs]
[GC [PSYoungGen: 492503K->33280K(647168K)] 2327831K->2327360K(3545088K), 0.0870810 secs] [Times: user=0.61 sys=0.92, real=0.09 secs]
[Full GC [PSYoungGen: 33280K->0K(647168K)] [ParOldGen: 2294080K->2326876K(3603968K)] 2327360K->2326876K(4251136K) [PSPermGen: 2993K->2993K(21504K)], 0.0512730 secs] [Times: user=0.09 sys=0.12, real=0.05 secs]

Heap
 PSYoungGen  total 647168K, used 340719K [0x00000006aaa80000, 0x00000006dd000000, 0x0000000800000000)
  eden space 613376K, 55% used [0x00000006aaa80000,0x00000006bf73bc10,0x00000006d0180000)
  from space 33792K, 0% used [0x00000006d2280000,0x00000006d2280000,0x00000006d4380000)
  to   space 33792K, 0% used [0x00000006d0180000,0x00000006d0180000,0x00000006d2280000)
 ParOldGen   total 3603968K, used 2326876K [0x0000000400000000, 0x00000004dbf80000, 0x00000006aaa80000)
  object space 3603968K, 64% used [0x0000000400000000,0x000000048e0572d8,0x00000004dbf80000)
 PSPermGen   total 21504K, used 3488K [0x00000003f5a00000, 0x00000003f6f00000, 0x0000000400000000)
  object space 21504K, 16% used [0x00000003f5a00000,0x00000003f5d68070,0x00000003f6f00000)

И для program.jar с большой начальной кучей:

Heap
 PSYoungGen  total 4893696K, used 2840362K [0x00000006aaa80000, 0x0000000800000000, 0x0000000800000000)
  eden space 4194816K, 67% used [0x00000006aaa80000,0x000000075804a920,0x00000007aab00000)
  from space 698880K, 0% used [0x00000007d5580000,0x00000007d5580000,0x0000000800000000)
  to   space 698880K, 0% used [0x00000007aab00000,0x00000007aab00000,0x00000007d5580000)
 ParOldGen   total 11185152K, used 0K [0x00000003fff80000, 0x00000006aaa80000, 0x00000006aaa80000)
  object space 11185152K, 0% used [0x00000003fff80000,0x00000003fff80000,0x00000006aaa80000)
 PSPermGen   total 21504K, used 3489K [0x00000003f5980000, 0x00000003f6e80000, 0x00000003fff80000)
  object space 21504K, 16% used [0x00000003f5980000,0x00000003f5ce8400,0x00000003f6e80000)

Ответы

Ответ 1

Доступ к памяти требует процессорного времени. Доступ к большему количеству памяти не только приводит к линейному увеличению времени процессора, но и, вероятно, увеличивает давление в кэше и, следовательно, снижает скорость, что требует сверхлинейного увеличения времени CPU.

Запустите программу с помощью perf stat java -jar ..., чтобы увидеть количество промахов в кэше. см. Учебник по Perf


Example Time Scale of System Latencies
Источник изображения: "Производительность систем: корпоративная и облачная мягкая обложка", Брендан Грегг, ISBN: 978-0133390094


Так как размер начальной кучи также влияет на размер пространства eden, а меньшее пространство с эденом запускает GC, это может привести к более компактной куче, которая может быть более удобной для кэширования (без временных объектов запуска, засоряющих кучу).

Чтобы уменьшить количество различий между обеими тиражами, попробуйте установить начальный и максимальный размер молодого поколения на одно и то же значение для обоих прогонов, чтобы отличаться только размер старого поколения. Это должно, вероятно, привести к той же производительности.

Как и в стороне: вы также можете попробовать запустить JVM с огромными страницами, это может (вам нужно измерить!) получить вам несколько дополнительных% производительности за счет сокращения пропусков TLB.

Примечание для будущих читателей: Ограничение размера нового генератора не обязательно ускоряет работу вашего JVM, он просто запускает GC, который ускоряет тем самым конкретную рабочую нагрузку @Peng.
Вручную запуск GC после запуска будет иметь тот же эффект.