Qual é a utilidade de converter o código-fonte em bytecode Java?

37

Se alguém precisar de JVMs diferentes para arquiteturas diferentes, não consigo descobrir qual é a lógica por trás da introdução desse conceito. Em outras linguagens, precisamos de compiladores diferentes para máquinas diferentes, mas em Java exigimos JVMs diferentes. Qual é a lógica por trás da introdução do conceito de JVM ou dessa etapa extra?

Pranjal Kumar
fonte
1
Possível duplicado de compilação para bytecode vs código de máquina
mosquito
12
@gnat: Na verdade, isso não é uma duplicata. Este é "código fonte vs byte", ou seja, apenas a primeira transformação. Em termos de linguagem, isso é Javascript versus Java; seu link seria C ++ versus Java.
MSalters
2
Você prefere escrever um interpretador de bytecode simples para os 50 modelos de dispositivos aos quais você está adicionando codificação digital para atualização ou 50 compiladores para 50 hardware diferentes. O Java foi desenvolvido originalmente para dispositivos e máquinas. Esse era o seu ponto forte. Lembre-se disso ao ler essas respostas, pois o java não tem nenhuma vantagem real atualmente (devido à ineficiência do processo de interpretação). É apenas um modelo que continuamos a usar.
The Great Duck
1
Você parece não entender o que uma máquina virtual é . É uma máquina. Pode ser implementado em hardware com compiladores de código nativo (e no caso da JVM). A parte 'virtual' é o que é importante aqui: você essencialmente imita essa arquitetura em cima de outra. Digamos que eu escrevi um emulador 8088 para rodar no x86. Você não vai portar o antigo código 8088 para x86, apenas executá-lo na plataforma emulada. A JVM é uma máquina que você segmenta como qualquer outra, a diferença é que ela é executada sobre as outras plataformas.
Jared Smith
7
@TheGreatDuck Processo de interpretação? Atualmente, a maioria das JVMs faz compilação just-in-time para o código da máquina. Sem mencionar que "interpretação" é um termo bastante amplo hoje em dia. A própria CPU apenas "interpreta" o código x86 em seu próprio microcódigo interno e é usada para melhorar a eficiência. As CPUs Intel mais recentes também são extremamente adequadas para intérpretes em geral (embora você obviamente encontre referências para provar o que quer provar).
Luaan

Respostas:

79

A lógica é que o bytecode da JVM é muito mais simples que o código-fonte Java.

Os compiladores podem ser considerados, em um nível altamente abstrato, como tendo três partes básicas: análise, análise semântica e geração de código.

A análise consiste em ler o código e transformá-lo em uma representação em árvore na memória do compilador. A análise semântica é a parte em que analisa essa árvore, descobre o que significa e simplifica todas as construções de alto nível até as de nível inferior. E a geração de código pega a árvore simplificada e a grava em uma saída plana.

Com um arquivo de bytecode, a fase de análise é bastante simplificada, pois é escrita no mesmo formato de fluxo de bytes simples que o JIT usa, em vez de um idioma de origem recursivo (estruturado em árvore). Além disso, grande parte do trabalho pesado da análise semântica já foi realizada pelo compilador Java (ou outra linguagem). Portanto, tudo o que precisa fazer é ler o código por fluxo, fazer análise mínima e análise semântica mínima e executar a geração de código.

Isso torna a tarefa que o JIT deve executar muito mais simples e, portanto, muito mais rápida de executar, preservando ainda os metadados de alto nível e as informações semânticas que possibilitam escrever teoricamente código de plataforma única e fonte única.

Mason Wheeler
fonte
7
Algumas das outras tentativas iniciais de distribuição de applets, como o SafeTCL, realmente distribuíram o código-fonte. O uso de Java de um bytecode simples e bem especificado torna a verificação do programa muito mais tratável, e esse era o difícil problema que estava sendo resolvido. Bytecodes como código-p já eram conhecidos como parte da solução do problema de portabilidade (e o ANDF provavelmente estava em desenvolvimento na época).
perfil completo de Toby Speight
9
Precisamente. Os tempos de inicialização do Java já são um problema devido à etapa de código de máquina bytecode ->. Execute o javac em seu projeto (não trivial) e, em seguida, imagine fazer todo o código Java -> da máquina em todas as partidas.
Paul Draper
24
Ele tem outro grande benefício: se algum dia todos quisermos mudar para uma nova linguagem hipotética - vamos chamá-la de "Scala" - precisamos escrever apenas um compilador Scala -> bytecode, em vez de dezenas de Scala -> código de máquina compiladores. Como bônus, obtemos todas as otimizações específicas da plataforma da JVM gratuitamente.
BlueRaja # Danny Pflughoeft
8
Algumas coisas ainda não são possíveis no código de bytes da JVM, como otimização de chamada de cauda. Lembro que isso compromete bastante uma linguagem funcional que é compilada na JVM.
JDługosz 04/04
8
@ JDługosz certo: a JVM infelizmente impõe algumas restrições / idiomas de design que, embora possam ser perfeitamente naturais se você vier de uma linguagem imperativa, pode se tornar uma obstrução artificial se você quiser escrever um compilador para uma linguagem que funcione fundamentalmente diferente. Assim, considero o LLVM um alvo melhor, no que diz respeito à reutilização do trabalho no idioma futuro - ele também tem limitações, mas elas correspondem mais ou menos às limitações que os processadores atuais (e provavelmente em algum momento no futuro) possuem.
precisa saber é o seguinte
27

Representações intermediárias de vários tipos são cada vez mais comuns no design do compilador / tempo de execução, por alguns motivos.

No caso de Java, o motivo número um inicialmente foi provavelmente a portabilidade : o Java foi amplamente comercializado inicialmente como "Write Once, Run Anywhere". Embora você possa conseguir isso distribuindo o código-fonte e usando compiladores diferentes para atingir plataformas diferentes, isso tem algumas desvantagens:

  • compiladores são ferramentas complexas que precisam entender todas as sintaxes de conveniência da linguagem; o bytecode pode ser uma linguagem mais simples, pois está mais próximo do código executável por máquina do que a fonte legível por humanos; isso significa:
    • a compilação pode ser lenta em comparação com a execução do bytecode
    • compiladores direcionados a plataformas diferentes podem acabar produzindo comportamentos diferentes ou não acompanhar as mudanças de idioma
    • produzir um compilador para uma nova plataforma é muito mais difícil do que produzir uma VM (ou compilador bytecode-to-native) para essa plataforma
  • distribuir código fonte nem sempre é desejável; O bytecode oferece alguma proteção contra a engenharia reversa (embora ainda seja bastante fácil descompilar, a menos que oculte deliberadamente)

Outras vantagens de uma representação intermediária incluem:

  • otimização , onde os padrões podem ser identificados no bytecode e compilados para equivalentes mais rápidos ou até otimizados para casos especiais à medida que o programa é executado (usando um compilador "JIT" ou "Just In Time")
  • interoperabilidade entre vários idiomas na mesma VM; isso se tornou popular na JVM (por exemplo, Scala) e é o objetivo explícito da estrutura .net
IMSoP
fonte
1
Java também foi orientado para sistemas incorporados. Nesses sistemas, o hardware tinha várias restrições de memória e CPU.
LAIV
Os compliers podem ser desenvolvidos de maneira a compilar primeiro o código-fonte Java no código de bytes e depois o código de bytes no código de máquina? Isso eliminaria a maioria das desvantagens que você mencionou?
1010 Sher10ck
@ Sher10ck Sim, é perfeitamente possível que o AFAIK escreva um compilador que converta estaticamente o bytecode da JVM em instruções da máquina para uma arquitetura específica. Mas só faria sentido se melhorasse o desempenho o suficiente para compensar o esforço extra do distribuidor ou o tempo extra para o primeiro uso do usuário. Um sistema embarcado de baixa potência pode se beneficiar; um PC moderno baixando e executando muitos programas diferentes provavelmente seria melhor com um JIT bem ajustado. Acho que o Android vai a algum lugar nessa direção, mas não sei detalhes.
IMSoP
8

Parece que você está se perguntando por que não distribuímos apenas o código-fonte. Deixe-me mudar essa questão: por que não distribuímos código de máquina?

Claramente, a resposta aqui é que Java, por design, não assume que sabe qual é a máquina onde seu código será executado; pode ser um desktop, um supercomputador, um telefone ou qualquer coisa entre e além. Java deixa espaço para o compilador JVM local fazer sua parte. Além de aumentar a portabilidade do seu código, isso tem o benefício de permitir que o compilador faça coisas como tirar proveito das otimizações específicas da máquina, se elas existirem, ou ainda produzir pelo menos código funcional, se não existirem. Coisas como instruções SSE ou aceleração de hardware podem ser usadas apenas nas máquinas que os suportam.

Visto sob essa luz, o raciocínio para usar o código de bytes em vez do código-fonte bruto é mais claro. Ficar o mais próximo possível da linguagem bruta da máquina nos permite perceber ou perceber parcialmente alguns dos benefícios do código da máquina, como:

  • Tempos de inicialização mais rápidos, já que parte da compilação e análise já está concluída.
  • Segurança, uma vez que o formato do código de bytes possui um mecanismo interno para assinar os arquivos de distribuição (a fonte pode fazer isso por convenção, mas o mecanismo para fazer isso não é interno da maneira que é com o código de bytes).

Note que eu não mencionei uma execução mais rápida. O código fonte e o código de bytes são ou podem (em teoria) ser totalmente compilados no mesmo código de máquina para execução real.

Além disso, o código de bytes permite algumas melhorias em relação ao código da máquina. É claro que existem a independência da plataforma e as otimizações específicas de hardware que mencionei anteriormente, mas também existem serviços como o compilador JVM para produzir novos caminhos de execução a partir do código antigo. Isso pode ser para corrigir problemas de segurança ou se novas otimizações forem descobertas ou tirar proveito das novas instruções de hardware. Na prática, é raro ver grandes mudanças dessa maneira, porque pode expor bugs, mas é possível, e é algo que acontece de maneiras pequenas o tempo todo.

Joel Coehoorn
fonte
8

Parece haver pelo menos duas questões possíveis diferentes aqui. Um é realmente sobre compiladores em geral, com Java basicamente apenas um exemplo do gênero. O outro é mais específico para Java, os códigos de bytes específicos que ele usa.

Compiladores em geral

Vamos primeiro considerar a questão geral: por que um compilador usaria uma representação intermediária no processo de compilação do código-fonte para executar em algum processador em particular?

Redução de complexidade

Uma resposta para isso é bastante simples: converte um problema de O (N * M) em um problema de O (N + M).

Se recebermos N idiomas de origem e M destinos, e cada compilador for completamente independente, precisamos de compiladores N * M para traduzir todos esses idiomas de origem para todos esses destinos (onde um "destino" é algo como uma combinação de um processador e SO).

Se, no entanto, todos esses compiladores concordarem com uma representação intermediária comum, poderemos ter N front-ends do compilador que traduzem os idiomas de origem para a representação intermediária e M back-ends do compilador que traduzem a representação intermediária em algo adequado para um destino específico.

Segmentação de Problemas

Melhor ainda, separa o problema em dois domínios mais ou menos exclusivos. Pessoas que conhecem / se preocupam com design de linguagem, análise e coisas assim podem se concentrar nos front-ends do compilador, enquanto pessoas que conhecem conjuntos de instruções, design de processador e coisas assim podem se concentrar no back-end.

Assim, por exemplo, considerando algo como LLVM, temos muitos front-ends para vários idiomas diferentes. Também temos back-ends para vários processadores diferentes. Um especialista em idiomas pode escrever um novo front-end para o seu idioma e suportar rapidamente muitos destinos. Um cara de processador pode escrever um novo back-end para seu destino sem lidar com o design, a análise de idiomas, etc.

Separar compiladores em um front end e back end, com uma representação intermediária para se comunicar entre os dois, não é original no Java. É uma prática bastante comum há muito tempo (desde muito antes de o Java aparecer).

Modelos de distribuição

Na medida em que o Java adicionou algo novo a esse respeito, estava no modelo de distribuição. Em particular, mesmo que os compiladores tenham sido separados internamente por partes de front-end e back-end por um longo tempo, eles geralmente eram distribuídos como um único produto. Por exemplo, se você comprou um compilador Microsoft C, internamente ele tinha um "C1" e um "C2", que eram o front-end e o back-end respectivamente - mas o que você comprou foi apenas "Microsoft C" que incluía ambos peças (com um "driver de compilador" que coordenava as operações entre os dois). Mesmo que o compilador tenha sido construído em duas partes, para um desenvolvedor normal usando o compilador, foi apenas uma coisa que foi traduzida do código-fonte para o objeto, sem nada visível no meio.

Java, em vez disso, distribuiu o front-end no Java Development Kit e o back-end na Java Virtual Machine. Todo usuário de Java tinha um back-end do compilador para segmentar qualquer sistema que estivesse usando. Os desenvolvedores de Java distribuíram o código no formato intermediário; portanto, quando um usuário o carregava, a JVM fazia o necessário para executá-lo em sua máquina específica.

Precedentes

Observe que esse modelo de distribuição também não era totalmente novo. Apenas por exemplo, o sistema P UCSD funcionou de maneira semelhante: os front-ends do compilador produziram código P, e cada cópia do sistema P incluía uma máquina virtual que fazia o necessário para executar o código P nesse destino específico 1 .

Código de bytes Java

O código de bytes Java é bastante semelhante ao código P. É basicamente instruções para uma máquina bastante simples. Essa máquina pretende ser uma abstração das máquinas existentes, por isso é bastante fácil traduzir rapidamente para praticamente qualquer destino específico. A facilidade da tradução foi importante desde o início, porque a intenção original era interpretar os códigos de bytes, da mesma forma que o P-System (e, sim, é exatamente assim que as implementações iniciais funcionavam).

Forças

O código de bytes Java é fácil para um front-end do compilador produzir. Se (por exemplo) você tiver uma árvore bastante típica que representa uma expressão, é muito fácil atravessá-la e gerar código bastante diretamente a partir do que você encontra em cada nó.

Os códigos de bytes Java são bastante compactos - na maioria dos casos, muito mais compactos do que o código-fonte ou o código de máquina dos processadores mais comuns (e, principalmente, para a maioria dos processadores RISC, como o SPARC que a Sun vendeu quando projetou o Java). Isso foi particularmente importante na época, porque uma das principais intenções do Java era oferecer suporte a applets - código incorporado em páginas da web que seriam baixadas antes da execução - no momento em que a maioria das pessoas acessava o nós via modems através de linhas telefônicas por volta de 28,8 kilobits por segundo (embora, é claro, ainda houvesse muitas pessoas usando modems mais antigos e lentos).

Fraquezas

A principal fraqueza dos códigos de bytes Java é que eles não são particularmente expressivos. Embora eles possam expressar os conceitos presentes em Java muito bem, eles não funcionam tão bem para expressar conceitos que não fazem parte do Java. Da mesma forma, embora seja fácil executar códigos de bytes na maioria das máquinas, é muito mais difícil fazer isso de uma maneira que tira o máximo proveito de qualquer máquina em particular.

Por exemplo, é bastante rotineiro que, se você realmente deseja otimizar códigos de bytes Java, faça basicamente alguma engenharia reversa para convertê-los para trás de uma representação como código de máquina e transformá-los novamente em instruções SSA (ou algo semelhante) 2 . Em seguida, você manipula as instruções SSA para fazer sua otimização e depois traduz a partir daí para algo que atinja a arquitetura com a qual você realmente gosta. Mesmo com esse processo bastante complexo, no entanto, alguns conceitos estranhos ao Java são suficientemente difíceis de expressar que é difícil traduzir de algumas linguagens de origem em código de máquina que é executado (até próximo) de maneira ideal nas máquinas mais comuns.

Sumário

Se você está perguntando por que usar representações intermediárias em geral, dois fatores principais são:

  1. Reduza um problema de O (N * M) para um problema de O (N + M) e
  2. Divida o problema em pedaços mais gerenciáveis.

Se você está perguntando sobre as especificidades dos códigos de bytes Java e por que eles escolheram essa representação específica em vez de outra, então eu diria que a resposta volta em grande parte à intenção original e às limitações da Web da época , levando às seguintes prioridades:

  1. Representação compacta.
  2. Rápido e fácil de decodificar e executar.
  3. Rápido e fácil de implementar nas máquinas mais comuns.

Ser capaz de representar muitos idiomas ou executar de maneira ideal em uma ampla variedade de alvos eram prioridades muito mais baixas (se eram consideradas prioridades).


  1. Então, por que o sistema P é praticamente esquecido? Principalmente uma situação de preços. O sistema P foi vendido decentemente nos Apple II, Commodore SuperPets etc. Quando o IBM PC foi lançado, o sistema P era um sistema operacional suportado, mas o MS-DOS custou menos (do ponto de vista da maioria das pessoas, foi essencialmente lançado de graça) e rapidamente teve mais programas disponíveis, pois é para isso que a Microsoft e a IBM (entre outros) escreveram.
  2. Por exemplo, é assim que a fuligem funciona.
Jerry Coffin
fonte
Bem perto dos applets da Web: a intenção original era distribuir o código para os appliances (decodificadores ...), da mesma maneira que o RPC distribui chamadas de função e o CORBA distribui objetos.
Njalj 04/04
2
Essa é uma ótima resposta e uma boa visão de como diferentes representações intermediárias fazem diferentes compensações. :)
IMSoP 04/04
@ninjalj: Aquele era realmente Oak. No momento em que ele se transformou em Java, acredito que as idéias do decodificador (e similares) haviam sido arquivadas (embora eu seja a primeira a admitir que há um argumento justo a ser feito de que Oak e Java são a mesma coisa).
Jerry Coffin
@TobySpeight: Sim, a expressão é provavelmente um ajuste melhor lá. Obrigado.
Jerry Coffin
0

Além das vantagens apontadas por outras pessoas, o bytecode é muito menor, portanto é mais fácil distribuir e atualizar e ocupa menos espaço no ambiente de destino. Isso é especialmente importante em ambientes com muita restrição de espaço.

Também facilita a proteção do código fonte protegido por direitos autorais.

EJoshuaS - Restabelecer Monica
fonte
2
O bytecode Java (e .NET) é tão fácil de se transformar em uma fonte razoavelmente legível que existem produtos para alterar nomes e, às vezes, outras informações para tornar isso mais difícil - algo também costuma ser feito no JavaScript para reduzi-lo, já que agora estamos agora talvez definindo um bytecode para navegadores da Web.
# 2
0

O sentido é que a compilação do código de bytes para o código da máquina é mais rápida do que interpretar o código original para o código da máquina na hora certa. Mas precisamos de interpretações para tornar nosso aplicativo multiplataforma, porque queremos usar nosso código original em todas as plataformas, sem alterações e sem preparativos (compilações). Então, primeiro o javac compila nossa fonte em código de bytes, então podemos executá-lo em qualquer lugar e ele será interpretado pela Java Virtual Machine para codificar mais rapidamente. A resposta: economiza tempo.

Sergey Orlov
fonte
0

Originalmente, a JVM era uma intérprete pura . E você obtém o intérprete com melhor desempenho se o idioma que você está interpretando for o mais simples possível. Esse era o objetivo do código de bytes: fornecer uma entrada eficientemente interpretável para o ambiente de tempo de execução. Essa decisão única colocou o Java mais próximo de uma linguagem compilada do que de uma linguagem interpretada, conforme julgado por seu desempenho.

Somente mais tarde, quando ficou claro que o desempenho das JVMs de interpretação ainda era ruim, as pessoas investiram o esforço para criar compiladores just-in-time de bom desempenho. Isso diminuiu um pouco a diferença para linguagens mais rápidas como C e C ++. (No entanto, alguns problemas de velocidade inerentes ao Java permanecem, portanto você provavelmente nunca obterá um ambiente Java que tenha um desempenho tão bom quanto o código C.

Obviamente, com as técnicas de compilação just-in-time disponíveis, poderíamos voltar a distribuir realmente o código-fonte e compilá-lo just-in-time no código de máquina. No entanto, isso diminuiria bastante o desempenho da inicialização até que todas as partes relevantes do código fossem compiladas. O código de bytes ainda é uma ajuda significativa aqui, porque é muito mais simples de analisar do que o código Java equivalente.

cmaster
fonte
O downvoter poderia se importar em explicar o porquê ?
precisa saber é
-5

O código-fonte do texto é uma estrutura que pretende ser fácil de ser lida e modificada por um ser humano.

O código de bytes é uma estrutura que pretende ser fácil de ser lida e executada por uma máquina.

Como tudo o que a JVM faz com o código é lido e executado, o código de bytes é mais adequado para consumo pela JVM.

Percebo que ainda não houve exemplos. Pseudo-exemplos tolos:

//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

Obviamente, o código de bytes não se trata apenas de otimizações. Grande parte disso é sobre a capacidade de executar código sem ter que se preocupar com regras complicadas, como verificar se a classe contém um membro chamado "foo" em algum lugar mais abaixo no arquivo quando um método se refere a "foo".

Pedro
fonte
2
Esses "exemplos" de código de bytes são legíveis por humanos. Isso não é um código de bytes. Isso é enganoso e também não aborda a pergunta.
Wildcard
@Wildcard Você pode ter perdido esse fórum, lido por humanos. É por isso que coloco o conteúdo em formato legível por humanos. Dado que o fórum é sobre engenharia de software, pedir aos leitores que entendam o conceito de abstração simples não é pedir muito.
Peter
A forma legível por humanos é o código fonte, não o código de bytes. Você está ilustrando o código-fonte com expressões pré-calculadas, NÃO código de bytes. E não perdi que este é um fórum legível por humanos: você é quem criticou outros respondentes por não incluir nenhum exemplo de código de bytes, não eu. Então você diz, "eu aviso não houve qualquer exemplos ainda", e depois prosseguir para dar não -exemplos que não ilustram código byte em tudo. E isso ainda não aborda a questão. Releia a pergunta.
Wildcard