Java intern() Speicheroptimierung

In Java, um den Speicherverbrauch zu reduzieren, werden konstante Strings durch String-Internierung zu den gleichen Objekten gemacht. Konstante Strings sind Klassenamen, Objektnamen und weitere String Objekte, die zur Laufzeit von Virtual Machine oder im Programm verwendet werden. Beispiel: String s = „text“; – „text“ wird zum konstanten String, alle weiteren vorkommen von „text“ in dem Programm werden durch ein eindeutiger String Objekt repräsentiert.

Java Virtual Machine verwaltet einen Pool von Strings im PermGen Speicher. Beim Erzeugen von konstanten String Objekten wird zuerst (nach hashCode) im Pool gesucht, nur wenn dort nicht bereits vorhanden, wird ein neuer String Objekt instantiiert. Die String-Internierung verursacht manchmal Verwirrungen bei Java Entwicklerneulingen, wenn sie feststellen, dass nicht immer zwei gleichwertige String Objekte mit „==“ Operator vergleichen werden können 🙂

Verschiedene String Objekte können auch zur Laufzeit mit String .intern() Funktion „gecached“ und zum gleichen Objekten verwandelt werden. Zwei gleichwertige String Objekte werden vollständig Zeichen für Zeichen verglichen, beim Vergleich von derselben Objekte reicht nur ein Vergleich dessen Referenzen. Dieser Trick wird von manchen XML Parser benutzt, um das Parsen von XML Dateien zu beschleunigen. Man darf aber mit String-Internierung zur Laufzeit nicht übertreiben, als Beispiel dazu ein kleines Programm, das sehr schnell eine Liste von verschiedenen internierten Strings erzeugt.

public class StringInternTest {
    public static void main(String[] args) {

        String[] array = new String[1000000];        
        for (int i = 1; i<= 1000000; i++){
            array[i] = Integer.toBinaryString(i).intern();
            
            if (i % 100000 == 0) {
                System.gc();
                long totalMemory = Runtime.getRuntime().totalMemory() / 1024;
                long refMemory =  i * 4 / 1024;
                long freeMemory = Runtime.getRuntime().freeMemory() / 1024;

                StringBuffer buf = new StringBuffer();
                buf.append("Counter: " + i);
                buf.append(" total memory: " + totalMemory + " kb,");
                buf.append(" ref: " + refMemory + " kb,");
                buf.append(" free: " + freeMemory + " kb");
                System.out.println(buf.toString());
            }            
        }
    }
}

Im Eclipse mit typischen Speicher-Einstellungen hat dieses Programm nur 8 Zyklen überlebt.

Counter: 100000 total memory: 7732 kb, ref: 390 kb, free: 3651 kb
Counter: 200000 total memory: 7732 kb, ref: 781 kb, free: 3651 kb
Counter: 300000 total memory: 7732 kb, ref: 1171 kb, free: 3654 kb
Counter: 400000 total memory: 7732 kb, ref: 1562 kb, free: 3654 kb
Counter: 500000 total memory: 7732 kb, ref: 1953 kb, free: 3654 kb
Counter: 600000 total memory: 7732 kb, ref: 2343 kb, free: 3654 kb
Counter: 700000 total memory: 7732 kb, ref: 2734 kb, free: 3654 kb
Counter: 800000 total memory: 7732 kb, ref: 3125 kb, free: 3654 kb
Exception in thread "main" java.lang.OutOfMemoryError: PermGen space
	at java.lang.String.intern(Native Method)
	at test.StringInternTest.main(StringInternTest.java:8)

Wie man sieht, wurde nur wenig vom normalen Speicher verbraucht, alle neue String Objekte sind durch Internierung im PermGen gelandet. PermGen ist dadurch überfüllt und Programm endet mit einem OutOfMemoryException. Deswegen, bei sehr großen Mengen von String Objekten mit verschiedenen Werten, darf man String-Internierung nicht zur Speicher-Optimierung verwenden. Dieses Problem kann mit Fliegengewicht Entwurfsmuster gelöst werden. Mit einer generischen Fliegengewicht-Fabrik können sogar nicht nur Strings, sondern alle Klassen von Objekten verwaltet werden.

Generische Fliegengewicht Fabrik:

public class GenericFlyweightFactory <E extends Object> {
     protected long analyzed = 0;
     protected long foundInCache = 0;
     private Map <E, E> cache = null;

   public GenericFlyweightFactory() {
       cache = new HashMap<E, E>();
   }
   public GenericFlyweightFactory(final int count) {
       cache = new HashMap <E, E>(count);
   }

   public E get(final E key) {
       analysed++;
       final E value = cache.get(key);
       if (value == null) {
           cache.put(key, key);
           return key;
       }
       foundInCache++;
       return value;
   }

   public int getSize() { return cache.size(); }
   public long getAnalyzed() { return analyzed; }
   public long getFondInCache() { return foundInCache; }

} 

Beispielszenario: Tabelle „CITIES“ enthält sehr viele Städte, die wir ins Java Programm laden wollen, dabei sind es nur wenige eindeutige Länder zu erwarten. Die gleichen Ländernamen müssen durch eindeutige String Objekte ersetzt werden.

GenericFlyweightFactory<String> factory = new GenericFlyweightFactory<String>();
List<City> cities = new ArrayList<City<();
ResultSet rs = con.executeQuery("SELECT * FROM CITIES");
while (rs.next()) {
    City city = new City();
    city.setId(rs.getLong("ID"));
    String country = rs.getString("COUNTRY_NAME");
    country = factory.get(country);
    city.setCountry(country);
    city.setName(rs.getString("CITY_NAME"));
    cities.add(city);
}

Statt HashMap kann die org.apache.commons.collections.ReferenceMap Klasse und entsprechende HARD, SOFT oder WEAK Referenzen für flexible speichersensitive Caches von Objekten verwendet werden. Der „==“ Operator in diesem Fall darf nicht mehr zum Vergleich von zwei Objekten verwendet werden.

PermGen Speicher-Einstellungen für Hotspot VM
Fliegengewicht Entwurfmuster

Veröffentlicht in Java