ℤ𝑛𝒶𝗖ɀ🇰ⓘ, 𤭢 szlaczki 👌 i krzaczki 🌵

czyli o Stringach w Javie

by jgardo.dev

Strings on heap

Case Rank Count Retained size [MiB]
Hello world 1 21 580 1,294
Pet Clinic (Spring) 3 118 083 6,422
Real prod app 4 361 748 56,858

Przykłady Stringów

  1. Dane refleksji (sygnatury metod, nazwy metod, klas itd.)
  2. Cache poszczególnych liter/alfabetów
  3. Classpathy dla aktualnego JARa
  4. Listy dostępnych języków
  5. Zcache'owane XMLe i inne dokumenty
  6. SQLki z Prepared Statements (JPA generated), nazwy pól, tabel itd.
  7. Message i template'y message'ów

Ile ramu?

JEP-192

O mnie:

  • Jakub Gardo
  • Senior Java developer
  • 11 lat stażu

  • Poznań JUG logo

"Aby zrobić szarlotkę od podstaw, trzeba najpierw wymyślić Wszechświat"

apple cake

Ważne momenty

  • Alfabet (łaciński - ok. 700 p.n.e. to uporządkowany zbiór 26 liter)
  • Maszyna drukarska
  • Maszyna do pisania
  • Telegraf
  • Maszyna licząca
  • Kodowania (n.p. kod Morse'a, kod Baudot'a)

Ascii - 1963/1967


Code Page 852, ISO 8859-2, Windows CP 1250...


Unicode (1991)

Unicode (1991)

  • 7129 characters in 1.0.0 in October 1991
  • 154 998 characters in 16.0 in September 2024
  • Two format versions
    • Universal Character Set (UCS)
    • Unicode Transformation Format (UTF)

Universal Character Set (UCS)

  • UCS2 - 2 bytes - 65536 characters
  • UCS4 - 4 bytes - 2147483647 characters

  • + fixed length
  • + simple read/write
  • - memory footprint

Unicode Transformation Format (UTF)

  • UTF-8 - 1-4 bytes
  • UTF-16 - 2-4 bytes
  • UTF-32 - 4 bytes

  • +/- variable length
  • - transformation required
Unicode Transformation Format (UTF)

Java 1.0 (1995)

  • String encoded with fixed-length UCS-2
  • 2 byte char type
    • char charAt()
    • String replace(char old, char new)
    • char[] toCharArray()
    • boolean contains(CharSequence s)
    • ...

Java 1.5 (2004)

  • Unicode 3.1 with 94k symbols >> 65k
  • 2 byte char interpreted as UTF-16
    • int-typed codePoint is considered as base character representation
  • Adjusted API
    • int codePointAt(int index)
    • int indexOf(int ch)
    • Character.toChars(int codePoint)
    • Character.toCodePoint(char high, char low)
    • ...

java.lang.String

  • Value is immutable
  • implements Comparable
  • implements CharSequence
  • Strings, which value is explicitly defined in code is called literal


public final class String
    implements Serializable,
						Comparable<String>,
						CharSequence,
						Constable, ConstantDesc {

  @Stable private final byte[] value;
	(...)
}
  

public final class String (...) {

  @Stable
  private final byte[] value;
  private final byte coder; // LATIN1 = 0 or UTF16 = 1

	(...)








  public char charAt(int index) {
    return this.isLatin1()
				? StringLatin1.charAt(this.value, index)
				: StringUTF16.charAt(this.value, index);
  }

  public int codePointBefore(int index) {
    int i = index - 1;
    checkIndex(i, this.length());
    return this.isLatin1()
				? this.value[i] & 255
				: StringUTF16.codePointBefore(this.value, index);
  }

  public void getChars(...) {
    checkBoundsBeginEnd(...);
    checkBoundsOffCount(...);
    if (this.isLatin1()) {
      StringLatin1.getChars(...);
    } else {
      StringUTF16.getChars(...);
    }
  }

  public boolean equals(Object anObject) {
    if (this == anObject) {
      return true;
    } else {
      if (anObject instanceof String) {
        String aString = (String)anObject;
        if ((!COMPACT_STRINGS
							|| this.coder == aString.coder)
							&& StringLatin1.equals(
									this.value, aString.value)) {
					return true;
        }
      }
    }
  }
}
  

.hashcode()

public final class String {

  @Stable
  private final byte[] value;
  private final byte coder; // LATIN1 = 0 or UTF16 = 1

	private int hash; // Default to 0
	private boolean hashIsZero; // Default to false;
								// sample zero-hashcoded value
							  // "ARcZguv" or just "\u0000"





  public int hashCode() {
    int h = this.hash;
    if (h == 0 && !this.hashIsZero) {
      h = this.isLatin1()
							? StringLatin1.hashCode(this.value)
							: StringUTF16.hashCode(this.value);
      if (h == 0) { this.hashIsZero = true; }
			else { this.hash = h; }
    }
    return h;
  }
}


final class StringLatin1 {
  public static int hashCode(byte[] value) {
    return switch (value.length) {
      case 0 -> 0;
      case 1 -> value[0] & 0xff;
      default -> ArraysSupport.vectorizedHashCode(
							value, 0, value.length,
							0, ArraysSupport.T_BOOLEAN);
    };
  }
}

public class ArraysSupport {
	@IntrinsicCandidate
	public static int vectorizedHashCode(...)
		return switch (basicType) {
			(...)
			case T_CHAR -> array instanceof byte[]
					? utf16hashCode(initialValue,
							(byte[]) array, fromIndex, length)
					: hashCode(initialValue,
							(char[]) array, fromIndex, length);
		}
	}

  private static int hashCode(int result, char[] a,
							int fromIndex, int length) {
		int end = fromIndex + length;
		for (int i = fromIndex; i < end; i++) {
			result = 31 * result + a[i];
		}
		return result;
	}
}
  

  private static int hashCode() {
		for (int i = 0; i < value.length; i++) {
			result = 31 * result + a[i];
		}
		return result;
	}
  
31 * ('C' - 'D') == ('B' - 'a')
"common_prefixDB".hashCode() == "common_prefixCa".hashCode()
s[0]* 31⁽ⁿ⁻¹⁾ + s[1] * 31⁽ⁿ⁻²⁾ + ... + s[n−1] =

= 31⁽ⁿ⁻⁴⁾(29791*s[0] + 961*s[1] + 31*s[2] + 1*s[3]) + ...


public final class String (...) {
  @Stable private final byte[] value;
  private final byte coder; // LATIN1 = 0 or UTF16 = 1
  private int hash;
  private boolean hashIsZero;
}
// an object header (12B)
// a reference to a byte[] (4B)
// a coder field (1B)
// an int hash (4B)
// a boolean hashIsZero (1B)

// Summary: 22B -> 24B
// + size of value array = 12B + 4B length + byte array
  

public final class String (...) {
  @Stable private final byte[] value;
  private final byte coder;
  private int hash;             // initially 0
  private boolean hashIsZero;   // initially false
}
  

Tworzenie Stringów

Literały


class HelloWorld {
  public static void main(String[] args) {
    System.out.println("Hello World!");
  }
}
class HelloWorldHolder {
  public String hello = "Hello World!";
}

Constant pool:
  #7 = String      #8   // Hello World!
  #8 = Utf8        Hello World!

Code:
  0: getstatic     #15  // Field System.out
  3: ldc           #7   // String Hello World!
  5: invokevirtual #21  // Method PrintStream.println
 

Handle java_lang_String::create_from_symbol(
  						Symbol* symbol, TRAPS) {
  const char* utf8_str = (char*)symbol->bytes();
  int utf8_len = symbol->utf8_length();

  bool has_multibyte, is_latin1;
  int length = UTF8::unicode_length(utf8_str, utf8_len,
  						is_latin1, has_multibyte);

  Handle h_obj = basic_create(length, is_latin1, ...);
  (...)
}


Handle java_lang_String::basic_create(int length,
							bool is_latin1, TRAPS) {
  (...)
  // Create the String object first,
  //						so there's a chance that the String
  // and the char array it
  //						points to end up in the same cache line.
  oop obj;
  obj = vmClasses::String_klass()
							->allocate_instance(CHECK_NH);
  Handle h_obj(THREAD, obj);
  int arr_length = is_latin1 ?
							length : length << 1; // 2 bytes per UTF16.
	(...)
}

StringTable

  • "Cache internowanych Stringów"
  • W natywnej części JVMa (implementowanej w C++)
  • HashTable z linked listą dla zarządzania konfliktami
  • Resizeable. Initial size -XX:StringTableSize
  • Może używać HalfSipHash-2-4 zamiast String.hashCode()
  • String.intern()

String.intern() - why not?

  • public native String intern();
    • Native call
    • Object referenced by native VM structures becomes GC Roots
    • StringTable is nonresizable

Konkatenacja


class Test {
  public static void main(String[] args) {
    String constantConcat = "Hello" + ' ' + "World!";
  }
}

Constant pool:
#7 = String             #8             // Hello World!
#8 = Utf8               Hello World!

public static void main(java.lang.String[]);
0: ldc           #7                  // String Hello World!
2: astore_1
3: return

	String name = args[0];
	String standardConcat = "Hello " + name + "!";
  1. Przy pomocy StringBuffer do Javy 1.4
  2. Przy pomocy StringBuilder do Javy 1.8
  3. Kod generowany jest indywidualnie dla każdej konkatenacji od Javy 9
  4. To zależy w Javie 23

Java 1.8


class Test {
  public static void main(String[] args) {
    String standardConcat = "Hello " + args[0] + "!";
  }
}

 4: invokespecial #3     // StringBuilder."<init>"()
 7: ldc           #4     // String Hello
 9: invokevirtual #5     // StringBuilder.append();
12: aload_0
13: iconst_0
14: aaload
15: invokevirtual #5     // StringBuilder.append();
18: ldc           #6     // String !
20: invokevirtual #5     // StringBuilder.append();
23: invokevirtual #7     // StringBuilder.toString();

Java 9


class Test {
  public static void main(String[] args) {
    String standardConcat = "Hello " + args[0] + "!";
  }
}

1: iconst_0
2: aaload
3: invokedynamic #2,  0   // InvokeDynamic #0;
8: astore_1

BootstrapMethods:
  0: #22 REF_invokeStatic    StringConcatFactory
		.makeConcatWithConstants:(...) CallSite;
    Method arguments:
      #23 Hello \u0001!

class Test {
  public static void main(String[] args) {
    String highArityConcat = f0+","+ f1+","+ f2+"," (...)
        + f10+","+ f11+","+ f12+","+ f13+","+ f14 (...)
        + f20+","+ f21+","+ f22+","+ f23+","+ f24 (...)
        + (...)
        + f120+","+ f121+","+ f122;
  }
}

Tworzenie Stringów
konstruktory


public String(char value[]);
public String(int[] codePoints, int offset, int count);
public String(byte[] bytes,int offset,int length,Charset);
public String(StringBuilder builder);
public String() {
	this.value = "".value;
	this.coder = "".coder;
}
@IntrinsicCandidate
public String(String original) {
	this.value = original.value;
	this.coder = original.coder;
	this.hash = original.hash;
}

String key = "The quick brown fox jumps over the lazy dog"
         .substring(4,5);
someHashMap.put(key, "value");

				

Usuwanie Stringów


public final class String (...) {
  @Stable private final byte[] value;
  private final byte coder;
  private int hash;
  private boolean hashIsZero;
}

Deduplikacja

JEP-192

Zalety Stringa

  • Good enough
  • Backward compatible
  • Always available
  • Supported by JVM
  • Fully supports Unicode
  • Value is immutable

Wady Stringa

  • Memory consuming
  • Unalterable
  • Difficult to remove
  • Not for large texts (large arrays -> G1)
  • Not for passwords or apikeys
  • Weak as hash key?

A jeśli nie String, to co?

  • CharSequence
    • StringBuilder
    • CharBuffer
  • byte[]
  • Reader/InputStream/nio alternative (jeśli za duże Stringi)

Co to będzie?

Valhalla project

  • value class
    • immutable
    • identityless
  • challenges
    • mutable arrays
    • Strings have identity
    • synchronization on Strings
    • hash and hashIsZero non-final fields

Lilliput project

  • Shrink object headers from 12 to 8 bytes
  • Will be delivered in JDK 25
  • -XX:+UseCompactObjectHeaders
    • 22% less heap space
    • 8% less CPU time
    • number of garbage collections reduced by 15%

Podsumowanie

  1. Update your JDK
  2. String is good enough for most cases
  3. JVM is 30 years old... legacy

Questions?


Feedback

Prezentacja