Macchine virtuali e JVM Introduzione Un calcolatore digitale è dotato di una serie di circuiti elettronici che sono in grado di riconoscere ed eseguire solo un insieme limitato di istruzioni elementari. Tutti i programmi devono essere convertiti in sequenze di tali istruzioni, prima di essere eseguiti: l’insieme (set) di istruzioni elementari di un processore è detto linguaggio macchina. Scrivere programmi in linguaggio macchina è complesso, principalmente per due motivi: le istruzioni sono molto “semplici” (elementari), pertanto per realizzare un’azione complessa è necessario scrivere numerose istruzioni in linguaggio macchina; le istruzioni sono in forma binaria. Il problema viene risolto progettando un nuovo linguaggio, costituito da un insieme di istruzioni che risulti più conveniente da usare. Indicando con L0 il linguaggio macchina e con L1 il nuovo linguaggio, possiamo osservare che, tra L0 ed L1 deve esistere una corrispondenza che ad ogni istruzione di L1 associ una o più istruzioni di L0. Esempio La semplice azione di “telefonare un amico”, espressa nel linguaggio L1 da un’unica istruzione, corrisponde alla seguente sequenza di istruzioni del linguaggio L0: 1. alzare la cornetta 2. accertarsi che la linea sia libera 3. comporre il numero 4. parlare 5. abbassare la cornetta per chiudere la comunicazione Il linguaggio L1, per come è stato definito, costituisce un’astrazione rispetto al linguaggio L0. Linguaggio L1 Telefonare a un amico Traduzione Linguaggio L0 1. Alzare la cornetta 2. Linea libera? 3. Comporre il numero 4. Parlare 5. Chiudere la comunicazione Per eseguire i programmi scritti nel linguaggio L1 è necessario disporre di un metodo per passare dalle istruzioni in linguaggio L1 alle corrispondenti istruzioni in linguaggio L0 (le uniche che la macchina può eseguire). Il passaggio da L1 a L0 può avvenire in due diversi modi: 1. mediante traduzione; 2. mediante interpretazione. Autore: Cinzia Bocchi Ultimo aggiornamento: 08/08/11 1 Traduttori La traduzione sostituisce ogni istruzione del linguaggio L1 con l’equivalente sequenza di istruzioni di L0 e genera un nuovo programma nel linguaggio L0. Il calcolatore eseguirà il nuovo programma invece del vecchio programma in L1. La traduzione è svolta da un modulo software chiamato compilatore. Un compilatore è un programma traduttore che riceve in ingresso un programma scritto in linguaggio ad alto livello (codice sorgente) e produce in uscita il suo equivalente in linguaggio macchina (codice oggetto), memorizzandolo in un file. Così un programma compilato una sola volta può essere eseguito quante volte si vuole. La compilazione deve essere seguita da un’ulteriore operazione detta linking (collegamento), svolta da un apposito programma chiamato linker. Tale operazione consiste nell’aggiungere al programma compilato i moduli software che realizzano le funzioni richieste dai vari comandi (librerie) e contemporaneamente nel risolvere i riferimenti a celle di memoria o a variabili. Alla fine di questo lavoro si ottiene il programma eseguibile. Codice sorgente (scritto in linguaggio L1) Codice oggetto (scritto in linguaggio macchina L0) Compilatore Codice eseguibile Linker La traduzione nel linguaggio macchina avviene a patto che il codice sorgente sia formalmente corretto, cioè rispetti le regole del linguaggio scelto. Ogni volta che questo non si verifica, viene emesso un messaggio di errore. Il modulo software che si occupa del controllo degli errori sintattici è chiamato analizzatore sintattico (parser). Gli errori possono riguardare l’uso di termini non appartenenti al linguaggio (errori di tipo lessicale) oppure la costruzione di frasi non corrette dal punto di vista delle regole grammaticali (errori di tipo sintattico). Normalmente il compilatore non è in grado di rilevare errori logici, riguardanti la correttezza dell’algoritmo. Così come la compilazione non può rilevare situazioni di errore che si possono verificare durante l’esecuzione del programma (errori runtime), ad esempio la divisione per zero. E’ importante sottolineare che, dato un linguaggio, non esiste un solo compilatore ma tanti quante sono le macchine sulle quali si desidera eseguire i programmi nel dato linguaggio. Ogni compilatore, infatti, traduce il codice sorgente nel codice macchina di una particolare piattaforma hardware. Interpreti L’interpretazione consiste nello scrivere un programma in L0, chiamato interprete, che prende il programma scritto in linguaggio L1 come input e, contemporaneamente, traduce in L0 ed esegue un’istruzione alla volta. Autore: Cinzia Bocchi Ultimo aggiornamento: 08/08/11 2 Istruzione 1 in linguaggio L1 Istruzione 2 in linguaggio L1 Istruzione n in linguaggio L1 Interpretazione Interpretazione Interpretazione Istruzione 1 in linguaggio L0 Istruzione 2 in linguaggio L0 Istruzione n in linguaggio L0 Esecuzione Esecuzione Esecuzione Il processo di interpretazione è svolto da un modulo software chiamato interprete. Esso accetta in input un programma sorgente e, invece di tradurlo completamente, ne analizza ogni singola istruzione, la trasforma in una sequenza di istruzioni in codice macchina e la esegue immediatamente. Eventuali errori formali vengono rilevati e segnalati solo quando l’istruzione errata viene tradotta e causano l’interruzione dell’esecuzione. L’esecuzione di un programma interpretato è in genere più lunga di quella dello stesso programma compilato. La prima osservazione che possiamo fare è che, mentre un programma compilato può essere eseguito immediatamente, senza un’ulteriore fase di traduzione, un programma interpretato non può essere eseguito immediatamente ma deve sempre essere preventivamente tradotto istruzione per istruzione. Inoltre, dato che le fasi di traduzione ed esecuzione in un programma interpretato, sono mescolate tra loro risulta evidente che, se un programma prevede l’esecuzione di una stessa istruzione per più volte (ad esempio in un ciclo), questa dovrà essere ogni volta tradotta, allungando così sensibilmente il tempo di esecuzione dell’intero programma. Esempio La differenza tra il processo di traduzione e quello di interpretazione è la stessa che si osserva fra la traduzione di un documento da una lingua ad un’altra e la traduzione simultanea dalla lingua di un probabile relatore alla lingua dei suoi interlocutori. Nel primo caso il traduttore produce un altro documento; nel secondo caso l’interprete non scrive alcunché ma si limita a tradurre le frasi, una alla volta. Macchine virtuali Per rendere pratico il processo di traduzione è necessario che L0 ed L1 non differiscano troppo; questo però fa si che anche L1 non sia molto semplice da usare per l’utente. In quest’ottica, possiamo costruire altri linguaggi (L2, L3, L4, ecc.) che rappresentino un’ulteriore astrazione rispetto al linguaggio del livello sottostante e che siano, di volta in volta, più orientati all’utente. La creazione di linguaggi più facili da usare può continuare fino al raggiungimento di un linguaggio ideale. Autore: Cinzia Bocchi Ultimo aggiornamento: 08/08/11 3 Ogni linguaggio definisce una macchina virtuale, intesa come quella macchina che può eseguire tutti i programmi scritti in quel linguaggio e viceversa. Ogni linguaggio Li usa il precedente, Li-1, come base. Possiamo allora pensare ad un elaboratore come ad una macchina dotata di n livelli, uno sopra l’altro, a ciascuno dei quali corrisponde un linguaggio e una macchina virtuale. I programmi scritti in un linguaggio diverso dal linguaggio macchina, dovranno subire o una serie di traduzioni, fino ad arrivare al linguaggio L0, oppure dovranno essere interpretati da un programma interprete, collocato al livello immediatamente inferiore, fino a giungere ad un interprete di livello L0. Ad ogni livello non ci si deve preoccupare dei livelli sottostanti, ma si possono offrire funzionalità al livello sovrastante. Livello n Macchina virtuale Mn ……. Livello 1 Macchina virtuale M1 Livello 0 Macchina virtuale M0 CPU Il linguaggio del livello n è detto linguaggio di programmazione ad alto livello. Java è un linguaggio di programmazione ad alto livello. Evoluzione dei linguaggi ad alto livello I moderni linguaggi di programmazione sono di tipo evoluto nel senso che utilizzano termini simili al linguaggio naturale, facilitando così il lavoro del programmatore. I linguaggi di programmazione possono essere orientati a specifiche applicazioni o classi di problemi, oppure possono essere adatti per risolvere qualsiasi problema (linguaggi general purpose). Segue una breve e non esaustiva panoramica dei linguaggi ad alto livello. FORTRAN (1956): FORmula TRANslation, nato per applicazioni tecnicoscientifiche e calcolo numerico. COBOL (1960): Common Business Oriented Language, per applicazioni di tipo commerciale e gestionale. ALGOL ( a partire dagli anni ’60): ALGOrithmic Language, per applicazioni scientifiche. LISP ( a partire dagli anni ’60): per applicazioni nel campo dell’intelligenza artificiale. BASIC (1964): Beginners All-purpose Symbolic Instruction Code, linguaggio general purpose; si è sviluppato contemporaneamente al diffondersi dei piccoli sistemi di elaborazione interattivi. RPG (1966): Report Program Generator, per applicazioni di tipo commerciale e in particolare per la preparazione di prospetti. PASCAL (1971): general purpose, orientato ai principi della programmazione strutturata e della tecnica di analisi top-down. Autore: Cinzia Bocchi Ultimo aggiornamento: 08/08/11 4 C (1974): linguaggio general purpose, utilizzato anche nell’ambito dello sviluppo di sistemi operativi e del software di base; si è diffuso insieme al sistema operativo UNIX. PROLOG (1972): PROgramming in LOGic, utilizzato nel campo dell’intelligenza artificiale, sfrutta le relazioni logiche tra i dati. C++ (1979): linguaggio general purpose, a oggetti. JAVA (1995): general purpose a oggetti, utilizzato inoltre per applicazioni che possono funzionare in modo interattivo sulla rete Internet. C# (2001): general purpose a oggetti, sviluppato da Microsoft all’interno del progetto .NET. I vantaggi derivanti dall’uso di un linguaggio ad alto livello sono: • facilità d’uso e di apprendimento, • indipendenza dalla macchina che facilità la portabilità dei programmi, • facilità di ricerca e di eliminazione degli errori, • aumento della leggibilità del programma e, quindi, alto livello documentazione dello stesso. di In generale, quindi, l’uso dei linguaggi di programmazione ad alto livello ha consentito un significativo miglioramento di produttività nella realizzazione del software, una diminuzione dei costi e un’esplosione dell’informatica in tutti i settori. L’approccio Java Java è un linguaggio compilato e interpretato. Il codice sorgente, contenuto in un file con estensione .java, per poter essere eseguito deve subire due trasformazioni: una compilazione, una interpretazione. La compilazione è a carico del compilatore javac, che traduce il codice sorgente in un codice intermedio detto bytecode. Il codice intermedio prodotto, che viene collocato in un file con lo stesso nome del file .java, ma estensione .class, è indipendente dalla piattaforma. In altre parole, il bytecode è sempre lo stesso su ogni macchina. Il bytecode è quindi un codice oggetto le cui istruzioni sono scritte in uno speciale linguaggio che deve essere successivamente interpretato da una macchina virtuale, denominata Java Virtual Machine (JVM). La JVM provvede poi a tradurre ogni istruzione del bytecode nel codice macchina opportuno e ad eseguirla. Pertanto esistono diverse JVM, dipendenti dalla piattaforma utilizzata. Codice sorgente f ile .jav a By tecode f ile .class IN OUT JVM Compilatore javac Interpretazione Autore: Cinzia Bocchi Ultimo aggiornamento: 08/08/11 5 Questo doppio passaggio di traduzione è una delle caratteristiche che ha contribuito al successo di Java, garantendo l’indipendenza del linguaggio stesso dalla piattaforma. La JVM è inclusa in tutti i principali browser e nel JDK. Se non si è interessati a sviluppare codice Java e quindi non si possiede il JDK, è possibile installare solo il Java Runtime Environment (JRE). JDK e JRE sono scaricabili gratuitamente all’url http://www.oracle.com/technetwork/java/javase/downloads/index.html _______________________________________________________________________ Quest'opera è stata rilasciata con licenza Creative Commons Attribution-ShareAlike 3.0 Unported. Per leggere una copia della licenza visita il sito web http://creativecommons.org/licenses/by-sa/3.0/ o spedisci una lettera a Creative Commons, 171 Second Street, Suite 300, San Francisco, California, 94105, USA. Autore: Cinzia Bocchi Ultimo aggiornamento: 08/08/11 6