Cookies op Tweakers

Tweakers maakt gebruik van cookies, onder andere om de website te analyseren, het gebruiksgemak te vergroten en advertenties te tonen. Door gebruik te maken van deze website, of door op 'Ga verder' te klikken, geef je toestemming voor het gebruik van cookies. Je kunt ook een cookievrije versie van de website bezoeken met minder functionaliteit. Wil je meer informatie over cookies en hoe ze worden gebruikt, bekijk dan ons cookiebeleid.

Meer informatie

Door , , reacties: 138, views: 131.321 •

Minder objecten gebruiken

De beste manier om geheugen te besparen is minder objecten te gebruiken. Vooral immutable objecten, zoals strings, zijn ideaal om te delen tussen verschillende objecten, waarbij maar één object nodig is voor een unieke waarde.

java.lang.String

Zoals we al eerder meldden, is de string in Java immutable. Dat houdt in dat de inhoud van een eenmaal aangemaakt object nooit meer wordt aangepast. Als er een bewerking op plaatsvindt, wordt er een nieuw object met een nieuwe inhoud gegenereerd; elke verwijzing naar de oude versie blijft echter ook geldig en geeft de oude, onveranderde waarde terug. Voor dezelfde tekst kan dus ook daadwerkelijk dezelfde string gebruikt worden, ongeacht of dat met diverse objecten, verschillende typen objecten of diverse threads gebeurt.

Aangezien de string een van de meest gebruikte data-objecten in Java is, hebben profilers vaak speciale routines om string-objecten naar inhoud te sorteren en om te zoeken naar dubbele of lege strings. Overigens kan Eclipse Memory Analyzer dat ook met andere objecten en zelfs met arrays.

Eclipse Memory Analyzer: group by string value

Als je net als Tweakers.net met databases werkt, is de kans groot dat je voor elke tekst in elke record een nieuw string-object krijgt, ook als de tekst leeg is. In ons geval hebben we een slordige miljoen lege strings in de database: niet ingevulde url-velden, lege titels of beschrijvingen, enzovoort. Met 40 bytes per stuk is een miljoen lege strings goed voor zowat 40MB verspild geheugen.

Hiervoor zijn allerlei eenvoudige oplossingen te verzinnen. Je kunt bijvoorbeeld een utility-klasse voorzien van een methode die nagaat of een string leeg is en dan een referentie naar een vaste, lege standaardstring teruggeeft. Dat kan met zoiets:

class StringUtil {
  private static final String EMPTY_STRING = "";
  /**
   * Prevent copies of the empty string "".
   * @param input A potential copy of the empty string.
   * @return The reference to the singleton empty String
   *         or the original string if it was non-empty.
   */
  public static String getUniqueString(String input) {
    return (input != null && input.isEmpty() ? EMPTY_STRING : input);
  }
}

Als er veel dubbele strings zijn, zijn er twee manieren om het aantal objecten te reduceren. Bij een kleine, vaste verzameling korte teksten is het al gauw interessant om met een enum te werken. Strings worden dan in feite weggegooid zodra de bijbehorende enum in de bijbehorende vaste lijst is gevonden.

Aangezien een enum ook een klasse is, kun je er eventueel extra functionaliteit in stoppen. Je kunt bijvoorbeeld een enum voor http-statuscodes definiëren die naast de tekst ook de statuscode zelf bevat. Bovendien bestaan voor enums ook de speciale EnumSet- en EnumMap-klassen, waarmee nog extra werk en geheugen bespaard kunnen worden.

De tweede manier is om te werken met een StringInterner of canonicalizer. Hiermee worden twee gelijkwaardige strings door hetzelfde, unieke object gerepresenteerd.

Java biedt zelf de String.intern()-methode aan, maar deze kan beter niet voor dit doel gebruikt worden. De string wordt dan in de permgen opgeslagen en die heeft doorgaans een zeer beperkte grootte. In plaats daarvan kan simpelweg een HashMap <String, String> gebruikt worden. Ook Lucenes StringInterner (als je toch al Lucene gebruikt) en de Interner uit Googles Guava-bibliotheek zijn interessant.

class TrivialStringInterner {
  private final HashMap<String, String> stringCache = new HashMap<>();
  /**
   * Return the single unique instance for the given String-content.
* I.e. guarantee that this is true:
* (output1.equals(output2)) == (output1 == output2)
 * @param input A potentially duplicate String. * @return The guaranteed singleton String (within this interner). */ public String intern(String input) { String output = stringCache.get(input); if(output == null) { stringCache.put(input); output = input; } return output; } }

Bij Tweakers.net wordt dit onder andere gebruikt voor de extensies van afbeeldingen. Dat zijn er maar heel weinig, in ons geval maar drie, maar we weten natuurlijk niet op voorhand of er niet toch nog eentje bijkomt. Met bijna 500.000 afbeeldingen in de database is het in elk geval al snel lonend om die te internen. Andere voorbeelden zijn plaatsnamen en landcodes.

Overigens is het internen of canonical maken van objecten ook mogelijk bij andere objecttypen. Bij elk type dat hergebruikt kan worden, kun je besparingen halen. Zo ontdekten we dat er voor onze 450.000 V&A-advertenties maar 60.000 unieke adressen werden gebruikt. Door adressen van de advertenties los te koppelen en ze uniek op te slaan, bespaarden we tientallen megabytes.

Let er wel op dat de datastructuur die je nodig hebt om de unieke objecten te vinden zelf ook geheugen kost. De hashmap voor de V&A-adressen neemt ruim 1,5MB in beslag; daar zouden nog aardig wat adresobjecten in gepast hebben.

Verder gebruikt de Oracle-JVM al unieke strings voor dezelfde tekst. Als in een stuk code tien keer dezelfde tekst staat, dan wordt binnen de applicatie ook steeds dezelfde string gebruikt. De Oracle-JVM roept namelijk voor elke hardcoded string in het programma zelf al String.intern() aan.

Primitieven gebruiken

Andere objecten die in de praktijk veel data opslokken, zijn de wrappers rond de primitieven. Ieder van deze objecten kost 16 bytes en bovendien 4 bytes aan referentie vanuit de plek waar ze gebruikt worden. Een object met acht primitieven van het byte-type neemt zo 168 echte bytes in beslag.

Mocht je toch primitieven in objecten opslaan, gebruik dan in geen geval de constructor, zoals new Boolean(boolVal), maar de factory-methode van Java zelf, zoals Boolean.valueOf(boolVal). Deze maakt handig gebruik van een aantal vaste caches om vaak gebruikte objecten te delen, zoals de booleans false en true, en de integerwaarden van -127 tot 128. Uiteraard kun je zelf ook objecten cachen als je ziet dat je ergens maar een beperkt aantal verschillende waarden hoeft te gebruiken.

Overigens kun je in uitzonderlijke situaties met een Long- of Double-wrapperobject juist geheugen besparen. De referenties ernaar kosten tenslotte maar 4 bytes, terwijl de primitieven zelf 8 bytes ruimte innemen.

Trove-collecties voor primitieven

Collecties in Java zijn erg handig en ze werken goed en snel. Ze hebben echter een nadeel; je kunt er geen primitieven in opslaan. Het enige alternatief is om met arrays van primitieven te werken en dat is niet erg praktisch. Als je een map van int naar int wil bijhouden, moet je Map<Integer, Integer> gebruiken. Met 100.000 key-valueparen komt een hashmap met de bijbehorende entry-objecten dan al op minstens 2,8MB en is er nog 3MB voor de integer-objecten nodig.

Gelukkig zijn er diverse bibliotheken te vinden die het mogelijk maken om direct met primitieven te werken. De compleetste die we zijn tegengekomen is Trove. Naast collecties voor primitieven biedt deze library ook hashtables die met 'open addressing' werken, waardoor er geen entry-objecten nodig zijn.

Java geheugen: HashMap<Integer, Integer> vs TIntIntHashMap volgens Yourkit

De TIntIntHashMap bevat een array met keys, een array met values, een byte-array met de status van velden, 8 floats en ints, en 2 booleans. De 3 arrays zijn wel standaard twee keer zo groot als het aantal opgeslagen objecten, vanwege de implicaties voor de prestaties die aan een kleinere array vastzitten. Voor onze 100.000 key-value paren is uiteindelijk 56 + (12+2*4*100.000) + (12+2*4*100.000) + (12+2*100.000) = 1,7MB nodig en omdat er geen 200.000 integer-objecten meer nodig zijn, ben je met die 1,7MB ook meteen klaar.

Naast de collecties voor primitieven kent Trove overigens ook een compacte THashMap en THashSet, waarbij vooral de laatste aanzienlijk kleiner is dan de Java-tegenhanger. De HashSet van Java is onder de motorkap namelijk gewoon een HashMap, die dus nog meer overhead heeft dan een 'echte' HashMap.

In onze code maken we tegenwoordig veelvuldig gebruik van de diverse primitieve-naar-object-arrays, zoals TByteObjectHashMap en TIntObjectHashMap, en we zijn ook fans geworden van de TIntArrayList en THashSet. Aangezien we deze vrij geleidelijk zijn gaan gebruiken, is het lastig te zeggen hoeveel geheugen we hiermee uiteindelijk hebben bespaard, maar met meer dan een miljoen van deze collecties in het geheugen bedraagt de totale besparing zeker enkele honderden megabytes.


Door Arjen van der Meijden

- Lead Developer

In Oktober 2001 begonnen met als voornaamste taak het technisch beheer van het forum. Daarna doorgegroeid tot senior developer en software architect. Nu lead developer, met een leidinggevende taak aan het team van programmeurs en systeembeheerders van Tweakers.net.



Populair:Apple iPhone 6DestinyAssassin's Creed UnityFIFA 15Nexus 6Call of Duty: Advanced WarfareApple WatchWorld of Warcraft: Warlords of Draenor, PC (Windows)Microsoft Xbox OneAsus

© 1998 - 2014 Tweakers.net B.V. Tweakers is onderdeel van De Persgroep en partner van Computable, Autotrack en Carsom.nl Hosting door True

Beste nieuwssite en prijsvergelijker van het jaar 2013