Скільки місця в купі займають 100 мільйонів рядків в Java?

При роботі з природним мовою і лінгвістичному аналізі текстів нам часто доводиться оперувати величезною кількістю унікальних коротких рядків. Рахунок йде на десятки і сотні мільйонів — саме стільки в мові існує, приміром, осмислених комбінацій з двох слів. Основною платформою для нас є Java і ми не з чуток знаємо про її ненажерливості при роботі з такою великою кількістю дрібних об'єктів.

Щоб оцінити масштаб лиха, ми вирішили провести простий експеримент — створити 100 мільйонів порожніх рядків у Яві і подивитися, скільки доведеться заплатити за них оперативної пам'яті.

Увага: В кінці статті наведено опитування. Буде цікаво, якщо ви спробуєте відповісти на нього до прочитання статті, для самоконтролю.

Правилом хорошого тону при проведенні будь-яких замірів вважається опублікувати версію віртуальної машини і параметри запуску тесту:

> java -версія

java version "1.8.0_101"
Java(TM) SE Runtime Environment (build 1.8.0_101-b13)
Java HotSpot(TM) 64-Bit Server VM (build 25.101-b13, mixed mode)

Стиснення покажчиків включено (читай: розмір купи менше 32 Гб):

java -Xmx12g -Xms12g -XX:+UseConcMarkSweepGC -XX:NewSize=4g -XX:+UseCompressedOops ... ru.habrahabr.experiment.HundredMillionEmptyStringsExperiment

Стиснення покажчиків вимкнено (читай: розмір купи більше 32 Гб):

java -Xmx12g -Xms12g -XX:+UseConcMarkSweepGC -XX:NewSize=4g -XX:-UseCompressedOops ... ru.habrahabr.experiment.HundredMillionEmptyStringsExperiment

Вихідний код самого тіста:

package ru.habrahabr.experiment;

import org.apache.commons.lang3.time.StopWatch;

import java.util.ArrayList;
import java.util.List;

public class HundredMillionEmptyStringsExperiment {
public static void main(String[] args) throws InterruptedException {
List<String> lines = new ArrayList<>();

StopWatch sw = new StopWatch();
sw.start();

for (int i = 0; i < 100_000_000L; i++) {
lines.add(new String(new char[0]));
}

sw.stop();
System.out.println("Created 100M empty strings: " + sw.getTime() + " millis");

// щоб не зберігати зайвого і було простіше аналізувати знімок купи
System.gc();

// захист від оптимізацій
while (true) {
System.out.println("Line count: " + lines.size());

Thread.sleep(10000);
}
}
}

Процес

Шукаємо ідентифікатор процесу з допомогою утиліти jps і робимо знімок купи (heap dump) з допомогою jmap:

> jps

12777 HundredMillionEmptyStringsExperiment

> jmap -dump:format=b,file=HundredMillionEmptyStringsExperiment.bin 12777

Dumping heap to E:\jdump\HundredMillionEmptyStringsExperiment.bin ...
Heap dump file created

Аналізуємо знімок купи, використовуючи Eclipse Memory Analyzer (MAT):

image

image

Для другого тесту з вимкненим стисненням покажчиків знімки не наводимо, але ми чесно провели експеримент і просимо повірити на слово (оптимально: відтворити тест і переконатися самим).

Висновки

  • 2.4 Гб займає обв'язка об'єктів класу String + покажчики на масиви символів.
  • 1.6 Гб займає обв'язка масивів символів.
  • 400 Мб займають покажчики на рядки.
Якщо ви працюєте з розміром купи більше 32Гб (стиснення покажчиків вимкнено), то вказівники будуть коштувати ще дорожче. Відповідно будуть такі результати:

  • 3.2 Гб займає обв'язка об'єктів класу String + покажчики на масиви символів.
  • 2.4 Гб займає обв'язка масивів символів.
  • 800 Мб займають покажчики на рядки.
Разом, за кожну рядок ви додатково до розміру масиву символів платите 44 байт (64 байта без стиснення покажчиків). Якщо середня довжина рядків становить 15 символів, то виходить майже 5 байт на символ. Дуже дорого, якщо мова йде про домашньому залозі.

Як боротися

На жаль в Яві не існує вбудованих механізмів, щоб безпосередньо скоротити споживання пам'яті при роботі з рядками. Існують дві основні стратегії для економії ресурсів:

  1. Для великої кількості повторюваних рядків можна використовувати інтернування (string interning). Суть механізму така: оскільки рядки в Яві незмінні, то можна зберігати їх в окремому пулі і при повторі посилатися на існуючий об'єкт замість створення нового рядка. Такий підхід не безкоштовний, він стоїть і пам'яті та процесорного часу для зберігання структури пулу і пошуку в ньому.
  2. Якщо, як у нашому випадку, унікальні рядки — не залишається нічого іншого, як використовувати різні алгоритмічні трюки. Міні-анонс: як ми працюємо з сотнею мільйонів биграмм (читай: слово + слово або 15 символів) в наших завданнях розповімо найближчим часом.
висновок

Рішення будь-якої проблеми починається з оцінки її масштабів. Тепер ви ці масштаби знаєте і можете враховувати накладні витрати при роботі з великою кількістю рядків у своїх проектах.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.