Skip to content
Articles

JVM Tutorial – Java Virtual Machine Architecture Explained for Beginners

Posted on

Of je Java nu gebruikt om programma’s te ontwikkelen of niet, misschien heb je ooit wel eens van de Java Virtual Machine (JVM) gehoord.

De JVM vormt de kern van het Java-ecosysteem en maakt het mogelijk dat op Java gebaseerde softwareprogramma’s de “write once, run anywhere”-aanpak kunnen volgen. Je kunt Java code schrijven op een machine, en het op elke andere machine uitvoeren met behulp van de JVM.

JVM was oorspronkelijk ontworpen om alleen Java te ondersteunen. In de loop der tijd zijn echter vele andere talen, zoals Scala, Kotlin en Groovy, op het Java-platform geïntroduceerd. Al deze talen staan gezamenlijk bekend als JVM talen.

In dit artikel zullen we meer leren over de JVM, hoe het werkt, en de verschillende componenten waaruit het is opgebouwd.

Voordat we ons gaan verdiepen in de JVM, laten we het concept van een Virtuele Machine (VM) nog eens bekijken.

Een virtuele machine is een virtuele representatie van een fysieke computer. We kunnen de virtuele machine de gastmachine noemen, en de fysieke computer waarop deze draait is de hostmachine.

Op een enkele fysieke machine kunnen meerdere virtuele machines draaien, elk met hun eigen besturingssysteem en toepassingen. Deze virtuele machines zijn van elkaar geïsoleerd.

Wat is de Java Virtual Machine?

In programmeertalen als C en C++ wordt de code eerst gecompileerd tot platform-specifieke machinecode. Deze talen worden gecompileerde talen genoemd.

Bij talen als JavaScript en Python daarentegen voert de computer de instructies direct uit zonder ze te compileren. Deze talen worden geïnterpreteerde talen genoemd.

Java maakt gebruik van een combinatie van beide technieken. Java-code wordt eerst gecompileerd in byte-code om een klassebestand te genereren. Dit klassebestand wordt vervolgens door de Java Virtual Machine geïnterpreteerd voor het onderliggende platform. Hetzelfde klassebestand kan worden uitgevoerd op elke versie van de JVM die op elk platform en besturingssysteem draait.

Gelijk aan virtuele machines, creëert de JVM een geïsoleerde ruimte op een host-machine. Deze ruimte kan worden gebruikt om Java-programma’s uit te voeren, ongeacht het platform of besturingssysteem van de machine.

Java Virtual Machine Architecture

De JVM bestaat uit drie afzonderlijke componenten:

  1. Class Loader
  2. Runtime Memory/Data Area
  3. Execution Engine

Laten we elk van hen eens in meer detail bekijken.

Class Loader

Wanneer u een .java bronbestand compileert, wordt dit geconverteerd naar byte-code als een .class bestand. Wanneer je deze class in je programma probeert te gebruiken, laadt de class loader hem in het hoofdgeheugen.

De eerste class die in het geheugen wordt geladen is meestal de class die de main() methode bevat.

Er zijn drie fasen in het class loading proces: loading, linking, en initialization.

Laden

Laden houdt in dat de binaire representatie (bytecode) van een klasse of interface met een bepaalde naam wordt genomen, en dat op basis daarvan de oorspronkelijke klasse of interface wordt gegenereerd.

Er zijn drie ingebouwde class loaders beschikbaar in Java:

  • Bootstrap Class Loader – Dit is de root class loader. Het is de superklasse van Extension Class Loader en laadt de standaard Java-pakketten zoals java.langjava.netjava.utiljava.io, enzovoort. Deze pakketten zijn aanwezig in het rt.jar bestand en andere core libraries aanwezig in de $JAVA_HOME/jre/lib directory.
  • Extension Class Loader – Dit is de subclass van de Bootstrap Class Loader en de superclass van de Application Class Loader. Deze laadt de extensies van standaard Java bibliotheken die aanwezig zijn in de $JAVA_HOME/jre/lib/ext directory.
  • Application Class Loader – Dit is de laatste class loader en de subklasse van Extension Class Loader. Hij laadt de bestanden die op het classpath staan. Standaard is het classpath ingesteld op de huidige directory van de applicatie. Het classpath kan ook worden gewijzigd door de -classpath of -cp command line optie toe te voegen.

De JVM gebruikt de ClassLoader.loadClass() methode om de class in het geheugen te laden. Hij probeert de klasse te laden op basis van een volledig gekwalificeerde naam.

Als een ouder class loader een class niet kan vinden, delegeert hij het werk naar een child class loader. Als de laatste child class loader de class ook niet kan laden, gooit hij NoClassDefFoundError of ClassNotFoundException.

Linking

Nadat een klasse in het geheugen is geladen, ondergaat deze het linking-proces. Bij het linken van een klasse of interface worden de verschillende elementen en afhankelijkheden van het programma samengevoegd.

Het koppelen omvat de volgende stappen:

Verificatie: Deze fase controleert de structurele juistheid van het .class bestand door het te toetsen aan een set van constraints of regels. Als de verificatie om een of andere reden mislukt, krijgen we een VerifyException.

Bijv. als de code is gebouwd met Java 11, maar wordt uitgevoerd op een systeem waarop Java 8 is geïnstalleerd, zal de verificatiefase mislukken.

Voorbereiding: In deze fase wijst de JVM geheugen toe voor de statische velden van een klasse of interface, en initialiseert deze met standaardwaarden.

Voorbeeld: u hebt de volgende variabele in uw klasse gedeclareerd:

private static final boolean enabled = true;

Tijdens de voorbereidingsfase wijst de JVM geheugen toe aan de variabele enabled en stelt de waarde in op de standaardwaarde voor een boolean, namelijk false.

Oplossing: In deze fase worden symbolische verwijzingen vervangen door directe verwijzingen die aanwezig zijn in de runtime constant pool.

Bijv. verwijzingen naar andere klassen of constante variabelen in andere klassen worden in deze fase opgelost en vervangen door hun werkelijke referenties.

Initialisatie

Initialisatie omvat het uitvoeren van de initialisatiemethode van de klasse of interface (bekend als <clinit>). Dit kan inhouden het aanroepen van de constructor van de klasse, het uitvoeren van het static block, en het toekennen van waarden aan alle statische variabelen. Dit is de laatste fase van het laden van de klasse.

Bijv. toen we eerder de volgende code declareerden:

private static final boolean enabled = true;

De variabele enabled werd tijdens de voorbereidingsfase op zijn standaardwaarde false gezet. In de initialisatiefase krijgt deze variabele de werkelijke waarde true.

Note: de JVM is multi-threaded. Het kan gebeuren dat meerdere threads tegelijkertijd dezelfde class proberen te initialiseren. Dit kan leiden tot concurrency problemen. Je moet zorgen voor thread safety om te zorgen dat het programma goed werkt in een multi-threaded omgeving.

Runtime Data Area

Er zijn vijf componenten in het runtime data area:

Laten we ze eens stuk voor stuk bekijken.

Method Area

Alle gegevens op klassenniveau, zoals de run-time constant pool, veld- en methodegegevens, en de code voor methoden en constructors, worden hier opgeslagen.

Als het beschikbare geheugen in het methodegebied niet voldoende is voor het opstarten van het programma, gooit de JVM een OutOfMemoryError.

Voorbeeld: u hebt de volgende class-definitie:

public class Employee { private String name; private int age; public Employee(String name, int age) { this.name = name; this.age = age; }}

In dit codevoorbeeld worden de gegevens op veldniveau, zoals name en age en de constructordetails in het methodegebied geladen.

Het methodegebied wordt gemaakt bij het opstarten van de virtuele machine, en er is maar één methodegebied per JVM.

Heap Area

Alle objecten en hun bijbehorende instantievariabelen worden hier opgeslagen. Dit is het runtime data gebied van waaruit geheugen voor alle klasse-instanties en arrays wordt gealloceerd.

Voorbeeld: u declareert de volgende instantie:

Employee employee = new Employee();

In dit codevoorbeeld wordt een instantie van Employee gecreëerd en in het heap-gebied geladen.

De heap wordt gecreëerd bij het opstarten van de virtuele machine, en er is maar één heap-gebied per JVM.

Opmerking: Omdat de Methode- en Heap-gebieden hetzelfde geheugen delen voor meerdere threads, zijn de hier opgeslagen gegevens niet thread safe.

Stack-gebied

Wanneer een nieuwe thread in de JVM wordt aangemaakt, wordt tegelijkertijd ook een aparte runtime stack aangemaakt. Alle lokale variabelen, methode-aanroepen en gedeeltelijke resultaten worden in het stackgebied opgeslagen.

Als de verwerking die in een thread wordt gedaan een grotere stackgrootte vereist dan wat beschikbaar is, gooit de JVM een StackOverflowError.

Voor elke methode-aanroep wordt een entry gemaakt in het stackgeheugen, dat het Stack Frame wordt genoemd. Als de methode-aanroep is voltooid, wordt de Stack Frame vernietigd.

De Stack Frame is onderverdeeld in drie subdelen:

  • Lokale variabelen – Elk frame bevat een array van variabelen die bekend staan als de lokale variabelen. Alle lokale variabelen en hun waarden worden hier opgeslagen. De lengte van deze array wordt bepaald tijdens het compileren.
  • Operand Stack – Elk frame bevat een last-in-first-out (LIFO) stack die bekend staat als de operand stack. Deze fungeert als een runtime werkruimte voor het uitvoeren van eventuele tussenliggende operaties. De maximale diepte van deze stack wordt bepaald tijdens het compileren.
  • Frame Data – Alle symbolen die overeenkomen met de methode worden hier opgeslagen. Hier wordt ook de catch block informatie opgeslagen in geval van exceptions.

Bij wijze van voorbeeld stel dat je de volgende code hebt:

double calculateNormalisedScore(List<Answer> answers) { double score = getScore(answers); return normalizeScore(score);}double normalizeScore(double score) { return (score – minScore) / (maxScore – minScore);}

In dit codevoorbeeld worden variabelen als answers en score in de Local Variables array geplaatst. De Operand Stack bevat de variabelen en operatoren die nodig zijn om de wiskundige berekeningen van aftrekken en delen uit te voeren.

Opmerking: aangezien het Stack-gebied niet wordt gedeeld, is het inherent thread-veilig.

Program Counter (PC) Registers

De JVM ondersteunt meerdere threads tegelijk. Elke thread heeft zijn eigen PC Register, waarin het adres van de op dat moment uitvoerende JVM instructie wordt opgeslagen. Zodra de instructie is uitgevoerd, wordt het PC-register bijgewerkt met de volgende instructie.

Native Method Stacks

De JVM bevat stacks die native methoden ondersteunen. Deze methoden zijn geschreven in een andere taal dan Java, zoals C en C++. Voor elke nieuwe thread wordt ook een aparte native method stack toegewezen.

Execution Engine

Als de bytecode eenmaal in het hoofdgeheugen is geladen, en de details beschikbaar zijn in het runtime data gebied, is de volgende stap het uitvoeren van het programma. De Execution Engine zorgt hiervoor door de code in elke class uit te voeren.

Hoewel, voordat het programma wordt uitgevoerd, moet de bytecode worden omgezet in machinetaal instructies. De JVM kan een interpreter of een JIT-compiler gebruiken voor de execution engine.

Interpreter

De interpreter leest en voert de bytecode-instructies regel voor regel uit. Vanwege de regel-voor-regel-uitvoering is de interpreter relatief trager.

Een ander nadeel van de interpreter is dat wanneer een methode meerdere malen wordt aangeroepen, elke keer een nieuwe interpretatie nodig is.

JIT Compiler

De JIT Compiler ondervangt het nadeel van de interpreter. De Execution Engine gebruikt eerst de interpreter om de byte-code uit te voeren, maar wanneer hij herhaalde code vindt, gebruikt hij de JIT-compiler.

De JIT compiler compileert dan de gehele bytecode en verandert deze in native machine code. Deze native machinecode wordt direct gebruikt voor herhaalde methode-aanroepen, waardoor de prestaties van het systeem worden verbeterd.

De JIT-compiler bestaat uit de volgende onderdelen:

  1. Intermediate Code Generator – genereert intermediate code
  2. Code Optimizer – optimaliseert de intermediate code voor betere prestaties
  3. Target Code Generator – converteert intermediate code naar native machine code
  4. Profiler – vindt de hotspots (code die herhaaldelijk wordt uitgevoerd)

Om het verschil tussen interpreter en JIT-compiler beter te begrijpen, stel dat u de volgende code heeft:

int sum = 10;for(int i = 0 ; i <= 10; i++) { sum += i;}System.out.println(sum);

Een interpreter haalt voor elke iteratie in de lus de waarde van sum op uit het geheugen, voegt de waarde van i eraan toe, en schrijft het terug naar het geheugen. Dit is een kostbare operatie, omdat het geheugen telkens wordt benaderd als de lus wordt binnengegaan.

De JIT-compiler zal echter herkennen dat deze code een HotSpot heeft, en zal er optimalisaties op uitvoeren. Hij slaat een lokale kopie van sum op in het PC-register voor de thread en voegt de waarde van i er steeds aan toe in de lus. Zodra de lus is voltooid, wordt de waarde sum terug naar het geheugen geschreven.

Note: een JIT-compiler heeft meer tijd nodig om de code te compileren dan de interpreter om de code regel voor regel te interpreteren. Als je een programma maar één keer gaat uitvoeren, is het beter om de interpreter te gebruiken.

Garbage Collector

De Garbage Collector (GC) verzamelt en verwijdert objecten van de heap waarnaar niet verwezen wordt. Het is het proces van het automatisch terugwinnen van runtime ongebruikt geheugen door ze te vernietigen.

Garbage collection maakt Java geheugen efficiënt, omdat het de objecten waarnaar niet verwezen wordt uit het heap geheugen verwijdert en ruimte vrij maakt voor nieuwe objecten. Het omvat twee fasen:

  1. Mark – in deze stap identificeert de GC de ongebruikte objecten in het geheugen
  2. Sweep – in deze stap verwijdert de GC de objecten die tijdens de vorige fase zijn geïdentificeerd

Garbage Collections wordt automatisch door de JVM gedaan op gezette tijden en hoeft niet apart te worden afgehandeld. Het kan ook worden gestart door System.gc() aan te roepen, maar de uitvoering is niet gegarandeerd.

De JVM bevat 3 verschillende typen garbage collectors:

  1. Serial GC – Dit is de eenvoudigste implementatie van GC, en is ontworpen voor kleine applicaties die op single-threaded omgevingen draaien. Het gebruikt een enkele thread voor garbage collection. Wanneer het wordt uitgevoerd, leidt het tot een “stop de wereld”-gebeurtenis waarbij de hele toepassing wordt gepauzeerd. Het JVM argument om Serial Garbage Collector te gebruiken is -XX:+UseSerialGC
  2. Parallel GC – Dit is de standaard implementatie van GC in de JVM, en is ook bekend als Throughput Collector. Het gebruikt meerdere threads voor garbage collection, maar pauzeert nog steeds de applicatie tijdens het draaien. Het JVM argument om Parallel Garbage Collector te gebruiken is -XX:+UseParallelGC.
  3. Garbage First (G1) GC – G1GC is ontworpen voor multi-threaded applicaties die een grote heap grootte beschikbaar hebben (meer dan 4GB). Het verdeelt de heap in een aantal regio’s van gelijke grootte, en gebruikt meerdere threads om ze te scannen. G1GC identificeert de regio’s met de meeste vuilnis en voert eerst vuilnisverzameling uit op die regio. Het JVM-argument om G1 Garbage Collector te gebruiken is -XX:+UseG1GC

Note: Er is een ander type garbage collector, genaamd Concurrent Mark Sweep (CMS) GC. Deze is echter sinds Java 9 afgeschreven en in Java 14 volledig verwijderd ten gunste van G1GC.

Java Native Interface (JNI)

Op sommige momenten is het nodig om native (niet-Java) code te gebruiken (bijvoorbeeld C/C++). Dit kan het geval zijn wanneer we moeten communiceren met hardware, of om het geheugenbeheer en de prestatiebeperkingen in Java te overwinnen. Java ondersteunt de uitvoering van native code via de Java Native Interface (JNI).

JNI fungeert als een brug voor het toestaan van de ondersteunende pakketten voor andere programmeertalen zoals C, C++, enzovoort. Dit is vooral handig in gevallen waarin je code moet schrijven die niet volledig door Java wordt ondersteund, zoals sommige platform-specifieke functies die alleen in C kunnen worden geschreven.

Je kunt het sleutelwoord native gebruiken om aan te geven dat de methode-implementatie zal worden verzorgd door een native bibliotheek. U moet ook System.loadLibrary() aanroepen om de gedeelde native library in het geheugen te laden, en de functies ervan beschikbaar te maken voor Java.

Native Method Libraries

Native Method Libraries zijn bibliotheken die zijn geschreven in andere programmeertalen, zoals C, C++, en assembly. Deze bibliotheken zijn meestal aanwezig in de vorm van .dll of .so bestanden. Deze native libraries kunnen via JNI worden geladen.

Common JVM Errors

  • ClassNotFoundExcecption – Dit treedt op wanneer de Class Loader klassen probeert te laden met Class.forName()ClassLoader.loadClass() of ClassLoader.findSystemClass() maar er geen definitie voor de klasse met de opgegeven naam wordt gevonden.
  • NoClassDefFoundError – Deze treedt op als een compiler de class met succes heeft gecompileerd, maar de Class Loader het class-bestand in runtime niet kan lokaliseren.
  • OutOfMemoryError – Dit gebeurt wanneer de JVM een object niet kan toewijzen omdat het geheugen op is, en er geen geheugen meer beschikbaar kon worden gemaakt door de garbage collector.
  • StackOverflowError – Dit treedt op als de JVM ruimte te kort komt bij het aanmaken van nieuwe stack frames tijdens het verwerken van een thread.

Conclusie

In dit artikel hebben we de architectuur van de Java Virtual Machine en de verschillende onderdelen ervan besproken. Vaak graven we niet diep in de interne mechanica van de JVM of geven we er niet om hoe het werkt terwijl onze code werkt.

Alleen als er iets fout gaat, en we de JVM moeten tweaken of een geheugenlek moeten repareren, proberen we de interne mechanismen te begrijpen.

Dit is ook een zeer populaire vraag in sollicitatiegesprekken, zowel op junior als senior niveau voor backend functies. Een goed begrip van de JVM helpt je om betere code te schrijven en valkuilen met betrekking tot stack- en geheugenfouten te vermijden.

Bedankt dat je tot nu toe bij me bent gebleven. Ik hoop dat je het artikel leuk vond. Je kunt met me in contact komen op LinkedIn, waar ik regelmatig discussieer over technologie en het leven. Neem ook een kijkje bij een aantal van mijn andere artikelen en mijn YouTube-kanaal. Veel leesplezier. 🙂

Geef een reactie Antwoord annuleren

Het e-mailadres wordt niet gepubliceerd. Vereiste velden zijn gemarkeerd met *

Proudly powered by WordPress | Theme: Monza by AZ-Theme.

深圳SEO优化公司大庆百度爱采购多少钱绥化网站seo优化多少钱营口seo哪家好诸城百度网站优化排名推荐鞍山网站制作多少钱固原网站建设设计抚顺网站建设哪家好肇庆优秀网站设计报价楚雄网站改版推荐西宁外贸网站制作公司白山百度关键词包年推广报价莆田阿里店铺运营推荐朝阳网站优化排名长沙至尊标王价格潜江网站优化排名哪家好天津网站排名优化公司桂林百姓网标王公司新乡建设网站公司长治网站优化排名报价吉祥企业网站改版公司白城关键词排名包年推广公司淮安seo网站优化昆明百姓网标王公司双龙SEO按天计费公司玉树外贸网站建设公司临沂百度网站优化三明模板网站建设多少钱衡阳关键词排名包年推广推荐九江百度seo报价周口网站推广方案歼20紧急升空逼退外机英媒称团队夜以继日筹划王妃复出草木蔓发 春山在望成都发生巨响 当地回应60岁老人炒菠菜未焯水致肾病恶化男子涉嫌走私被判11年却一天牢没坐劳斯莱斯右转逼停直行车网传落水者说“没让你救”系谣言广东通报13岁男孩性侵女童不予立案贵州小伙回应在美国卖三蹦子火了淀粉肠小王子日销售额涨超10倍有个姐真把千机伞做出来了近3万元金手镯仅含足金十克呼北高速交通事故已致14人死亡杨洋拄拐现身医院国产伟哥去年销售近13亿男子给前妻转账 现任妻子起诉要回新基金只募集到26元还是员工自购男孩疑遭霸凌 家长讨说法被踢出群充个话费竟沦为间接洗钱工具新的一天从800个哈欠开始单亲妈妈陷入热恋 14岁儿子报警#春分立蛋大挑战#中国投资客涌入日本东京买房两大学生合买彩票中奖一人不认账新加坡主帅:唯一目标击败中国队月嫂回应掌掴婴儿是在赶虫子19岁小伙救下5人后溺亡 多方发声清明节放假3天调休1天张家界的山上“长”满了韩国人?开封王婆为何火了主播靠辱骂母亲走红被批捕封号代拍被何赛飞拿着魔杖追着打阿根廷将发行1万与2万面值的纸币库克现身上海为江西彩礼“减负”的“试婚人”因自嘲式简历走红的教授更新简介殡仪馆花卉高于市场价3倍还重复用网友称在豆瓣酱里吃出老鼠头315晚会后胖东来又人满为患了网友建议重庆地铁不准乘客携带菜筐特朗普谈“凯特王妃P图照”罗斯否认插足凯特王妃婚姻青海通报栏杆断裂小学生跌落住进ICU恒大被罚41.75亿到底怎么缴湖南一县政协主席疑涉刑案被控制茶百道就改标签日期致歉王树国3次鞠躬告别西交大师生张立群任西安交通大学校长杨倩无缘巴黎奥运

深圳SEO优化公司 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化