Компактни низове в Java 9

1. Общ преглед

Низовете в Java са вътрешно представени от знак [], съдържащ символите на низа . И всеки знак се състои от 2 байта, защото Java вътрешно използва UTF-16.

Например, ако String съдържа дума на английски език, водещите 8 бита ще бъдат 0 за всеки знак , тъй като ASCII знак може да бъде представен с помощта на един байт.

Много символи изискват 16 бита, за да ги представят, но статистически повечето изискват само 8 бита - представяне на символи LATIN-1. Така че има възможност за подобряване на консумацията и производителността на паметта.

Важното е също, че String обикновено заемат голяма част от JVM купчината пространство. И заради начина, по който се съхраняват от JVM, в повечето случаи, един низ Например може да отнеме двойно пространство всъщност се нуждае .

В тази статия ще обсъдим опцията за компресиран низ, въведена в JDK6 и новия компактен низ, наскоро въведен с JDK9. И двете са проектирани да оптимизират консумацията на памет на Strings на JMV.

2. Компресиран низ - Java 6

JDK 6 update 21 Performance Release представи нова опция за виртуална машина:

-XX:+UseCompressedStrings

Когато тази опция е активирана, низовете се съхраняват като байт [] , вместо като char [] - по този начин спестявайки много памет. Тази опция обаче в крайна сметка беше премахната в JDK 7, главно защото имаше някои нежелани последици за производителността.

3. Компактен низ - Java 9

Java 9 донесе концепцията за компактни Strings ba ck.

Това означава, че всеки път, когато създаваме String, ако всички символи на String могат да бъдат представени чрез представяне на байт - LATIN-1, байтов масив ще бъде използван вътрешно, така че един байт е даден за един символ.

В други случаи, ако някой символ изисква повече от 8 бита, за да го представи, всички символи се съхраняват с помощта на два байта за всеки - представяне на UTF-16.

Така че, когато е възможно, той просто ще използва един байт за всеки символ.

Сега въпросът е - как ще работят всички операции String ? Как ще прави разлика между представленията LATIN-1 и UTF-16?

Е, за да се справим с този проблем, се прави още една промяна във вътрешното изпълнение на String . Имаме окончателен полеви кодер , който съхранява тази информация.

3.1. Внедряване на низове в Java 9

Досега String се съхраняваше като char [] :

private final char[] value;

Отсега нататък това ще бъде байт []:

private final byte[] value;

Променливият кодер :

private final byte coder;

Къде може да бъде кодерът :

static final byte LATIN1 = 0; static final byte UTF16 = 1;

Повечето от операциите String сега проверяват кодера и изпращат до конкретното изпълнение:

public int indexOf(int ch, int fromIndex) { return isLatin1() ? StringLatin1.indexOf(value, ch, fromIndex) : StringUTF16.indexOf(value, ch, fromIndex); } private boolean isLatin1() { return COMPACT_STRINGS && coder == LATIN1; } 

С цялата информация, от която JVM се нуждае, е готова и налична, опцията CompactString VM е активирана по подразбиране. За да го деактивираме, можем да използваме:

+XX:-CompactStrings

3.2. Как работи кодерът

В изпълнението на Java 9 String клас дължината се изчислява като:

public int length() { return value.length >> coder; }

Ако String съдържа само LATIN-1, стойността на кодера ще бъде 0, така че дължината на String ще бъде същата като дължината на байтовия масив.

В други случаи, ако низът е в представяне на UTF-16, стойността на кодера ще бъде 1 и следователно дължината ще бъде половината от размера на действителния байтов масив.

Имайте предвид, че всички промени, направени за Compact String, са във вътрешното изпълнение на класа String и са напълно прозрачни за разработчиците, използващи String .

4. Компактни струни срещу компресирани струни

В случай на JDK 6 компресирани низове, основен проблем е, че конструкторът String приема само char [] като аргумент. В допълнение към това, много String операции зависят от представяне char [], а не от байтов масив. Поради това трябваше да се направи много разопаковане, което се отрази на производителността.

Докато в случай на компактен низ, поддържането на допълнителното поле „кодер“ също може да увеличи режийните разходи. За да се смекчат разходите за кодера и разопаковането на байтове към символи (в случай на представяне на UTF-16), някои от методите са заплетени и ASM кодът, генериран от JIT компилатора, също е подобрен.

Тази промяна доведе до някои контраинтуитивни резултати. LATIN-1 indexOf (String) извиква присъщ метод, докато indexOf (char) не. В случай на UTF-16 и двата метода извикват вътрешен метод. Този проблем засяга само низа LATIN-1 и ще бъде коригиран в бъдещи версии.

По този начин компактните струни са по-добри от компресираните струни по отношение на производителността.

За да разберете колко памет е спестена с помощта на компактните низове, бяха анализирани различни дъмп копия на Java приложения. И докато резултатите бяха силно зависими от конкретните приложения, цялостните подобрения бяха почти винаги значителни.

4.1. Разлика в представянето

Let's see a very simple example of the performance difference between enabling and disabling Compact Strings:

long startTime = System.currentTimeMillis(); List strings = IntStream.rangeClosed(1, 10_000_000) .mapToObj(Integer::toString) .collect(toList()); long totalTime = System.currentTimeMillis() - startTime; System.out.println( "Generated " + strings.size() + " strings in " + totalTime + " ms."); startTime = System.currentTimeMillis(); String appended = (String) strings.stream() .limit(100_000) .reduce("", (l, r) -> l.toString() + r.toString()); totalTime = System.currentTimeMillis() - startTime; System.out.println("Created string of length " + appended.length() + " in " + totalTime + " ms.");

Here, we are creating 10 million Strings and then appending them in a naive manner. When we run this code (Compact Strings are enabled by default), we get the output:

Generated 10000000 strings in 854 ms. Created string of length 488895 in 5130 ms.

Similarly, if we run it by disabling the Compact Strings using: -XX:-CompactStrings option, the output is:

Generated 10000000 strings in 936 ms. Created string of length 488895 in 9727 ms.

Clearly, this is a surface level test, and it can't be highly representative – it's only a snapshot of what the new option may do to improve performance in this particular scenario.

5. Conclusion

In this tutorial, we saw the attempts to optimize the performance and memory consumption on the JVM – by storing Strings in a memory efficient way.

Както винаги, целият код е достъпен в Github.