1 Het Java Virtual Machine Bij de compilatie van een C programma wordt de code van de programmeur omgezet in machinecode, die kan worden uitgevoerd door de processor van de machine waarop het programma gecompileerd werd. Een Java programma wordt ook gecompileerd, maar het resultaat hiervan is geen machinecode, maar wel zogenaamde Java bytecode. Deze kan niet worden uitgevoerd op een echte hardware processor, maar wordt gedraaid op een virtuele processor, het Java Virtual Machine (JVM). Hier zijn een aantal voordelen aan verbonden: • Platformonafhankelijkheid: de bytecode kan ongewijzigd draaien op elk platform waarop een JVM draait. • Garbage collection of vuilnisophaling: de programmeur wordt verlost van een deel van de verantwoordelijkheid voor het geheugenbeheer. • Hoog-niveau informatie: de bytecode bevat (in tegenstelling tot gewone machinecode) nog informatie over objecten en klassen. Dit laat bijvoorbeeld toe om interessantere foutboodschappen te genereren, maar zorgt ook voor de mogelijkheden tot reflectie, zoals verderop uitgelegd. 1.1 Wat terminologie Een programma kan gegevens bijhouden op twee verschillende plaatsen: • Variabelen (bv. lokale variabelen in een methode, argumenten van een methode) komen op de stack. Elke methode-oproep krijgt zijn eigen stackframe op deze stack, met daarin haar eigen variabelen en argumenten. • Geheugen dat door de programmeur zelf gealloceerd wordt (in Java met een new; in C met een malloc) komt op de heap. Dit is gewoon ongestructureerde geheugenruimte die voor dit programma opzij gezet is. Het beheer van de stack gebeurt sowieso transparant voor de programmeur, maar bijvoorbeeld in C staat hij zelf in voor het beheer van de heap. Dit betekent o.a. dat hij ervoor moet zorgen dat het geheugen dat hij aanvraagt met een malloc ook weer wordt vrijgegeven met een free. Als dit niet gebeurt, spreken we van een geheugenlek (memory leak) en zal het programma langzaam zijn beschikbare hoeveelheid geheugen zien afnemen. Dergelijke geheugenlekken zijn nefast voor de performatie van een programma en treden helaas veel op. Nog erger is het als een programmeur geheugen te vroeg vrijgeeft. Dit kan immers aanleiding geven tot moeilijk op te sporen, niet-reproduceerbare bugs in het programma. 1 1.2 Vuilnisophaling Java zal de programmeur ook ontlasten van het beheer van de heap. Het geheugen dat de programmeur toewijst aan zijn objecten middels de new operator zal door het JVM automatisch weer worden vrijgegeven als de programmeur dit niet meer nodig heeft. Hoe gaat het JVM dit nu beslissen? Het criterium is dat het JVM een geheugenplaats mag vrijgeven van zodra de programmeur geen enkele manier heeft om nog te verwijzen naar het desbetreffende object. Een eenvoudige manier om na te gaan welke geheugenplaatsen dit zijn, is als volgt. Elk object in het geheugen heeft een extra bit, die gebruikt wordt om aan te geven of dit object nog bereikbaar is of niet. Als het tijd wordt om het vuilnis op te halen (bijvoorbeeld omdat het geheugen begint vol te raken), dan gebeurt het volgende: • We zorgen dat alle “bereikbaar”-bits om te beginnen om 0 staan. • Dan overlopen we alle referenties naar die heap die momenteel nog op de stack staan. Voor elk van de gerefereerde objecten zetten we eerst1 zijn “bereikbaar”-bit op 1, en bekijken dan alle referentie die dit object zelf nog heeft. Voor elke referentie die verwijst naar een object waarvan de “bereikbaar”-bit nog op 0 staat, gaan we recursief deze procedure herhalen, dwz. we zetten eerst de bit van dat object op 1 en overlopen dan al zijn referenties, en voor elk gerefereerd object wiens bit nog op 0 staat kijken, zetten we eerst zijn bit op 1 enzovoort. • Tot slot overlopen we alle in gebruik zijnde geheugenplaatsen en geven we elke plaats waarvan de “bereikbaar”-bit nog steeds op 0 staat weer vrij. Daarbij zetten we meteen ook aal “bereikbaar” bits weer op 0, zodat deze al meteen klaar zijn voor een volgende vuilnisophaling. Het vrijgeven van geheugen kan eenvoudig gebeuren door dit geheugen op te nemen in een lijst (mogelijk een gelinkte lijst) van vrije geheugenplaatsen. Dit wordt het “markeer en veeg” algoritme (mark and sweep) genoemd, omdat we eerst de bereikbare geheugenplaatsen gaan markeren en daarna al de overschot gaan opvegen. Dit algoritme was het eerste algoritme dat voor vuilnisophaling ontwikkeld werd (in 1959!). Varianten van dit algoritme worden vandaag de dag nog steeds gebruikt. We zien hier dat de vuilnisophaling een object enkel kan verwijderen als geen enkel bereikbaar object nog een referentie heeft naar dit object. Voor de programmeur betekent dit dat het aanbeveling verdient om zijn referenties naar een bepaald object zo snel mogelijk weg te gooien als hij dit niet meer nodig heeft (bv. met een object = null;), zodat dit zo snel mogelijk opgehaald kan worden. 1 Het is belangrijk om dit eerst te doen, omdat we anders het risico lopen van een oneindige lus. 2 Dit markeer-en-veeg algoritme heeft twee belangrijke nadelen: • Tijdens de markeringsfase mag er niets veranderen aan de structuren in het geheugen. Dit betekent dat het programma dan tijdelijk gepauzeerd moet worden, wat mogelijk tot frustratie kan leiden bij de gebruiker, en al helemaal niet door de beugel kan bij real-time toepassingen. • Als er eerst een aantal objecten worden toegekend in een aanéénsluitend geheugenblok en sommige van deze objecten daarna worden opgeruimd, kan er een erg gefragmenteerd geheugen ontstaan, zodat het moeilijk wordt om nog grote blokken geheugen toe te kennen. Het tweede probleem valt te verhelpen door een “markeer en compacteer” algoritme, dat na de markeer-fase alle nog bereikbare objecten weer gaat samenvoegen tot een aané’éngesloten blok geheugen. Een nadeel hiervan is wel dat het vuilnisophalen zo nog langer zal gaan duren. 1.3 Reflectie Een interessant eigenschap van Java is dat de taal de mogelijkheid biedt om at runtime klassen en objecten te inspecteren, te laden, en te manipuleren. Een eenvoudig voorbeeld is de klasse Demo.java, die een gebruiker toelaat om een nieuw object van een at runtime ingegeven klasse aan te maken, en hierop een methode op te roepen. Een nuttigere toepassing is de klasse ReflectiveState.java. Deze hoort bij de eerder geziene XML parser en vervangt door de klassen die instonden voor het parsen van de tags <boek>, <boeken> en <persoon>. De parser van het tag <auteur> is bijvoorbeeld dit: State AUTEUR = new ReflectiveState("Persoon", "auteur", voornaamEnAchternaam); Hierbij wordt de ReflectiveState opgedragen om het tag <auteur> te parsen en het resultaat in een object van de klasse Persoon onder te brengen. De reflectie mogelijkheden van Java komen hierbij goed van om de eigenschappen van de klasse met als naam Persoon te bekijken. Zo zullen we bijvoorbeeld het resultaat van het geparsete <voornaam> tag behandelen door te kijken of deze klasse een methode met als naam “setVoornaam”, “addVoornaam”, “set”, of “add” kent, die de juiste argumenten aanvaardt. 2 2.1 Nuttige tools JVisualVM Deze handige GUI visualizeert de toestand van een JVM. Zo kan bijvoorbeeld gekeken worden naar verbruikte CPU tijd en geheugenruimte. Ook kan de 3 toestand van de heap op een bepaald moment in detail geı̈nspecteerd worden en valt er te zien welke threads er op elk moment actief zijn. Dit kan handig zijn tijdens het debuggen of profilen (dwz. nagaan of de performantie van het programma voldoet en opsporen waar eventuele problemen juist gesitueerd zijn) van een programma. 2.2 Javadoc Met deze tool kan automatisch HTML documentatie gegenereerd worden op basis van Java klassen. De programmeur kan in zijn commentaar speciale annotaties voor javadoc opnemen. De klasse State.java uit de XML parser bevat hier voorbeelden van. 2.3 JUnit Bij het schrijven van een programma is het belangrijk dat de code vaak en grondig getest wordt. Het JUnit pakket ondersteunt het schrijven van zogenaamde unit tests (tests van kleine eenheden code, zoals een enkele methode). Dit pakket is geı̈ ntegreerd in IDEs zoals Eclipse en NetBeans, wat het gebuik ervan natuurlijk sterk vereenvoudigt. 4