A che serve convertire il codice sorgente in bytecode Java?


37

Se uno ha bisogno di JVM diverse per architetture diverse, non riesco a capire quale sia la logica dietro l'introduzione di questo concetto. In altre lingue abbiamo bisogno di compilatori diversi per macchine diverse, ma in Java abbiamo bisogno di JVM diverse, quindi qual è la logica dietro l'introduzione del concetto di JVM o questo passaggio extra ??



12
@gnat: in realtà, non è un duplicato. Questo è "codice sorgente vs byte", ovvero solo la prima trasformazione. In termini linguistici, si tratta di Javascript rispetto a Java; il tuo collegamento sarebbe C ++ contro Java.
Salterio

2
Preferiresti scrivere un semplice interprete bytecode per quei 50 modelli di appliance a cui stai aggiungendo la codifica digitale per l'aggiornamento o 50 compilatori per 50 hardware diversi. Java è stato originariamente sviluppato per elettrodomestici e macchinari. Quello era il suo punto forte. Tienilo a mente quando leggi queste risposte poiché java non ha un vero vantaggio al giorno d'oggi (a causa dell'inefficienza del processo di interpretazione). È solo un modello che continuiamo a utilizzare.
The Great Duck

1
Lei sembra non capire che cosa una macchina virtuale è . È una macchina. Potrebbe essere implementato in hardware con compilatori di codice nativo (e nel caso della JVM lo è stato). La parte 'virtuale' è ciò che è importante qui: essenzialmente stai emulando quell'architettura sopra un'altra. Supponiamo di aver scritto un emulatore 8088 per l'esecuzione su x86. Non eseguirai il porting del vecchio codice 8088 su x86, lo eseguirai solo sulla piattaforma emulata. La JVM è una macchina che prendi di mira come qualsiasi altra, con la differenza che gira su altre piattaforme.
Jared Smith,

7
@TheGreatDuck Interpretazione del processo? La maggior parte delle JVM oggigiorno esegue compilazioni just-in-time su codice macchina. Per non parlare del fatto che "interpretazione" è un termine piuttosto ampio al giorno d'oggi. La CPU stessa "interpreta" il codice x86 nel proprio microcodice interno e viene utilizzata per migliorare l'efficienza. Le più recenti CPU Intel sono estremamente adatte anche agli interpreti in generale (anche se ovviamente troverai benchmark per dimostrare ciò che vuoi dimostrare).
Luaan,

Risposte:


79

La logica è che il bytecode JVM è molto più semplice del codice sorgente Java.

Si può pensare che i compilatori, a livello altamente astratto, abbiano tre parti fondamentali: analisi, analisi semantica e generazione di codice.

L'analisi consiste nel leggere il codice e trasformarlo in una rappresentazione ad albero all'interno della memoria del compilatore. L'analisi semantica è la parte in cui analizza questo albero, capisce cosa significa e semplifica tutti i costrutti di alto livello fino a quelli di livello inferiore. E la generazione del codice prende l'albero semplificato e lo scrive in un output piatto.

Con un file bytecode, la fase di analisi è notevolmente semplificata, poiché è scritta nello stesso formato di flusso a byte piatto utilizzato da JIT, anziché in un linguaggio di origine ricorsivo (strutturato ad albero). Inoltre, gran parte del pesante sollevamento dell'analisi semantica è già stata eseguita dal compilatore Java (o altro linguaggio). Quindi tutto ciò che deve fare è leggere il codice in streaming, eseguire l'analisi minima e l'analisi semantica minima, quindi eseguire la generazione del codice.

Ciò rende il compito che JIT deve svolgere molto più semplice, e quindi molto più veloce da eseguire, preservando comunque i metadati di alto livello e le informazioni semantiche che consentono di scrivere teoricamente codice multipiattaforma a sorgente singolo.


7
Alcuni degli altri primi tentativi di distribuzione di applet, come SafeTCL, hanno effettivamente distribuito il codice sorgente. L'uso di Java di un bytecode semplice e ben specificato rende la verifica del programma molto più tracciabile, e questo è stato il problema difficile che veniva risolto. Bytecode come p-code erano già noti come parte della soluzione al problema della portabilità (e all'epoca probabilmente ANDF era in fase di sviluppo).
Toby Speight,

9
Precisamente. I tempi di avvio di Java sono già un po 'un problema a causa del bytecode -> passaggio del codice macchina. Esegui javac sul tuo progetto (non banale), quindi immagina di fare l'intero codice Java -> macchina ad ogni avvio.
Paul Draper,

24
Ha un altro enorme vantaggio: se un giorno vogliamo tutti passare a un nuovo ipotetico linguaggio - chiamiamolo "Scala" - dobbiamo solo scrivere un compilatore Scala -> bytecode, anziché dozzine di Scala -> codice macchina compilatori. Come bonus, otteniamo gratuitamente tutte le ottimizzazioni specifiche della piattaforma di JVM.
BlueRaja - Danny Pflughoeft,

8
Alcune cose non sono ancora possibili nel codice byte JVM, come l'ottimizzazione delle chiamate di coda. Ricordo che questo compromette notevolmente un linguaggio funzionale che viene compilato in JVM.
JDługosz,

8
@JDługosz giusto: JVM purtroppo impone alcune restrizioni / idiomi di progettazione che, sebbene possano essere perfettamente naturali se provieni da un linguaggio imperativo, possono diventare un ostacolo artificiale se vuoi scrivere un compilatore per un linguaggio che funziona fondamentalmente diverso. Ritengo quindi che LLVM sia un obiettivo migliore, per quanto riguarda il riutilizzo del lavoro in una lingua futura - ha anche delle limitazioni, ma esse corrispondono più o meno alle limitazioni che i processori attuali (e probabilmente in futuro) avranno comunque.
leftaroundabout

27

Le rappresentazioni intermedie di vario tipo sono sempre più comuni nella progettazione di compilatori / runtime, per alcuni motivi.

Nel caso di Java, il primo motivo era probabilmente la portabilità : Java era inizialmente commercializzato come "Scrivi una volta, corri ovunque". Sebbene sia possibile raggiungere questo obiettivo distribuendo il codice sorgente e utilizzando diversi compilatori per indirizzare piattaforme diverse, ci sono alcuni aspetti negativi:

  • i compilatori sono strumenti complessi che devono comprendere tutte le sintassi utili del linguaggio; il bytecode può essere un linguaggio più semplice, poiché è più vicino al codice eseguibile dalla macchina che alla fonte leggibile dall'uomo; questo significa:
    • la compilazione potrebbe essere lenta rispetto all'esecuzione del bytecode
    • compilatori rivolti a piattaforme diverse possono finire per produrre comportamenti diversi o non tenere il passo con i cambiamenti di lingua
    • produrre un compilatore per una nuova piattaforma è molto più difficile che produrre una VM (o un compilatore bytecode-nativo) per quella piattaforma
  • la distribuzione del codice sorgente non è sempre auspicabile; bytecode offre una certa protezione contro il reverse engineering (anche se è ancora abbastanza facile decompilare se non deliberatamente offuscato)

Altri vantaggi di una rappresentazione intermedia includono:

  • ottimizzazione , in cui i modelli possono essere individuati nel bytecode e compilati in equivalenti più veloci, o addirittura ottimizzati per casi speciali durante l'esecuzione del programma (utilizzando un compilatore "JIT" o "Just In Time")
  • interoperabilità tra più lingue nella stessa macchina virtuale; questo è diventato popolare con JVM (es. Scala), ed è lo scopo esplicito del framework .net

1
Java era anche orientato ai sistemi integrati. In tali sistemi, l'hardware aveva diversi vincoli di memoria e CPU.
Laiv

I complier possono essere sviluppati in modo da compilare prima il codice sorgente Java in codice byte e quindi compilare il codice byte in codice macchina? Eliminerebbe la maggior parte degli aspetti negativi che hai citato?
Sher10ck

@ Sher10ck Sì, è perfettamente possibile che AFAIK scriva un compilatore che converte staticamente il bytecode JVM in istruzioni macchina per una particolare architettura. Avrebbe senso solo se migliorasse le prestazioni abbastanza da superare lo sforzo extra per il distributore o il tempo extra per il primo utilizzo per l'utente. Un sistema incorporato a bassa potenza potrebbe trarne vantaggio; un PC moderno che scarica e esegue molti programmi diversi sarebbe probabilmente meglio con un JIT ben sintonizzato. Penso che Android vada da qualche parte in questa direzione, ma non conosco i dettagli.
IMSoP

8

Sembra che ti stia chiedendo perché non ci limitiamo a distribuire il codice sorgente. Consentitemi di invertire questa domanda: perché non distribuiamo semplicemente il codice macchina?

Chiaramente la risposta qui è che Java, di progettazione, non presume di sapere quale sia la macchina in cui verrà eseguito il codice; potrebbe essere un desktop, un supercomputer, un telefono o qualsiasi cosa tra e oltre. Java lascia spazio al compilatore JVM locale per fare le sue cose. Oltre ad aumentare la portabilità del tuo codice, questo ha il piacevole vantaggio di consentire al compilatore di fare cose come sfruttare le ottimizzazioni specifiche della macchina, se esistono, o produrre comunque codice funzionante se non lo fanno. Cose come le istruzioni SSE o l'accelerazione hardware possono essere utilizzate solo sui computer che le supportano.

Visto in questa luce, il ragionamento per usare il codice byte sul codice sorgente grezzo è più chiaro. Avvicinarsi il più possibile al linguaggio macchina grezzo ci consente di realizzare o realizzare parzialmente alcuni dei vantaggi del codice macchina, come:

  • Tempi di avvio più rapidi, poiché parte della compilazione e dell'analisi sono già state eseguite.
  • Sicurezza, poiché il formato del codice byte ha un meccanismo incorporato per la firma dei file di distribuzione (il sorgente potrebbe farlo per convenzione, ma il meccanismo per farlo non è incorporato come nel codice byte).

Nota che non menziono un'esecuzione più rapida. Sia il codice sorgente che il codice byte sono o possono (in teoria) essere completamente compilati nello stesso codice macchina per l'esecuzione effettiva.

Inoltre, il codice byte consente alcuni miglioramenti rispetto al codice macchina. Naturalmente ci sono l'indipendenza della piattaforma e le ottimizzazioni specifiche dell'hardware che ho citato in precedenza, ma ci sono anche cose come la manutenzione del compilatore JVM per produrre nuovi percorsi di esecuzione dal vecchio codice. Questo può essere per correggere problemi di sicurezza, o se vengono scoperte nuove ottimizzazioni, o per trarre vantaggio dalle nuove istruzioni hardware. In pratica è raro vedere grandi cambiamenti in questo modo, perché può esporre bug, ma è possibile, ed è qualcosa che accade in piccoli modi tutto il tempo.


8

Sembra che ci siano almeno due diverse possibili domande qui. Uno riguarda davvero i compilatori in generale, con Java praticamente solo un esempio del genere. L'altro è più specifico per Java dei codici byte specifici che utilizza.

Compilatori in generale

Consideriamo innanzitutto la domanda generale: perché un compilatore dovrebbe utilizzare una rappresentazione intermedia nel processo di compilazione del codice sorgente per eseguire un determinato processore?

Riduzione della complessità

Una risposta è abbastanza semplice: converte un problema O (N * M) in un problema O (N + M).

Se ci vengono dati N lingue di origine e target M e ogni compilatore è completamente indipendente, allora abbiamo bisogno di compilatori N * M per tradurre tutte quelle lingue di origine in tutti quei target (dove un "target" è qualcosa come una combinazione di un processore e sistema operativo).

Se, tuttavia, tutti quei compilatori concordano su una rappresentazione intermedia comune, allora possiamo avere N front-end del compilatore che traducono le lingue di origine nella rappresentazione intermedia, e back-end del compilatore M che traducono la rappresentazione intermedia in qualcosa di adatto per un obiettivo specifico.

Segmentazione del problema

Meglio ancora, separa il problema in due domini più o meno esclusivi. Le persone che conoscono / si preoccupano della progettazione del linguaggio, dell'analisi e cose del genere possono concentrarsi sui front-end del compilatore, mentre le persone che conoscono i set di istruzioni, la progettazione del processore e cose del genere possono concentrarsi sul back-end.

Quindi, ad esempio, dato qualcosa come LLVM, abbiamo molti front-end per varie lingue diverse. Abbiamo anche back-end per molti processori diversi. Un ragazzo di lingua può scrivere un nuovo front-end per la sua lingua e supportare rapidamente molti obiettivi. Un ragazzo del processore può scrivere un nuovo back-end per il suo target senza occuparsi di design del linguaggio, analisi, ecc.

Separare i compilatori in un front-end e un back-end, con una rappresentazione intermedia per comunicare tra i due non è originale con Java. È stata una pratica abbastanza comune per molto tempo (dal momento che molto prima che arrivasse Java, comunque).

Modelli di distribuzione

Nella misura in cui Java ha aggiunto qualcosa di nuovo a questo proposito, era nel modello di distribuzione. In particolare, anche se i compilatori sono stati separati internamente in pezzi front-end e back-end per lungo tempo, in genere sono stati distribuiti come un singolo prodotto. Ad esempio, se hai acquistato un compilatore Microsoft C, internamente aveva un "C1" e un "C2", che erano rispettivamente il front-end e il back-end - ma quello che hai acquistato era solo "Microsoft C" che includeva entrambi pezzi (con un "driver del compilatore" che ha coordinato le operazioni tra i due). Anche se il compilatore è stato creato in due parti, per un normale sviluppatore che utilizzava il compilatore era solo una cosa che traduceva da codice sorgente a codice oggetto, con nulla di visibile in mezzo.

Java, invece, ha distribuito il front-end nel Java Development Kit e il back-end nella Java Virtual Machine. Ogni utente Java aveva un back-end del compilatore per indirizzare qualunque sistema stesse stava usando. Gli sviluppatori Java hanno distribuito il codice nel formato intermedio, quindi quando un utente lo ha caricato, JVM ha fatto tutto il necessario per eseguirlo sul proprio computer.

precedenti

Si noti che anche questo modello di distribuzione non era del tutto nuovo. Ad esempio, il sistema P UCSD ha funzionato in modo simile: i front-end del compilatore producevano il codice P e ogni copia del sistema P includeva una macchina virtuale che faceva il necessario per eseguire il codice P su quel particolare target 1 .

Codice byte Java

Il codice byte Java è abbastanza simile al codice P. Fondamentalmente sono le istruzioni per una macchina abbastanza semplice. Quella macchina vuole essere un'astrazione delle macchine esistenti, quindi è abbastanza facile da tradurre rapidamente in quasi tutti gli obiettivi specifici. La facilità di traduzione era importante sin dall'inizio perché l'intento originale era quello di interpretare i codici byte, proprio come aveva fatto P-System (e, sì, è esattamente così che funzionavano le prime implementazioni).

Punti di forza

Il codice byte Java è facile da produrre per un front-end del compilatore. Se (ad esempio) hai un albero abbastanza tipico che rappresenta un'espressione, in genere è abbastanza facile attraversare l'albero e generare codice abbastanza direttamente da ciò che trovi in ​​ciascun nodo.

I codici byte Java sono piuttosto compatti - nella maggior parte dei casi, molto più compatti del codice sorgente o del codice macchina per i processori più tipici (e, soprattutto per la maggior parte dei processori RISC, come lo SPARC che Sun ha venduto quando hanno progettato Java). Ciò era particolarmente importante in quel momento, perché uno dei principali intenti di Java era supportare applet - codice incorporato in pagine Web che sarebbero state scaricate prima dell'esecuzione - in un momento in cui la maggior parte delle persone accedeva a noi tramite modem tramite linee telefoniche a circa 28,8 kilobit al secondo (anche se, naturalmente, c'erano ancora alcune persone che usano modem più vecchi e più lenti).

Punti di debolezza

Il principale punto debole dei codici byte Java è che non sono particolarmente espressivi. Sebbene possano esprimere abbastanza bene i concetti presenti in Java, non funzionano altrettanto bene per esprimere concetti che non fanno parte di Java. Allo stesso modo, sebbene sia facile eseguire codici byte sulla maggior parte dei computer, è molto più difficile farlo in un modo che sfrutti appieno ogni particolare computer.

Ad esempio, è abbastanza normale che se si desidera davvero ottimizzare i codici byte Java, fondamentalmente si fa un po 'di reverse engineering per tradurli all'indietro da una rappresentazione simile a un codice macchina e trasformarli in istruzioni SSA (o qualcosa di simile) 2 . Quindi manipoli le istruzioni SSA per eseguire l'ottimizzazione, quindi traduci da lì in qualcosa che si rivolge all'architettura a cui tieni davvero. Anche con questo processo piuttosto complesso, tuttavia, alcuni concetti che sono estranei a Java sono sufficientemente difficili da esprimere che è difficile tradurre da alcune lingue di origine in codice macchina che viene eseguito (anche vicino) in modo ottimale sulla maggior parte delle macchine tipiche.

Sommario

Se ti stai chiedendo perché utilizzare le rappresentazioni intermedie in generale, due fattori principali sono:

  1. Ridurre un problema O (N * M) a un problema O (N + M) e
  2. Rompere il problema in pezzi più gestibili.

Se stai chiedendo quali siano i dettagli dei codici byte Java e perché abbiano scelto questa particolare rappresentazione invece di qualche altra, allora direi che la risposta in gran parte ritorna al loro intento originale e ai limiti del web in quel momento , portando alle seguenti priorità:

  1. Rappresentazione compatta.
  2. Facile e veloce da decodificare ed eseguire.
  3. Veloce e facile da implementare sulle macchine più comuni.

Essere in grado di rappresentare molte lingue o eseguire in modo ottimale su una vasta gamma di obiettivi erano priorità molto più basse (se fossero considerate priorità).


  1. Quindi perché il sistema P è quasi completamente dimenticato? Principalmente una situazione di prezzi. Il sistema P è stato venduto abbastanza decentemente su Apple II, Commodore SuperPet, ecc. Quando è uscito il PC IBM, il sistema P era un sistema operativo supportato, ma MS-DOS costava meno (dal punto di vista della maggior parte delle persone, era essenzialmente gettato gratuitamente) e rapidamente ha avuto più programmi disponibili, poiché è quello per cui Microsoft e IBM (tra gli altri) hanno scritto.
  2. Ad esempio, ecco come funziona la fuliggine .

Abbastanza vicino con le applet web: l'intento originale era distribuire il codice agli apparecchi (set top box ...), allo stesso modo in cui RPC distribuisce le chiamate di funzione e CORBA distribuisce gli oggetti.
ninjalj,

2
Questa è un'ottima risposta e una buona visione di come diverse rappresentazioni intermedie fanno diversi compromessi. :)
IMSoP

@ninjalj: Quello era davvero Oak. Quando si era trasformato in Java, credo che le idee del set top box (e simili) fossero state accantonate (anche se sono il primo ad ammettere che c'è una buona argomentazione da sostenere sul fatto che Oak e Java siano la stessa cosa).
Jerry Coffin,

@TobySpeight: Sì, probabilmente l'espressione si adatta meglio lì. Grazie.
Jerry Coffin,

0

Oltre ai vantaggi evidenziati da altre persone, il bytecode è molto più piccolo, quindi è più facile da distribuire e aggiornare e occupa meno spazio nell'ambiente di destinazione. Ciò è particolarmente importante in ambienti fortemente limitati dallo spazio.

Inoltre, semplifica la protezione del codice sorgente protetto da copyright.


2
Il bytecode Java (e .NET) è così facile da tornare a una fonte ragionevolmente leggibile che ci sono prodotti per manipolare i nomi e talvolta altre informazioni per renderlo più difficile, qualcosa che spesso viene fatto anche su JavaScript per renderlo più piccolo, dato che siamo solo ora magari impostando un bytecode per i browser Web.
LnxPrgr3,

0

La sensazione è che la compilazione dal codice byte al codice macchina sia più veloce dell'interpretazione del codice originale al codice macchina appena in tempo. Ma abbiamo bisogno di interpretazioni per rendere la nostra applicazione multipiattaforma, perché vogliamo usare il nostro codice originale su ogni piattaforma senza modifiche e senza preparazioni (compilazioni). Quindi prima javac compila il nostro codice sorgente in codice byte, quindi possiamo eseguire questo codice byte ovunque e sarà interpretato da Java Virtual Machine per codice macchina più rapidamente. La risposta: fa risparmiare tempo.


0

In origine, JVM era un puro interprete . E ottieni l'interprete con le migliori prestazioni se la lingua che stai interpretando è il più semplice possibile. Questo era l'obiettivo del codice byte: fornire un input interpretabile in modo efficiente all'ambiente run-time. Questa singola decisione ha avvicinato Java a un linguaggio compilato piuttosto che a un linguaggio interpretato, come giudicato dalle sue prestazioni.

Solo più tardi, quando è apparso evidente che le interpretazioni delle JVM interpretative continuavano a risucchiare, le persone hanno investito gli sforzi per creare compilatori just-in-time ben funzionanti. Ciò ha in qualche modo colmato il divario con linguaggi più veloci come C e C ++. (Tuttavia, permangono alcuni problemi inerenti alla velocità intrinseca di Java, quindi probabilmente non avrai mai un ambiente Java che funzioni così come codice C ben scritto.)

Naturalmente, con le tecniche just-in-time compilazione a portata di mano, siamo riusciti a tornare al codice sorgente in realtà la distribuzione, e just-in-time compilarlo in codice macchina. Tuttavia, ciò ridurrebbe notevolmente le prestazioni di avvio fino a quando tutte le parti pertinenti del codice non saranno compilate. Il codice byte è ancora un aiuto significativo qui perché è molto più semplice da analizzare rispetto al codice Java equivalente.


Il downvoter potrebbe spiegare perché ?
cmaster

-5

Il codice sorgente del testo è una struttura che intende essere facile da leggere e modificare da un essere umano.

Il codice byte è una struttura che intende essere facile da leggere ed eseguire da una macchina.

Poiché tutto ciò che JVM fa con il codice viene letto ed eseguito, il codice byte si adatta meglio al consumo da parte di JVM.

Ho notato che non ci sono ancora esempi. Esempi di pseudo sciocchi:

//Source code
i += 1 + 5 * 2 + x;

// Byte code
i += 11, i += x
____

//Source code
i = sin(1);

// Byte code
i = 0.8414709848
_____

//Source code
i = sin(x)^2+cos(x)^2;

// Byte code (actually that one isn't true)
i = 1

Ovviamente il codice byte non riguarda solo le ottimizzazioni. Gran parte di esso riguarda la possibilità di eseguire il codice senza preoccuparsi di regole complicate, come verificare se la classe contiene un membro chiamato "pippo" da qualche parte più in basso nel file quando un metodo fa riferimento a "pippo".


2
Questi "esempi" di codici byte sono leggibili dall'uomo. Questo non è affatto un codice byte. Questo è fuorviante e inoltre non affronta la domanda posta.
Wildcard il

@Wildcard Potresti aver perso questo forum, letto da umani. Ecco perché ho messo il contenuto in forma leggibile dall'uomo. Dato che il forum riguarda l'ingegneria del software, chiedere ai lettori di capire il concetto di semplice astrazione non è chiedere molto.
Peter,

La forma leggibile dall'uomo è il codice sorgente, non il codice byte. Stai illustrando il codice sorgente con espressioni pre-calcolate, NON con codice byte. E non mi sono perso che questo è un forum leggibile dall'uomo: sei tu quello che ha criticato gli altri risponditori per non aver incluso esempi di codice byte, non io. Così dite, "Noto non ci sono state ancora esempi," e quindi procedere a dare non -examples che non illustrano bytecode a tutti. E questo ancora non affronta affatto la domanda. Rileggi la domanda.
Wildcard il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.