1 Het Java Virtual Machine

advertisement
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
Download