:Z #23 = Methodref #2.#22 // Person.m:()I #25 = Fieldref #2.#24 // Person.m4:I #31 = Fieldref #27.#30 // scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; #48 = Methodref #2.#47 // Person.m4$lzycompute:()I // ... private volatile boolean bitmap

Suje Suas Mãos Com Bytecode Scala JVM



A linguagem Scala continuou a ganhar popularidade nos últimos anos, graças à sua excelente combinação de princípios de desenvolvimento de software funcional e orientado a objetos , e sua implementação em cima da comprovada Java Virtual Machine (JVM).

Apesar Escada compila para bytecode Java, é projetado para melhorar muitas das deficiências percebidas da linguagem Java. Oferecendo suporte de programação funcional completo, a sintaxe central do Scala contém muitas estruturas implícitas que devem ser construídas explicitamente por programadores Java, algumas envolvendo considerável complexidade.



A criação de uma linguagem que compila para bytecode Java requer um conhecimento profundo do funcionamento interno da Java Virtual Machine. Para apreciar o que os desenvolvedores do Scala realizaram, é necessário ir nos bastidores e explorar como o código-fonte do Scala é interpretado pelo compilador para produzir bytecode JVM eficiente e eficaz.



Vamos dar uma olhada em como tudo isso é implementado.



Pré-requisitos

Ler este artigo requer algum conhecimento básico do bytecode da Java Virtual Machine. A especificação completa da máquina virtual pode ser obtida em Documentação oficial da Oracle . Ler toda a especificação não é crítica para a compreensão deste artigo, então, para uma rápida introdução ao básico, preparei um pequeno guia no final do artigo.

Clique aqui para ler um curso intensivo sobre noções básicas de JVM.

Um utilitário é necessário para desmontar o bytecode Java para reproduzir os exemplos fornecidos abaixo e para prosseguir com uma investigação adicional. O Java Development Kit fornece seu próprio utilitário de linha de comando, javap, que usaremos aqui. Uma rápida demonstração de como javap trabalhos estão incluídos no guia na parte inferior .



E, claro, uma instalação funcional do compilador Scala é necessária para os leitores que desejam acompanhar os exemplos. Este artigo foi escrito usando Escala 2.11.7 . Diferentes versões do Scala podem produzir bytecode ligeiramente diferente.

Getters e setters padrão

Embora a convenção Java sempre forneça métodos getter e setter para atributos públicos, os programadores Java são obrigados a escrevê-los, apesar do fato de que o padrão para cada um não mudou em décadas. O Scala, em contraste, fornece getters e setters padrão.



Vejamos o seguinte exemplo:

class Person(val name:String) { }

Vamos dar uma olhada dentro da classe Person. Se compilarmos este arquivo com scalac, então executar $ javap -p Person.class nos dá:



Compiled from 'Person.scala' public class Person { private final java.lang.String name; // field public java.lang.String name(); // getter method public Person(java.lang.String); // constructor }

Podemos ver que para cada campo da classe Scala, um campo e seu método getter são gerados. O campo é privado e final, enquanto o método é público.

Se substituirmos val com var no Person fonte e recompilar, então o campo final o modificador é eliminado e o método setter também é adicionado:



Compiled from 'Person.scala' public class Person { private java.lang.String name; // field public java.lang.String name(); // getter method public void name_$eq(java.lang.String); // setter method public Person(java.lang.String); // constructor }

Se houver val ou var é definido dentro do corpo da classe, então o campo privado correspondente e os métodos de acesso são criados e inicializados apropriadamente na criação da instância.

Observe que tal implementação de nível de classe val e var campos significa que se algumas variáveis ​​são usadas no nível da classe para armazenar valores intermediários e nunca são acessadas diretamente pelo programador, a inicialização de cada um desses campos adicionará um ou dois métodos ao espaço da classe. Adicionando um private modificador para tais campos não significa que os acessadores correspondentes serão descartados. Eles apenas se tornarão privados.



Definições de variáveis ​​e funções

Vamos supor que temos um método, m(), e criamos três referências diferentes no estilo Scala para esta função:

class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }

Como cada uma dessas referências a m construído? Quando m ser executado em cada caso? Vamos dar uma olhada no bytecode resultante. A saída a seguir mostra os resultados de javap -v Person.class (omitindo muita saída supérflua):

Constant pool: #22 = Fieldref #2.#21 // Person.m1:I #24 = Fieldref #2.#23 // Person.m2:I #30 = Methodref #2.#29 // Person.m:()I #35 = Methodref #4.#34 // java/lang/Object.'':()V // ... public int m(); Code: // other methods refer to this method // ... public int m1(); Code: // get the value of field m1 and return it 0: aload_0 1: getfield #22 // Field m1:I 4: ireturn public int m2(); Code: // get the value of field m2 and return it 0: aload_0 1: getfield #24 // Field m2:I 4: ireturn public void m2_$eq(int); Code: // get the value of this method's input argument 0: aload_0 1: iload_1 // write it to the field m2 and return 2: putfield #24 // Field m2:I 5: return public int m3(); Code: // execute the instance method m(), and return 0: aload_0 1: invokevirtual #30 // Method m:()I 4: ireturn public Person(java.lang.String); Code: // instance constructor ... // execute the instance method m(), and write the result to field m1 9: aload_0 10: aload_0 11: invokevirtual #30 // Method m:()I 14: putfield #22 // Field m1:I // execute the instance method m(), and write the result to field m2 17: aload_0 18: aload_0 19: invokevirtual #30 // Method m:()I 22: putfield #24 // Field m2:I 25: return

No pool constante, vemos que a referência ao método m() é armazenado no índice #30. No código do construtor, vemos que esse método é chamado duas vezes durante a inicialização, com a instrução invokevirtual #30 aparecendo primeiro no deslocamento de byte 11 e, em seguida, no deslocamento 19. A primeira chamada é seguida pela instrução putfield #22 que atribui o resultado deste método ao campo m1, referenciado pelo índice #22 na piscina constante. A segunda chamada é seguida pelo mesmo padrão, desta vez atribuindo o valor ao campo m2, indexado em #24 na piscina constante.

Em outras palavras, atribuir um método a uma variável definida com val ou var apenas atribui o resultado do método para essa variável. Podemos ver que os métodos m1() e m2() que são criados são simplesmente getters para essas variáveis. No caso de var m2, também vemos que o setter m2_$eq(int) é criado, que se comporta como qualquer outro configurador, sobrescrevendo o valor no campo.

No entanto, usando a palavra-chave def dá um resultado diferente. Em vez de buscar um valor de campo para retornar, o método m3() também inclui a instrução invokevirtual #30. Ou seja, cada vez que esse método é chamado, ele chama m() e retorna o resultado desse método.

Portanto, como podemos ver, Scala fornece três maneiras de trabalhar com campos de classe, e elas são facilmente especificadas por meio das palavras-chave val, var e def. Em Java, teríamos que implementar os setters e getters necessários explicitamente, e esse código clichê escrito manualmente seria muito menos expressivo e mais sujeito a erros.

Valores preguiçosos

Um código mais complicado é produzido ao declarar um valor lento. Suponha que adicionamos o seguinte campo à classe definida anteriormente:

lazy val m4 = m

Em execução javap -p -v Person.class irá agora revelar o seguinte:

Constant pool: #20 = Fieldref #2.#19 // Person.bitmap$0:Z #23 = Methodref #2.#22 // Person.m:()I #25 = Fieldref #2.#24 // Person.m4:I #31 = Fieldref #27.#30 // scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; #48 = Methodref #2.#47 // Person.m4$lzycompute:()I // ... private volatile boolean bitmap$0; private int m4$lzycompute(); Code: // lock the thread 0: aload_0 1: dup 2: astore_1 3: monitorenter // check the flag for whether this field has already been set 4: aload_0 5: getfield #20 // Field bitmap$0:Z // if it has, skip to position 24 (unlock the thread and return) 8: ifne 24 // if it hasn't, execute the method m() 11: aload_0 12: aload_0 13: invokevirtual #23 // Method m:()I // write the method to the field m4 16: putfield #25 // Field m4:I // set the flag indicating the field has been set 19: aload_0 20: iconst_1 21: putfield #20 // Field bitmap$0:Z // unlock the thread 24: getstatic #31 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 27: pop 28: aload_1 29: monitorexit // get the value of field m4 and return it 30: aload_0 31: getfield #25 // Field m4:I 34: ireturn // ... public int m4(); Code: // check the flag for whether this field has already been set 0: aload_0 1: getfield #20 // Field bitmap$0:Z // if it hasn't, skip to position 14 (invoke lazy method and return) 4: ifeq 14 // if it has, get the value of field m4, then skip to position 18 (return) 7: aload_0 8: getfield #25 // Field m4:I 11: goto 18 // execute the method m4$lzycompute() to set the field 14: aload_0 15: invokespecial #48 // Method m4$lzycompute:()I // return 18: ireturn

Nesse caso, o valor do campo m4 não é calculado até que seja necessário. O método especial privado m4$lzycompute() é produzido para calcular o valor lento e o campo bitmap$0 para rastrear seu estado. Método m4() verifica se o valor deste campo é 0, indicando que m4 ainda não foi inicializado; nesse caso, m4$lzycompute() é invocado, populando m4 e retornando seu valor. Este método privado também define o valor de bitmap$0 para 1, de modo que da próxima vez m4() for chamado, ele ignorará a chamada do método de inicialização e, em vez disso, simplesmente retornará o valor de m4.

Os resultados da primeira chamada para um valor lento Scala.

O bytecode que Scala produz aqui é projetado para ser seguro para thread e eficaz. Para ser seguro para threads, o método de computação lenta usa o método monitorenter / monitorexit par de instruções. O método permanece efetivo, pois a sobrecarga de desempenho dessa sincronização ocorre apenas na primeira leitura do valor lento.

Apenas um bit é necessário para indicar o estado do valor lento. Portanto, se não houver mais do que 32 valores lazy, um único campo int pode rastrear todos eles. Se mais de um valor lento for definido no código-fonte, o bytecode acima será modificado pelo compilador para implementar uma máscara de bits para esse propósito.

Novamente, Scala nos permite tirar vantagem facilmente de um tipo específico de comportamento que deveria ser implementado explicitamente em Java, economizando esforços e reduzindo o risco de erros de digitação.

Função como valor

Agora vamos dar uma olhada no seguinte código-fonte do Scala:

class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }

O Printer classe tem um campo, output, com o tipo String => Unit: uma função que leva um String e retorna um objeto do tipo Unit (semelhante a void em Java). No método principal, criamos um desses objetos e atribuímos a esse campo uma função anônima que imprime uma determinada string.

Compilar este código gera quatro arquivos de classe:

O código-fonte é compilado em quatro arquivos de classe.

Hello.class é uma classe wrapper cujo método principal simplesmente chama Hello$.main():

public final class Hello // ... public static void main(java.lang.String[]); Code: 0: getstatic #16 // Field Hello$.MODULE$:LHello$; 3: aload_0 4: invokevirtual #18 // Method Hello$.main:([Ljava/lang/String;)V 7: return

O oculto Hello$.class contém a implementação real do método principal. Para dar uma olhada em seu bytecode, certifique-se de escapar corretamente de $ de acordo com as regras do seu shell de comando, para evitar sua interpretação como caractere especial:

public final class Hello$ // ... public void main(java.lang.String[]); Code: // initialize Printer and anonymous function 0: new #16 // class Printer 3: dup 4: new #18 // class Hello$$anonfun$1 7: dup 8: invokespecial #19 // Method Hello$$anonfun$1.'':()V 11: invokespecial #22 // Method Printer.'':(Lscala/Function1;)V 14: astore_2 // load the anonymous function onto the stack 15: aload_2 16: invokevirtual #26 // Method Printer.output:()Lscala/Function1; // execute the anonymous function, passing the string 'Hello' 19: ldc #28 // String Hello 21: invokeinterface #34, 2 // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object; // return 26: pop 27: return

O método cria um Printer. Em seguida, ele cria um Hello$$anonfun$1, que contém nossa função anônima s => println(s). O Printer é inicializado com este objeto como output campo. Este campo é então carregado na pilha e executado com o operando 'Hello'.

Vamos dar uma olhada na classe de função anônima, Hello$$anonfun$1.class, abaixo. Podemos ver que ele estende Function1 de Scala | (como AbstractFunction1) implementando apply() método. Na verdade, ele cria dois apply() métodos, um envolvendo o outro, que juntos executam a verificação de tipo (neste caso, se a entrada é um String), e executam a função anônima (imprimindo a entrada com println()).

public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 implements scala.Serializable // ... // Takes an argument of type String. Invoked second. public final void apply(java.lang.String); Code: // execute Scala's built-in method println(), passing the input argument 0: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: aload_1 4: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 7: return // Takes an argument of type Object. Invoked first. public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 // check that the input argument is a String (throws exception if not) 1: aload_1 2: checkcast #36 // class java/lang/String // invoke the method apply( String ), passing the input argument 5: invokevirtual #38 // Method apply:(Ljava/lang/String;)V // return the void type 8: getstatic #44 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 11: areturn

Olhando para trás, para Hello$.main() acima, podemos ver que, no deslocamento 21, a execução da função anônima é disparada por uma chamada a seu apply( Object ) método.

Finalmente, para completar, vamos dar uma olhada no bytecode para Printer.class:

public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return

Podemos ver que a função anônima aqui é tratada como qualquer val variável. Ele é armazenado no campo de classe output e no getter output() é criado. A única diferença é que essa variável deve agora implementar a interface Scala scala.Function1 (o que AbstractFunction1 faz).

Portanto, o custo desse recurso elegante do Scala são as classes de utilitário subjacentes, criadas para representar e executar uma única função anônima que pode ser usada como um valor. Você deve levar em consideração o número de tais funções, bem como os detalhes de sua implementação de VM, para descobrir o que isso significa para seu aplicativo específico.

Explorando os bastidores com Scala: Explore como essa linguagem poderosa é implementada no bytecode JVM. Tweet

Traços de Escala

As características do Scala são semelhantes às interfaces em Java. A característica a seguir define duas assinaturas de método e fornece uma implementação padrão da segunda. Vamos ver como isso é implementado:

trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }

O código-fonte é compilado em dois arquivos de classe.

Duas entidades são produzidas: Similarity.class, a interface declarando ambos os métodos, e a classe sintética, Similarity$class.class, fornecendo a implementação padrão:

public interface Similarity { public abstract boolean isSimilar(java.lang.Object); public abstract boolean isNotSimilar(java.lang.Object); } public abstract class Similarity$class public static boolean isNotSimilar(Similarity, java.lang.Object); Code: 0: aload_0 // execute the instance method isSimilar() 1: aload_1 2: invokeinterface #13, 2 // InterfaceMethod Similarity.isSimilar:(Ljava/lang/Object;)Z // if the returned value is 0, skip to position 14 (return with value 1) 7: ifeq 14 // otherwise, return with value 0 10: iconst_0 11: goto 15 // return the value 1 14: iconst_1 15: ireturn public static void $init$(Similarity); Code: 0: return

Quando uma classe implementa essa característica e chama o método isNotSimilar, o compilador Scala gera a instrução bytecode invokestatic para chamar o método estático fornecido pela classe que o acompanha.

Polimorfismo complexo e estruturas de herança podem ser criados a partir de características. Por exemplo, vários traços, bem como a classe de implementação, podem substituir um método com a mesma assinatura, chamando super.methodName() para passar o controle para a próxima característica. Quando o compilador Scala encontra essas chamadas, ele:

Portanto, podemos ver que o poderoso conceito de características é implementado no nível da JVM de uma forma que não leva a uma sobrecarga significativa, e os programadores de Scala podem aproveitar esse recurso sem se preocupar se ele será muito caro em tempo de execução.

Singletons

Scala fornece a definição explícita de classes singleton usando a palavra-chave object. Vamos considerar a seguinte classe única:

object Config { val home_dir = '/home/user' }

O compilador produz dois arquivos de classe:

O código-fonte é compilado em dois arquivos de classe.

Config.class é muito simples:

public final class Config public static java.lang.String home_dir(); Code: // execute the method Config$.home_dir() 0: getstatic #16 // Field Config$.MODULE$:LConfig$; 3: invokevirtual #18 // Method Config$.home_dir:()Ljava/lang/String; 6: areturn

Este é apenas um decorador para o material sintético Config$ classe que incorpora a funcionalidade do singleton. Examinando essa classe com javap -p -c produz o seguinte bytecode:

public final class Config$ public static final Config$ MODULE$; // a public reference to the singleton object private final java.lang.String home_dir; // static initializer public static {}; Code: 0: new #2 // class Config$ 3: invokespecial #12 // Method '':()V 6: return public java.lang.String home_dir(); Code: // get the value of field home_dir and return it 0: aload_0 1: getfield #17 // Field home_dir:Ljava/lang/String; 4: areturn private Config$(); Code: // initialize the object 0: aload_0 1: invokespecial #19 // Method java/lang/Object.'':()V // expose a public reference to this object in the synthetic variable MODULE$ 4: aload_0 5: putstatic #21 // Field MODULE$:LConfig$; // load the value '/home/user' and write it to the field home_dir 8: aload_0 9: ldc #23 // String /home/user 11: putfield #17 // Field home_dir:Ljava/lang/String; 14: return

Consiste no seguinte:

O singleton é um padrão de design popular e útil. A linguagem Java não fornece uma maneira direta de especificá-la no nível da linguagem; em vez disso, é responsabilidade do desenvolvedor implementá-lo no código-fonte Java. Scala, por outro lado, fornece uma maneira clara e conveniente de declarar um singleton explicitamente usando object palavra-chave. Como podemos ver olhando por baixo do capô, ele é implementado de forma acessível e natural.

Conclusão

Agora vimos como o Scala compila vários recursos de programação implícitos e funcionais em sofisticadas estruturas de bytecode Java. Com este vislumbre do funcionamento interno do Scala, podemos obter uma apreciação mais profunda do poder do Scala, ajudando-nos a obter o máximo desta linguagem poderosa.

Agora também temos as ferramentas para explorar a linguagem por conta própria. Existem muitos recursos úteis da sintaxe Scala que não são abordados neste artigo, como classes de caso, currying e compreensões de lista. Eu o encorajo a investigar a implementação dessas estruturas em Scala, para que você possa aprender como ser um ninja Scala de próximo nível!


A máquina virtual Java: um curso intensivo

Assim como o compilador Java, o compilador Scala converte o código-fonte em .class arquivos, contendo bytecode Java a ser executado pela Java Virtual Machine. Para entender como os dois idiomas diferem nos bastidores, é necessário entender o sistema que ambos visam. Aqui, apresentamos uma breve visão geral de alguns dos principais elementos da arquitetura da Java Virtual Machine, da estrutura do arquivo de classe e dos fundamentos do assembler.

Observe que este guia cobrirá apenas o mínimo para permitir o acompanhamento do artigo acima. Embora muitos dos principais componentes da JVM não sejam discutidos aqui, detalhes completos podem ser encontrados nos documentos oficiais, Aqui .

Decompilando arquivos de classe com javap
Pool Constante
Tabelas de campo e método
Bytecode JVM
Chamadas de método e pilha de chamadas
Execução na pilha de operando
Variáveis ​​Locais
Voltar ao topo

Decompilando arquivos de classe com javap

Java é fornecido com javap utilitário de linha de comando, que descompila .class arquivos em um formato legível por humanos. Como os arquivos de classe Scala e Java têm como destino a mesma JVM, javap pode ser usado para examinar arquivos de classe compilados pelo Scala.

Vamos compilar o seguinte código-fonte:

// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }

Compilando isso com scalac RegularPolygon.scala irá produzir RegularPolygon.class. Se executarmos javap RegularPolygon.class veremos o seguinte:

$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Esta é uma divisão muito simples do arquivo de classe que simplesmente mostra os nomes e tipos dos membros públicos da classe. Adicionando o -p opção incluirá membros privados:

$ javap -p RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Isso ainda não é muita informação. Para ver como os métodos são implementados no bytecode Java, vamos adicionar o -c opção:

$ javap -p -c RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); Code: 0: aload_0 1: getfield #13 // Field numSides:I 4: ireturn public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn public RegularPolygon(int); Code: 0: aload_0 1: iload_1 2: putfield #13 // Field numSides:I 5: aload_0 6: invokespecial #38 // Method java/lang/Object.'':()V 9: return }

Isso é um pouco mais interessante. No entanto, para realmente obter a história toda, devemos usar o -v ou -verbose opção, como em javap -p -v RegularPolygon.class:

O conteúdo completo de um arquivo de classe Java.

Aqui finalmente vemos o que realmente está no arquivo da classe. O que tudo isso significa? Vamos dar uma olhada em algumas das partes mais importantes.

Pool Constante

O ciclo de desenvolvimento para aplicativos C ++ inclui estágios de compilação e vinculação. O ciclo de desenvolvimento para Java ignora um estágio de ligação explícita porque a ligação acontece no tempo de execução. O arquivo de classe deve oferecer suporte a esse link de tempo de execução. Isso significa que quando o código-fonte se refere a qualquer campo ou método, o bytecode resultante deve manter as referências relevantes em forma simbólica, prontas para serem desreferenciadas assim que o aplicativo for carregado na memória e os endereços reais puderem ser resolvidos pelo vinculador em tempo de execução. Esta forma simbólica deve conter:

A especificação do formato do arquivo de classe inclui uma seção do arquivo chamada de piscina constante , uma tabela de todas as referências necessárias para o vinculador. Ele contém entradas de diferentes tipos.

// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...

O primeiro byte de cada entrada é uma etiqueta numérica que indica o tipo de entrada. Os bytes restantes fornecem informações sobre o valor da entrada. O número de bytes e as regras para sua interpretação dependem do tipo indicado pelo primeiro byte.

Por exemplo, uma classe Java que usa um número inteiro constante 365 pode ter uma entrada de pool constante com o seguinte código de bytes:

x03 00 00 01 6D

O primeiro byte, x03, identifica o tipo de entrada, CONSTANT_Integer. Isso informa ao vinculador que os próximos quatro bytes contêm o valor do inteiro. (Observe que 365 em hexadecimal é x16D). Se esta for a 14ª entrada no pool constante, javap -v irá renderizá-lo assim:

#14 = Integer 365

Muitos tipos de constantes são compostos de referências a tipos de constantes mais “primitivos” em outras partes do pool de constantes. Por exemplo, nosso código de exemplo contém a instrução:

println( 'Calculating perimeter...' )

O uso de uma constante de string produzirá duas entradas no pool de constantes: uma entrada com tipo CONSTANT_String e outra entrada do tipo CONSTANT_Utf8. A entrada do tipo Constant_UTF8 contém a representação UTF8 real do valor da string. A entrada do tipo CONSTANT_String contém uma referência ao CONSTANT_Utf8 entrada:

#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...

Essa complicação é necessária porque existem outros tipos de entradas de pool constantes que se referem a entradas do tipo Utf8 e que não são entradas do tipo String. Por exemplo, qualquer referência a um atributo de classe produzirá um CONSTANT_Fieldref tipo, que contém uma série de referências ao nome da classe, nome do atributo e tipo de atributo:

#1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #9 = Utf8 numSides #10 = Utf8 I #12 = NameAndType #9:#10 // numSides:I #13 = Fieldref #2.#12 // RegularPolygon.numSides:I

Para obter mais detalhes sobre o pool constante, consulte a documentação JVM .

Tabelas de campo e método

Um arquivo de classe contém um tabela de campo que contém informações sobre cada campo (ou seja, atributo) definido na classe. Estas são referências a entradas de pool constantes que descrevem o nome e tipo do campo, bem como sinalizadores de controle de acesso e outros dados relevantes.

Um similar tabela de métodos está presente no arquivo de classe. No entanto, além das informações de nome e tipo, para cada método não abstrato, ele contém as instruções de bytecode reais a serem executadas pela JVM, bem como estruturas de dados usadas pelo frame de pilha do método, descrito abaixo.

Bytecode JVM

A JVM usa seu próprio conjunto de instruções internas para executar o código compilado. Em execução javap com o -c opção inclui as implementações do método compilado na saída. Se examinarmos nosso RegularPolygon.class dessa forma, veremos a seguinte saída para o nosso getPerimeter() método:

public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn

O bytecode real pode ser parecido com isto:

xB2 00 17 x12 19 xB6 00 1D x27 ...

Cada instrução começa com um byte Código de operação identificação da instrução JVM, seguida de zero ou mais operandos de instrução a serem operados, dependendo do formato da instrução específica. Normalmente, são valores constantes ou referências ao conjunto de constantes. javap traduz o bytecode em uma forma legível exibindo:

Operandos que são exibidos com um sinal de libra, como #23, são referências a entradas no conjunto de constantes. Como podemos ver, javap também produz comentários úteis na saída, identificando o que exatamente está sendo referenciado no pool.

Discutiremos algumas das instruções comuns abaixo. Para obter informações detalhadas sobre o conjunto completo de instruções JVM, consulte o documentação .

Chamadas de método e pilha de chamadas

Cada chamada de método deve ser capaz de rodar com seu próprio contexto, que inclui coisas como variáveis ​​declaradas localmente ou argumentos que foram passados ​​para o método. Juntos, eles formam um empilhar quadro . Após a invocação de um método, um novo quadro é criado e colocado no topo do pilha de chamadas . Quando o método retorna, o quadro atual é removido da pilha de chamadas e descartado, e o quadro que estava em vigor antes da chamada do método é restaurado.

Um quadro de pilha inclui algumas estruturas distintas. Dois importantes são os pilha de operandos e a tabela de variável local , discutido a seguir.

A pilha de chamadas JVM.

Execução na pilha de operando

Muitas instruções JVM operam em seus quadros pilha de operandos . Em vez de especificar um operando constante explicitamente no bytecode, essas instruções tomam os valores no topo da pilha de operandos como entrada. Normalmente, esses valores são removidos da pilha no processo. Algumas instruções também colocam novos valores no topo da pilha. Desta forma, as instruções JVM podem ser combinadas para executar operações complexas. Por exemplo, a expressão:

sideLength * this.numSides

é compilado da seguinte forma em nosso getPerimeter() método:

8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul

As instruções JVM podem operar na pilha de operandos para realizar funções complexas.

Quando um método é chamado, uma nova pilha de operandos é criada como parte de sua estrutura de pilha, onde as operações serão realizadas. Devemos ter cuidado com a terminologia aqui: a palavra 'pilha' pode se referir ao pilha de chamadas , a pilha de quadros fornecendo contexto para a execução do método ou para um determinado quadro pilha de operandos , sobre o qual as instruções JVM operam.

Variáveis ​​Locais

Cada frame de pilha mantém uma tabela de variáveis ​​locais . Isso normalmente inclui uma referência a this objeto, quaisquer argumentos que foram passados ​​quando o método foi chamado e quaisquer variáveis ​​locais declaradas dentro do corpo do método. Em execução javap com o -v opção incluirá informações sobre como o quadro de pilha de cada método deve ser configurado, incluindo sua tabela de variável local:

public double getPerimeter(double); // ... Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... // ... LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LRegularPolygon; 0 16 1 sideLength D

Neste exemplo, existem duas variáveis ​​locais. A variável no slot 0 é denominada this, com o tipo RegularPolygon. Esta é a referência à própria classe do método. A variável no slot 1 é denominada sideLength, com o tipo D (indicando um duplo). Este é o argumento que é passado para o nosso getPerimeter() método.

Instruções como iload_1, fstore_2 ou aload [n] transferem diferentes tipos de variáveis ​​locais entre a pilha de operandos e a tabela de variáveis ​​locais. Como o primeiro item da tabela é geralmente a referência a this, a instrução aload_0 é comumente visto em qualquer método que opere em sua própria classe.

Isso conclui nosso passo a passo dos fundamentos da JVM. Clique aqui para retornar ao artigo principal.

; private int m4$lzycompute(); Code: // lock the thread 0: aload_0 1: dup 2: astore_1 3: monitorenter // check the flag for whether this field has already been set 4: aload_0 5: getfield #20 // Field bitmap

Suje Suas Mãos Com Bytecode Scala JVM



A linguagem Scala continuou a ganhar popularidade nos últimos anos, graças à sua excelente combinação de princípios de desenvolvimento de software funcional e orientado a objetos , e sua implementação em cima da comprovada Java Virtual Machine (JVM).

Apesar Escada compila para bytecode Java, é projetado para melhorar muitas das deficiências percebidas da linguagem Java. Oferecendo suporte de programação funcional completo, a sintaxe central do Scala contém muitas estruturas implícitas que devem ser construídas explicitamente por programadores Java, algumas envolvendo considerável complexidade.



A criação de uma linguagem que compila para bytecode Java requer um conhecimento profundo do funcionamento interno da Java Virtual Machine. Para apreciar o que os desenvolvedores do Scala realizaram, é necessário ir nos bastidores e explorar como o código-fonte do Scala é interpretado pelo compilador para produzir bytecode JVM eficiente e eficaz.



Vamos dar uma olhada em como tudo isso é implementado.



Pré-requisitos

Ler este artigo requer algum conhecimento básico do bytecode da Java Virtual Machine. A especificação completa da máquina virtual pode ser obtida em Documentação oficial da Oracle . Ler toda a especificação não é crítica para a compreensão deste artigo, então, para uma rápida introdução ao básico, preparei um pequeno guia no final do artigo.

Clique aqui para ler um curso intensivo sobre noções básicas de JVM.

Um utilitário é necessário para desmontar o bytecode Java para reproduzir os exemplos fornecidos abaixo e para prosseguir com uma investigação adicional. O Java Development Kit fornece seu próprio utilitário de linha de comando, javap, que usaremos aqui. Uma rápida demonstração de como javap trabalhos estão incluídos no guia na parte inferior .



E, claro, uma instalação funcional do compilador Scala é necessária para os leitores que desejam acompanhar os exemplos. Este artigo foi escrito usando Escala 2.11.7 . Diferentes versões do Scala podem produzir bytecode ligeiramente diferente.

Getters e setters padrão

Embora a convenção Java sempre forneça métodos getter e setter para atributos públicos, os programadores Java são obrigados a escrevê-los, apesar do fato de que o padrão para cada um não mudou em décadas. O Scala, em contraste, fornece getters e setters padrão.



Vejamos o seguinte exemplo:

class Person(val name:String) { }

Vamos dar uma olhada dentro da classe Person. Se compilarmos este arquivo com scalac, então executar $ javap -p Person.class nos dá:



Compiled from 'Person.scala' public class Person { private final java.lang.String name; // field public java.lang.String name(); // getter method public Person(java.lang.String); // constructor }

Podemos ver que para cada campo da classe Scala, um campo e seu método getter são gerados. O campo é privado e final, enquanto o método é público.

Se substituirmos val com var no Person fonte e recompilar, então o campo final o modificador é eliminado e o método setter também é adicionado:



Compiled from 'Person.scala' public class Person { private java.lang.String name; // field public java.lang.String name(); // getter method public void name_$eq(java.lang.String); // setter method public Person(java.lang.String); // constructor }

Se houver val ou var é definido dentro do corpo da classe, então o campo privado correspondente e os métodos de acesso são criados e inicializados apropriadamente na criação da instância.

Observe que tal implementação de nível de classe val e var campos significa que se algumas variáveis ​​são usadas no nível da classe para armazenar valores intermediários e nunca são acessadas diretamente pelo programador, a inicialização de cada um desses campos adicionará um ou dois métodos ao espaço da classe. Adicionando um private modificador para tais campos não significa que os acessadores correspondentes serão descartados. Eles apenas se tornarão privados.



Definições de variáveis ​​e funções

Vamos supor que temos um método, m(), e criamos três referências diferentes no estilo Scala para esta função:

class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }

Como cada uma dessas referências a m construído? Quando m ser executado em cada caso? Vamos dar uma olhada no bytecode resultante. A saída a seguir mostra os resultados de javap -v Person.class (omitindo muita saída supérflua):

Constant pool: #22 = Fieldref #2.#21 // Person.m1:I #24 = Fieldref #2.#23 // Person.m2:I #30 = Methodref #2.#29 // Person.m:()I #35 = Methodref #4.#34 // java/lang/Object.'':()V // ... public int m(); Code: // other methods refer to this method // ... public int m1(); Code: // get the value of field m1 and return it 0: aload_0 1: getfield #22 // Field m1:I 4: ireturn public int m2(); Code: // get the value of field m2 and return it 0: aload_0 1: getfield #24 // Field m2:I 4: ireturn public void m2_$eq(int); Code: // get the value of this method's input argument 0: aload_0 1: iload_1 // write it to the field m2 and return 2: putfield #24 // Field m2:I 5: return public int m3(); Code: // execute the instance method m(), and return 0: aload_0 1: invokevirtual #30 // Method m:()I 4: ireturn public Person(java.lang.String); Code: // instance constructor ... // execute the instance method m(), and write the result to field m1 9: aload_0 10: aload_0 11: invokevirtual #30 // Method m:()I 14: putfield #22 // Field m1:I // execute the instance method m(), and write the result to field m2 17: aload_0 18: aload_0 19: invokevirtual #30 // Method m:()I 22: putfield #24 // Field m2:I 25: return

No pool constante, vemos que a referência ao método m() é armazenado no índice #30. No código do construtor, vemos que esse método é chamado duas vezes durante a inicialização, com a instrução invokevirtual #30 aparecendo primeiro no deslocamento de byte 11 e, em seguida, no deslocamento 19. A primeira chamada é seguida pela instrução putfield #22 que atribui o resultado deste método ao campo m1, referenciado pelo índice #22 na piscina constante. A segunda chamada é seguida pelo mesmo padrão, desta vez atribuindo o valor ao campo m2, indexado em #24 na piscina constante.

Em outras palavras, atribuir um método a uma variável definida com val ou var apenas atribui o resultado do método para essa variável. Podemos ver que os métodos m1() e m2() que são criados são simplesmente getters para essas variáveis. No caso de var m2, também vemos que o setter m2_$eq(int) é criado, que se comporta como qualquer outro configurador, sobrescrevendo o valor no campo.

No entanto, usando a palavra-chave def dá um resultado diferente. Em vez de buscar um valor de campo para retornar, o método m3() também inclui a instrução invokevirtual #30. Ou seja, cada vez que esse método é chamado, ele chama m() e retorna o resultado desse método.

Portanto, como podemos ver, Scala fornece três maneiras de trabalhar com campos de classe, e elas são facilmente especificadas por meio das palavras-chave val, var e def. Em Java, teríamos que implementar os setters e getters necessários explicitamente, e esse código clichê escrito manualmente seria muito menos expressivo e mais sujeito a erros.

Valores preguiçosos

Um código mais complicado é produzido ao declarar um valor lento. Suponha que adicionamos o seguinte campo à classe definida anteriormente:

lazy val m4 = m

Em execução javap -p -v Person.class irá agora revelar o seguinte:

Constant pool: #20 = Fieldref #2.#19 // Person.bitmap$0:Z #23 = Methodref #2.#22 // Person.m:()I #25 = Fieldref #2.#24 // Person.m4:I #31 = Fieldref #27.#30 // scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; #48 = Methodref #2.#47 // Person.m4$lzycompute:()I // ... private volatile boolean bitmap$0; private int m4$lzycompute(); Code: // lock the thread 0: aload_0 1: dup 2: astore_1 3: monitorenter // check the flag for whether this field has already been set 4: aload_0 5: getfield #20 // Field bitmap$0:Z // if it has, skip to position 24 (unlock the thread and return) 8: ifne 24 // if it hasn't, execute the method m() 11: aload_0 12: aload_0 13: invokevirtual #23 // Method m:()I // write the method to the field m4 16: putfield #25 // Field m4:I // set the flag indicating the field has been set 19: aload_0 20: iconst_1 21: putfield #20 // Field bitmap$0:Z // unlock the thread 24: getstatic #31 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 27: pop 28: aload_1 29: monitorexit // get the value of field m4 and return it 30: aload_0 31: getfield #25 // Field m4:I 34: ireturn // ... public int m4(); Code: // check the flag for whether this field has already been set 0: aload_0 1: getfield #20 // Field bitmap$0:Z // if it hasn't, skip to position 14 (invoke lazy method and return) 4: ifeq 14 // if it has, get the value of field m4, then skip to position 18 (return) 7: aload_0 8: getfield #25 // Field m4:I 11: goto 18 // execute the method m4$lzycompute() to set the field 14: aload_0 15: invokespecial #48 // Method m4$lzycompute:()I // return 18: ireturn

Nesse caso, o valor do campo m4 não é calculado até que seja necessário. O método especial privado m4$lzycompute() é produzido para calcular o valor lento e o campo bitmap$0 para rastrear seu estado. Método m4() verifica se o valor deste campo é 0, indicando que m4 ainda não foi inicializado; nesse caso, m4$lzycompute() é invocado, populando m4 e retornando seu valor. Este método privado também define o valor de bitmap$0 para 1, de modo que da próxima vez m4() for chamado, ele ignorará a chamada do método de inicialização e, em vez disso, simplesmente retornará o valor de m4.

Os resultados da primeira chamada para um valor lento Scala.

O bytecode que Scala produz aqui é projetado para ser seguro para thread e eficaz. Para ser seguro para threads, o método de computação lenta usa o método monitorenter / monitorexit par de instruções. O método permanece efetivo, pois a sobrecarga de desempenho dessa sincronização ocorre apenas na primeira leitura do valor lento.

Apenas um bit é necessário para indicar o estado do valor lento. Portanto, se não houver mais do que 32 valores lazy, um único campo int pode rastrear todos eles. Se mais de um valor lento for definido no código-fonte, o bytecode acima será modificado pelo compilador para implementar uma máscara de bits para esse propósito.

Novamente, Scala nos permite tirar vantagem facilmente de um tipo específico de comportamento que deveria ser implementado explicitamente em Java, economizando esforços e reduzindo o risco de erros de digitação.

Função como valor

Agora vamos dar uma olhada no seguinte código-fonte do Scala:

class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }

O Printer classe tem um campo, output, com o tipo String => Unit: uma função que leva um String e retorna um objeto do tipo Unit (semelhante a void em Java). No método principal, criamos um desses objetos e atribuímos a esse campo uma função anônima que imprime uma determinada string.

Compilar este código gera quatro arquivos de classe:

O código-fonte é compilado em quatro arquivos de classe.

Hello.class é uma classe wrapper cujo método principal simplesmente chama Hello$.main():

public final class Hello // ... public static void main(java.lang.String[]); Code: 0: getstatic #16 // Field Hello$.MODULE$:LHello$; 3: aload_0 4: invokevirtual #18 // Method Hello$.main:([Ljava/lang/String;)V 7: return

O oculto Hello$.class contém a implementação real do método principal. Para dar uma olhada em seu bytecode, certifique-se de escapar corretamente de $ de acordo com as regras do seu shell de comando, para evitar sua interpretação como caractere especial:

public final class Hello$ // ... public void main(java.lang.String[]); Code: // initialize Printer and anonymous function 0: new #16 // class Printer 3: dup 4: new #18 // class Hello$$anonfun$1 7: dup 8: invokespecial #19 // Method Hello$$anonfun$1.'':()V 11: invokespecial #22 // Method Printer.'':(Lscala/Function1;)V 14: astore_2 // load the anonymous function onto the stack 15: aload_2 16: invokevirtual #26 // Method Printer.output:()Lscala/Function1; // execute the anonymous function, passing the string 'Hello' 19: ldc #28 // String Hello 21: invokeinterface #34, 2 // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object; // return 26: pop 27: return

O método cria um Printer. Em seguida, ele cria um Hello$$anonfun$1, que contém nossa função anônima s => println(s). O Printer é inicializado com este objeto como output campo. Este campo é então carregado na pilha e executado com o operando 'Hello'.

Vamos dar uma olhada na classe de função anônima, Hello$$anonfun$1.class, abaixo. Podemos ver que ele estende Function1 de Scala | (como AbstractFunction1) implementando apply() método. Na verdade, ele cria dois apply() métodos, um envolvendo o outro, que juntos executam a verificação de tipo (neste caso, se a entrada é um String), e executam a função anônima (imprimindo a entrada com println()).

public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 implements scala.Serializable // ... // Takes an argument of type String. Invoked second. public final void apply(java.lang.String); Code: // execute Scala's built-in method println(), passing the input argument 0: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: aload_1 4: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 7: return // Takes an argument of type Object. Invoked first. public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 // check that the input argument is a String (throws exception if not) 1: aload_1 2: checkcast #36 // class java/lang/String // invoke the method apply( String ), passing the input argument 5: invokevirtual #38 // Method apply:(Ljava/lang/String;)V // return the void type 8: getstatic #44 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 11: areturn

Olhando para trás, para Hello$.main() acima, podemos ver que, no deslocamento 21, a execução da função anônima é disparada por uma chamada a seu apply( Object ) método.

Finalmente, para completar, vamos dar uma olhada no bytecode para Printer.class:

public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return

Podemos ver que a função anônima aqui é tratada como qualquer val variável. Ele é armazenado no campo de classe output e no getter output() é criado. A única diferença é que essa variável deve agora implementar a interface Scala scala.Function1 (o que AbstractFunction1 faz).

Portanto, o custo desse recurso elegante do Scala são as classes de utilitário subjacentes, criadas para representar e executar uma única função anônima que pode ser usada como um valor. Você deve levar em consideração o número de tais funções, bem como os detalhes de sua implementação de VM, para descobrir o que isso significa para seu aplicativo específico.

Explorando os bastidores com Scala: Explore como essa linguagem poderosa é implementada no bytecode JVM. Tweet

Traços de Escala

As características do Scala são semelhantes às interfaces em Java. A característica a seguir define duas assinaturas de método e fornece uma implementação padrão da segunda. Vamos ver como isso é implementado:

trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }

O código-fonte é compilado em dois arquivos de classe.

Duas entidades são produzidas: Similarity.class, a interface declarando ambos os métodos, e a classe sintética, Similarity$class.class, fornecendo a implementação padrão:

public interface Similarity { public abstract boolean isSimilar(java.lang.Object); public abstract boolean isNotSimilar(java.lang.Object); } public abstract class Similarity$class public static boolean isNotSimilar(Similarity, java.lang.Object); Code: 0: aload_0 // execute the instance method isSimilar() 1: aload_1 2: invokeinterface #13, 2 // InterfaceMethod Similarity.isSimilar:(Ljava/lang/Object;)Z // if the returned value is 0, skip to position 14 (return with value 1) 7: ifeq 14 // otherwise, return with value 0 10: iconst_0 11: goto 15 // return the value 1 14: iconst_1 15: ireturn public static void $init$(Similarity); Code: 0: return

Quando uma classe implementa essa característica e chama o método isNotSimilar, o compilador Scala gera a instrução bytecode invokestatic para chamar o método estático fornecido pela classe que o acompanha.

Polimorfismo complexo e estruturas de herança podem ser criados a partir de características. Por exemplo, vários traços, bem como a classe de implementação, podem substituir um método com a mesma assinatura, chamando super.methodName() para passar o controle para a próxima característica. Quando o compilador Scala encontra essas chamadas, ele:

Portanto, podemos ver que o poderoso conceito de características é implementado no nível da JVM de uma forma que não leva a uma sobrecarga significativa, e os programadores de Scala podem aproveitar esse recurso sem se preocupar se ele será muito caro em tempo de execução.

Singletons

Scala fornece a definição explícita de classes singleton usando a palavra-chave object. Vamos considerar a seguinte classe única:

object Config { val home_dir = '/home/user' }

O compilador produz dois arquivos de classe:

O código-fonte é compilado em dois arquivos de classe.

Config.class é muito simples:

public final class Config public static java.lang.String home_dir(); Code: // execute the method Config$.home_dir() 0: getstatic #16 // Field Config$.MODULE$:LConfig$; 3: invokevirtual #18 // Method Config$.home_dir:()Ljava/lang/String; 6: areturn

Este é apenas um decorador para o material sintético Config$ classe que incorpora a funcionalidade do singleton. Examinando essa classe com javap -p -c produz o seguinte bytecode:

public final class Config$ public static final Config$ MODULE$; // a public reference to the singleton object private final java.lang.String home_dir; // static initializer public static {}; Code: 0: new #2 // class Config$ 3: invokespecial #12 // Method '':()V 6: return public java.lang.String home_dir(); Code: // get the value of field home_dir and return it 0: aload_0 1: getfield #17 // Field home_dir:Ljava/lang/String; 4: areturn private Config$(); Code: // initialize the object 0: aload_0 1: invokespecial #19 // Method java/lang/Object.'':()V // expose a public reference to this object in the synthetic variable MODULE$ 4: aload_0 5: putstatic #21 // Field MODULE$:LConfig$; // load the value '/home/user' and write it to the field home_dir 8: aload_0 9: ldc #23 // String /home/user 11: putfield #17 // Field home_dir:Ljava/lang/String; 14: return

Consiste no seguinte:

O singleton é um padrão de design popular e útil. A linguagem Java não fornece uma maneira direta de especificá-la no nível da linguagem; em vez disso, é responsabilidade do desenvolvedor implementá-lo no código-fonte Java. Scala, por outro lado, fornece uma maneira clara e conveniente de declarar um singleton explicitamente usando object palavra-chave. Como podemos ver olhando por baixo do capô, ele é implementado de forma acessível e natural.

Conclusão

Agora vimos como o Scala compila vários recursos de programação implícitos e funcionais em sofisticadas estruturas de bytecode Java. Com este vislumbre do funcionamento interno do Scala, podemos obter uma apreciação mais profunda do poder do Scala, ajudando-nos a obter o máximo desta linguagem poderosa.

Agora também temos as ferramentas para explorar a linguagem por conta própria. Existem muitos recursos úteis da sintaxe Scala que não são abordados neste artigo, como classes de caso, currying e compreensões de lista. Eu o encorajo a investigar a implementação dessas estruturas em Scala, para que você possa aprender como ser um ninja Scala de próximo nível!


A máquina virtual Java: um curso intensivo

Assim como o compilador Java, o compilador Scala converte o código-fonte em .class arquivos, contendo bytecode Java a ser executado pela Java Virtual Machine. Para entender como os dois idiomas diferem nos bastidores, é necessário entender o sistema que ambos visam. Aqui, apresentamos uma breve visão geral de alguns dos principais elementos da arquitetura da Java Virtual Machine, da estrutura do arquivo de classe e dos fundamentos do assembler.

Observe que este guia cobrirá apenas o mínimo para permitir o acompanhamento do artigo acima. Embora muitos dos principais componentes da JVM não sejam discutidos aqui, detalhes completos podem ser encontrados nos documentos oficiais, Aqui .

Decompilando arquivos de classe com javap
Pool Constante
Tabelas de campo e método
Bytecode JVM
Chamadas de método e pilha de chamadas
Execução na pilha de operando
Variáveis ​​Locais
Voltar ao topo

Decompilando arquivos de classe com javap

Java é fornecido com javap utilitário de linha de comando, que descompila .class arquivos em um formato legível por humanos. Como os arquivos de classe Scala e Java têm como destino a mesma JVM, javap pode ser usado para examinar arquivos de classe compilados pelo Scala.

Vamos compilar o seguinte código-fonte:

// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }

Compilando isso com scalac RegularPolygon.scala irá produzir RegularPolygon.class. Se executarmos javap RegularPolygon.class veremos o seguinte:

$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Esta é uma divisão muito simples do arquivo de classe que simplesmente mostra os nomes e tipos dos membros públicos da classe. Adicionando o -p opção incluirá membros privados:

$ javap -p RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Isso ainda não é muita informação. Para ver como os métodos são implementados no bytecode Java, vamos adicionar o -c opção:

$ javap -p -c RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); Code: 0: aload_0 1: getfield #13 // Field numSides:I 4: ireturn public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn public RegularPolygon(int); Code: 0: aload_0 1: iload_1 2: putfield #13 // Field numSides:I 5: aload_0 6: invokespecial #38 // Method java/lang/Object.'':()V 9: return }

Isso é um pouco mais interessante. No entanto, para realmente obter a história toda, devemos usar o -v ou -verbose opção, como em javap -p -v RegularPolygon.class:

O conteúdo completo de um arquivo de classe Java.

Aqui finalmente vemos o que realmente está no arquivo da classe. O que tudo isso significa? Vamos dar uma olhada em algumas das partes mais importantes.

Pool Constante

O ciclo de desenvolvimento para aplicativos C ++ inclui estágios de compilação e vinculação. O ciclo de desenvolvimento para Java ignora um estágio de ligação explícita porque a ligação acontece no tempo de execução. O arquivo de classe deve oferecer suporte a esse link de tempo de execução. Isso significa que quando o código-fonte se refere a qualquer campo ou método, o bytecode resultante deve manter as referências relevantes em forma simbólica, prontas para serem desreferenciadas assim que o aplicativo for carregado na memória e os endereços reais puderem ser resolvidos pelo vinculador em tempo de execução. Esta forma simbólica deve conter:

A especificação do formato do arquivo de classe inclui uma seção do arquivo chamada de piscina constante , uma tabela de todas as referências necessárias para o vinculador. Ele contém entradas de diferentes tipos.

// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...

O primeiro byte de cada entrada é uma etiqueta numérica que indica o tipo de entrada. Os bytes restantes fornecem informações sobre o valor da entrada. O número de bytes e as regras para sua interpretação dependem do tipo indicado pelo primeiro byte.

Por exemplo, uma classe Java que usa um número inteiro constante 365 pode ter uma entrada de pool constante com o seguinte código de bytes:

x03 00 00 01 6D

O primeiro byte, x03, identifica o tipo de entrada, CONSTANT_Integer. Isso informa ao vinculador que os próximos quatro bytes contêm o valor do inteiro. (Observe que 365 em hexadecimal é x16D). Se esta for a 14ª entrada no pool constante, javap -v irá renderizá-lo assim:

#14 = Integer 365

Muitos tipos de constantes são compostos de referências a tipos de constantes mais “primitivos” em outras partes do pool de constantes. Por exemplo, nosso código de exemplo contém a instrução:

println( 'Calculating perimeter...' )

O uso de uma constante de string produzirá duas entradas no pool de constantes: uma entrada com tipo CONSTANT_String e outra entrada do tipo CONSTANT_Utf8. A entrada do tipo Constant_UTF8 contém a representação UTF8 real do valor da string. A entrada do tipo CONSTANT_String contém uma referência ao CONSTANT_Utf8 entrada:

#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...

Essa complicação é necessária porque existem outros tipos de entradas de pool constantes que se referem a entradas do tipo Utf8 e que não são entradas do tipo String. Por exemplo, qualquer referência a um atributo de classe produzirá um CONSTANT_Fieldref tipo, que contém uma série de referências ao nome da classe, nome do atributo e tipo de atributo:

#1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #9 = Utf8 numSides #10 = Utf8 I #12 = NameAndType #9:#10 // numSides:I #13 = Fieldref #2.#12 // RegularPolygon.numSides:I

Para obter mais detalhes sobre o pool constante, consulte a documentação JVM .

Tabelas de campo e método

Um arquivo de classe contém um tabela de campo que contém informações sobre cada campo (ou seja, atributo) definido na classe. Estas são referências a entradas de pool constantes que descrevem o nome e tipo do campo, bem como sinalizadores de controle de acesso e outros dados relevantes.

Um similar tabela de métodos está presente no arquivo de classe. No entanto, além das informações de nome e tipo, para cada método não abstrato, ele contém as instruções de bytecode reais a serem executadas pela JVM, bem como estruturas de dados usadas pelo frame de pilha do método, descrito abaixo.

Bytecode JVM

A JVM usa seu próprio conjunto de instruções internas para executar o código compilado. Em execução javap com o -c opção inclui as implementações do método compilado na saída. Se examinarmos nosso RegularPolygon.class dessa forma, veremos a seguinte saída para o nosso getPerimeter() método:

public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn

O bytecode real pode ser parecido com isto:

xB2 00 17 x12 19 xB6 00 1D x27 ...

Cada instrução começa com um byte Código de operação identificação da instrução JVM, seguida de zero ou mais operandos de instrução a serem operados, dependendo do formato da instrução específica. Normalmente, são valores constantes ou referências ao conjunto de constantes. javap traduz o bytecode em uma forma legível exibindo:

Operandos que são exibidos com um sinal de libra, como #23, são referências a entradas no conjunto de constantes. Como podemos ver, javap também produz comentários úteis na saída, identificando o que exatamente está sendo referenciado no pool.

Discutiremos algumas das instruções comuns abaixo. Para obter informações detalhadas sobre o conjunto completo de instruções JVM, consulte o documentação .

Chamadas de método e pilha de chamadas

Cada chamada de método deve ser capaz de rodar com seu próprio contexto, que inclui coisas como variáveis ​​declaradas localmente ou argumentos que foram passados ​​para o método. Juntos, eles formam um empilhar quadro . Após a invocação de um método, um novo quadro é criado e colocado no topo do pilha de chamadas . Quando o método retorna, o quadro atual é removido da pilha de chamadas e descartado, e o quadro que estava em vigor antes da chamada do método é restaurado.

Um quadro de pilha inclui algumas estruturas distintas. Dois importantes são os pilha de operandos e a tabela de variável local , discutido a seguir.

A pilha de chamadas JVM.

Execução na pilha de operando

Muitas instruções JVM operam em seus quadros pilha de operandos . Em vez de especificar um operando constante explicitamente no bytecode, essas instruções tomam os valores no topo da pilha de operandos como entrada. Normalmente, esses valores são removidos da pilha no processo. Algumas instruções também colocam novos valores no topo da pilha. Desta forma, as instruções JVM podem ser combinadas para executar operações complexas. Por exemplo, a expressão:

sideLength * this.numSides

é compilado da seguinte forma em nosso getPerimeter() método:

8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul

As instruções JVM podem operar na pilha de operandos para realizar funções complexas.

Quando um método é chamado, uma nova pilha de operandos é criada como parte de sua estrutura de pilha, onde as operações serão realizadas. Devemos ter cuidado com a terminologia aqui: a palavra 'pilha' pode se referir ao pilha de chamadas , a pilha de quadros fornecendo contexto para a execução do método ou para um determinado quadro pilha de operandos , sobre o qual as instruções JVM operam.

Variáveis ​​Locais

Cada frame de pilha mantém uma tabela de variáveis ​​locais . Isso normalmente inclui uma referência a this objeto, quaisquer argumentos que foram passados ​​quando o método foi chamado e quaisquer variáveis ​​locais declaradas dentro do corpo do método. Em execução javap com o -v opção incluirá informações sobre como o quadro de pilha de cada método deve ser configurado, incluindo sua tabela de variável local:

public double getPerimeter(double); // ... Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... // ... LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LRegularPolygon; 0 16 1 sideLength D

Neste exemplo, existem duas variáveis ​​locais. A variável no slot 0 é denominada this, com o tipo RegularPolygon. Esta é a referência à própria classe do método. A variável no slot 1 é denominada sideLength, com o tipo D (indicando um duplo). Este é o argumento que é passado para o nosso getPerimeter() método.

Instruções como iload_1, fstore_2 ou aload [n] transferem diferentes tipos de variáveis ​​locais entre a pilha de operandos e a tabela de variáveis ​​locais. Como o primeiro item da tabela é geralmente a referência a this, a instrução aload_0 é comumente visto em qualquer método que opere em sua própria classe.

Isso conclui nosso passo a passo dos fundamentos da JVM. Clique aqui para retornar ao artigo principal.

:Z // if it has, skip to position 24 (unlock the thread and return) 8: ifne 24 // if it hasn't, execute the method m() 11: aload_0 12: aload_0 13: invokevirtual #23 // Method m:()I // write the method to the field m4 16: putfield #25 // Field m4:I // set the flag indicating the field has been set 19: aload_0 20: iconst_1 21: putfield #20 // Field bitmap

Suje Suas Mãos Com Bytecode Scala JVM



A linguagem Scala continuou a ganhar popularidade nos últimos anos, graças à sua excelente combinação de princípios de desenvolvimento de software funcional e orientado a objetos , e sua implementação em cima da comprovada Java Virtual Machine (JVM).

Apesar Escada compila para bytecode Java, é projetado para melhorar muitas das deficiências percebidas da linguagem Java. Oferecendo suporte de programação funcional completo, a sintaxe central do Scala contém muitas estruturas implícitas que devem ser construídas explicitamente por programadores Java, algumas envolvendo considerável complexidade.



A criação de uma linguagem que compila para bytecode Java requer um conhecimento profundo do funcionamento interno da Java Virtual Machine. Para apreciar o que os desenvolvedores do Scala realizaram, é necessário ir nos bastidores e explorar como o código-fonte do Scala é interpretado pelo compilador para produzir bytecode JVM eficiente e eficaz.



Vamos dar uma olhada em como tudo isso é implementado.



Pré-requisitos

Ler este artigo requer algum conhecimento básico do bytecode da Java Virtual Machine. A especificação completa da máquina virtual pode ser obtida em Documentação oficial da Oracle . Ler toda a especificação não é crítica para a compreensão deste artigo, então, para uma rápida introdução ao básico, preparei um pequeno guia no final do artigo.

Clique aqui para ler um curso intensivo sobre noções básicas de JVM.

Um utilitário é necessário para desmontar o bytecode Java para reproduzir os exemplos fornecidos abaixo e para prosseguir com uma investigação adicional. O Java Development Kit fornece seu próprio utilitário de linha de comando, javap, que usaremos aqui. Uma rápida demonstração de como javap trabalhos estão incluídos no guia na parte inferior .



E, claro, uma instalação funcional do compilador Scala é necessária para os leitores que desejam acompanhar os exemplos. Este artigo foi escrito usando Escala 2.11.7 . Diferentes versões do Scala podem produzir bytecode ligeiramente diferente.

Getters e setters padrão

Embora a convenção Java sempre forneça métodos getter e setter para atributos públicos, os programadores Java são obrigados a escrevê-los, apesar do fato de que o padrão para cada um não mudou em décadas. O Scala, em contraste, fornece getters e setters padrão.



Vejamos o seguinte exemplo:

class Person(val name:String) { }

Vamos dar uma olhada dentro da classe Person. Se compilarmos este arquivo com scalac, então executar $ javap -p Person.class nos dá:



Compiled from 'Person.scala' public class Person { private final java.lang.String name; // field public java.lang.String name(); // getter method public Person(java.lang.String); // constructor }

Podemos ver que para cada campo da classe Scala, um campo e seu método getter são gerados. O campo é privado e final, enquanto o método é público.

Se substituirmos val com var no Person fonte e recompilar, então o campo final o modificador é eliminado e o método setter também é adicionado:



Compiled from 'Person.scala' public class Person { private java.lang.String name; // field public java.lang.String name(); // getter method public void name_$eq(java.lang.String); // setter method public Person(java.lang.String); // constructor }

Se houver val ou var é definido dentro do corpo da classe, então o campo privado correspondente e os métodos de acesso são criados e inicializados apropriadamente na criação da instância.

Observe que tal implementação de nível de classe val e var campos significa que se algumas variáveis ​​são usadas no nível da classe para armazenar valores intermediários e nunca são acessadas diretamente pelo programador, a inicialização de cada um desses campos adicionará um ou dois métodos ao espaço da classe. Adicionando um private modificador para tais campos não significa que os acessadores correspondentes serão descartados. Eles apenas se tornarão privados.



Definições de variáveis ​​e funções

Vamos supor que temos um método, m(), e criamos três referências diferentes no estilo Scala para esta função:

class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }

Como cada uma dessas referências a m construído? Quando m ser executado em cada caso? Vamos dar uma olhada no bytecode resultante. A saída a seguir mostra os resultados de javap -v Person.class (omitindo muita saída supérflua):

Constant pool: #22 = Fieldref #2.#21 // Person.m1:I #24 = Fieldref #2.#23 // Person.m2:I #30 = Methodref #2.#29 // Person.m:()I #35 = Methodref #4.#34 // java/lang/Object.'':()V // ... public int m(); Code: // other methods refer to this method // ... public int m1(); Code: // get the value of field m1 and return it 0: aload_0 1: getfield #22 // Field m1:I 4: ireturn public int m2(); Code: // get the value of field m2 and return it 0: aload_0 1: getfield #24 // Field m2:I 4: ireturn public void m2_$eq(int); Code: // get the value of this method's input argument 0: aload_0 1: iload_1 // write it to the field m2 and return 2: putfield #24 // Field m2:I 5: return public int m3(); Code: // execute the instance method m(), and return 0: aload_0 1: invokevirtual #30 // Method m:()I 4: ireturn public Person(java.lang.String); Code: // instance constructor ... // execute the instance method m(), and write the result to field m1 9: aload_0 10: aload_0 11: invokevirtual #30 // Method m:()I 14: putfield #22 // Field m1:I // execute the instance method m(), and write the result to field m2 17: aload_0 18: aload_0 19: invokevirtual #30 // Method m:()I 22: putfield #24 // Field m2:I 25: return

No pool constante, vemos que a referência ao método m() é armazenado no índice #30. No código do construtor, vemos que esse método é chamado duas vezes durante a inicialização, com a instrução invokevirtual #30 aparecendo primeiro no deslocamento de byte 11 e, em seguida, no deslocamento 19. A primeira chamada é seguida pela instrução putfield #22 que atribui o resultado deste método ao campo m1, referenciado pelo índice #22 na piscina constante. A segunda chamada é seguida pelo mesmo padrão, desta vez atribuindo o valor ao campo m2, indexado em #24 na piscina constante.

Em outras palavras, atribuir um método a uma variável definida com val ou var apenas atribui o resultado do método para essa variável. Podemos ver que os métodos m1() e m2() que são criados são simplesmente getters para essas variáveis. No caso de var m2, também vemos que o setter m2_$eq(int) é criado, que se comporta como qualquer outro configurador, sobrescrevendo o valor no campo.

No entanto, usando a palavra-chave def dá um resultado diferente. Em vez de buscar um valor de campo para retornar, o método m3() também inclui a instrução invokevirtual #30. Ou seja, cada vez que esse método é chamado, ele chama m() e retorna o resultado desse método.

Portanto, como podemos ver, Scala fornece três maneiras de trabalhar com campos de classe, e elas são facilmente especificadas por meio das palavras-chave val, var e def. Em Java, teríamos que implementar os setters e getters necessários explicitamente, e esse código clichê escrito manualmente seria muito menos expressivo e mais sujeito a erros.

Valores preguiçosos

Um código mais complicado é produzido ao declarar um valor lento. Suponha que adicionamos o seguinte campo à classe definida anteriormente:

lazy val m4 = m

Em execução javap -p -v Person.class irá agora revelar o seguinte:

Constant pool: #20 = Fieldref #2.#19 // Person.bitmap$0:Z #23 = Methodref #2.#22 // Person.m:()I #25 = Fieldref #2.#24 // Person.m4:I #31 = Fieldref #27.#30 // scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; #48 = Methodref #2.#47 // Person.m4$lzycompute:()I // ... private volatile boolean bitmap$0; private int m4$lzycompute(); Code: // lock the thread 0: aload_0 1: dup 2: astore_1 3: monitorenter // check the flag for whether this field has already been set 4: aload_0 5: getfield #20 // Field bitmap$0:Z // if it has, skip to position 24 (unlock the thread and return) 8: ifne 24 // if it hasn't, execute the method m() 11: aload_0 12: aload_0 13: invokevirtual #23 // Method m:()I // write the method to the field m4 16: putfield #25 // Field m4:I // set the flag indicating the field has been set 19: aload_0 20: iconst_1 21: putfield #20 // Field bitmap$0:Z // unlock the thread 24: getstatic #31 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 27: pop 28: aload_1 29: monitorexit // get the value of field m4 and return it 30: aload_0 31: getfield #25 // Field m4:I 34: ireturn // ... public int m4(); Code: // check the flag for whether this field has already been set 0: aload_0 1: getfield #20 // Field bitmap$0:Z // if it hasn't, skip to position 14 (invoke lazy method and return) 4: ifeq 14 // if it has, get the value of field m4, then skip to position 18 (return) 7: aload_0 8: getfield #25 // Field m4:I 11: goto 18 // execute the method m4$lzycompute() to set the field 14: aload_0 15: invokespecial #48 // Method m4$lzycompute:()I // return 18: ireturn

Nesse caso, o valor do campo m4 não é calculado até que seja necessário. O método especial privado m4$lzycompute() é produzido para calcular o valor lento e o campo bitmap$0 para rastrear seu estado. Método m4() verifica se o valor deste campo é 0, indicando que m4 ainda não foi inicializado; nesse caso, m4$lzycompute() é invocado, populando m4 e retornando seu valor. Este método privado também define o valor de bitmap$0 para 1, de modo que da próxima vez m4() for chamado, ele ignorará a chamada do método de inicialização e, em vez disso, simplesmente retornará o valor de m4.

Os resultados da primeira chamada para um valor lento Scala.

O bytecode que Scala produz aqui é projetado para ser seguro para thread e eficaz. Para ser seguro para threads, o método de computação lenta usa o método monitorenter / monitorexit par de instruções. O método permanece efetivo, pois a sobrecarga de desempenho dessa sincronização ocorre apenas na primeira leitura do valor lento.

Apenas um bit é necessário para indicar o estado do valor lento. Portanto, se não houver mais do que 32 valores lazy, um único campo int pode rastrear todos eles. Se mais de um valor lento for definido no código-fonte, o bytecode acima será modificado pelo compilador para implementar uma máscara de bits para esse propósito.

Novamente, Scala nos permite tirar vantagem facilmente de um tipo específico de comportamento que deveria ser implementado explicitamente em Java, economizando esforços e reduzindo o risco de erros de digitação.

Função como valor

Agora vamos dar uma olhada no seguinte código-fonte do Scala:

class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }

O Printer classe tem um campo, output, com o tipo String => Unit: uma função que leva um String e retorna um objeto do tipo Unit (semelhante a void em Java). No método principal, criamos um desses objetos e atribuímos a esse campo uma função anônima que imprime uma determinada string.

Compilar este código gera quatro arquivos de classe:

O código-fonte é compilado em quatro arquivos de classe.

Hello.class é uma classe wrapper cujo método principal simplesmente chama Hello$.main():

public final class Hello // ... public static void main(java.lang.String[]); Code: 0: getstatic #16 // Field Hello$.MODULE$:LHello$; 3: aload_0 4: invokevirtual #18 // Method Hello$.main:([Ljava/lang/String;)V 7: return

O oculto Hello$.class contém a implementação real do método principal. Para dar uma olhada em seu bytecode, certifique-se de escapar corretamente de $ de acordo com as regras do seu shell de comando, para evitar sua interpretação como caractere especial:

public final class Hello$ // ... public void main(java.lang.String[]); Code: // initialize Printer and anonymous function 0: new #16 // class Printer 3: dup 4: new #18 // class Hello$$anonfun$1 7: dup 8: invokespecial #19 // Method Hello$$anonfun$1.'':()V 11: invokespecial #22 // Method Printer.'':(Lscala/Function1;)V 14: astore_2 // load the anonymous function onto the stack 15: aload_2 16: invokevirtual #26 // Method Printer.output:()Lscala/Function1; // execute the anonymous function, passing the string 'Hello' 19: ldc #28 // String Hello 21: invokeinterface #34, 2 // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object; // return 26: pop 27: return

O método cria um Printer. Em seguida, ele cria um Hello$$anonfun$1, que contém nossa função anônima s => println(s). O Printer é inicializado com este objeto como output campo. Este campo é então carregado na pilha e executado com o operando 'Hello'.

Vamos dar uma olhada na classe de função anônima, Hello$$anonfun$1.class, abaixo. Podemos ver que ele estende Function1 de Scala | (como AbstractFunction1) implementando apply() método. Na verdade, ele cria dois apply() métodos, um envolvendo o outro, que juntos executam a verificação de tipo (neste caso, se a entrada é um String), e executam a função anônima (imprimindo a entrada com println()).

public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 implements scala.Serializable // ... // Takes an argument of type String. Invoked second. public final void apply(java.lang.String); Code: // execute Scala's built-in method println(), passing the input argument 0: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: aload_1 4: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 7: return // Takes an argument of type Object. Invoked first. public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 // check that the input argument is a String (throws exception if not) 1: aload_1 2: checkcast #36 // class java/lang/String // invoke the method apply( String ), passing the input argument 5: invokevirtual #38 // Method apply:(Ljava/lang/String;)V // return the void type 8: getstatic #44 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 11: areturn

Olhando para trás, para Hello$.main() acima, podemos ver que, no deslocamento 21, a execução da função anônima é disparada por uma chamada a seu apply( Object ) método.

Finalmente, para completar, vamos dar uma olhada no bytecode para Printer.class:

public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return

Podemos ver que a função anônima aqui é tratada como qualquer val variável. Ele é armazenado no campo de classe output e no getter output() é criado. A única diferença é que essa variável deve agora implementar a interface Scala scala.Function1 (o que AbstractFunction1 faz).

Portanto, o custo desse recurso elegante do Scala são as classes de utilitário subjacentes, criadas para representar e executar uma única função anônima que pode ser usada como um valor. Você deve levar em consideração o número de tais funções, bem como os detalhes de sua implementação de VM, para descobrir o que isso significa para seu aplicativo específico.

Explorando os bastidores com Scala: Explore como essa linguagem poderosa é implementada no bytecode JVM. Tweet

Traços de Escala

As características do Scala são semelhantes às interfaces em Java. A característica a seguir define duas assinaturas de método e fornece uma implementação padrão da segunda. Vamos ver como isso é implementado:

trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }

O código-fonte é compilado em dois arquivos de classe.

Duas entidades são produzidas: Similarity.class, a interface declarando ambos os métodos, e a classe sintética, Similarity$class.class, fornecendo a implementação padrão:

public interface Similarity { public abstract boolean isSimilar(java.lang.Object); public abstract boolean isNotSimilar(java.lang.Object); } public abstract class Similarity$class public static boolean isNotSimilar(Similarity, java.lang.Object); Code: 0: aload_0 // execute the instance method isSimilar() 1: aload_1 2: invokeinterface #13, 2 // InterfaceMethod Similarity.isSimilar:(Ljava/lang/Object;)Z // if the returned value is 0, skip to position 14 (return with value 1) 7: ifeq 14 // otherwise, return with value 0 10: iconst_0 11: goto 15 // return the value 1 14: iconst_1 15: ireturn public static void $init$(Similarity); Code: 0: return

Quando uma classe implementa essa característica e chama o método isNotSimilar, o compilador Scala gera a instrução bytecode invokestatic para chamar o método estático fornecido pela classe que o acompanha.

Polimorfismo complexo e estruturas de herança podem ser criados a partir de características. Por exemplo, vários traços, bem como a classe de implementação, podem substituir um método com a mesma assinatura, chamando super.methodName() para passar o controle para a próxima característica. Quando o compilador Scala encontra essas chamadas, ele:

Portanto, podemos ver que o poderoso conceito de características é implementado no nível da JVM de uma forma que não leva a uma sobrecarga significativa, e os programadores de Scala podem aproveitar esse recurso sem se preocupar se ele será muito caro em tempo de execução.

Singletons

Scala fornece a definição explícita de classes singleton usando a palavra-chave object. Vamos considerar a seguinte classe única:

object Config { val home_dir = '/home/user' }

O compilador produz dois arquivos de classe:

O código-fonte é compilado em dois arquivos de classe.

Config.class é muito simples:

public final class Config public static java.lang.String home_dir(); Code: // execute the method Config$.home_dir() 0: getstatic #16 // Field Config$.MODULE$:LConfig$; 3: invokevirtual #18 // Method Config$.home_dir:()Ljava/lang/String; 6: areturn

Este é apenas um decorador para o material sintético Config$ classe que incorpora a funcionalidade do singleton. Examinando essa classe com javap -p -c produz o seguinte bytecode:

public final class Config$ public static final Config$ MODULE$; // a public reference to the singleton object private final java.lang.String home_dir; // static initializer public static {}; Code: 0: new #2 // class Config$ 3: invokespecial #12 // Method '':()V 6: return public java.lang.String home_dir(); Code: // get the value of field home_dir and return it 0: aload_0 1: getfield #17 // Field home_dir:Ljava/lang/String; 4: areturn private Config$(); Code: // initialize the object 0: aload_0 1: invokespecial #19 // Method java/lang/Object.'':()V // expose a public reference to this object in the synthetic variable MODULE$ 4: aload_0 5: putstatic #21 // Field MODULE$:LConfig$; // load the value '/home/user' and write it to the field home_dir 8: aload_0 9: ldc #23 // String /home/user 11: putfield #17 // Field home_dir:Ljava/lang/String; 14: return

Consiste no seguinte:

O singleton é um padrão de design popular e útil. A linguagem Java não fornece uma maneira direta de especificá-la no nível da linguagem; em vez disso, é responsabilidade do desenvolvedor implementá-lo no código-fonte Java. Scala, por outro lado, fornece uma maneira clara e conveniente de declarar um singleton explicitamente usando object palavra-chave. Como podemos ver olhando por baixo do capô, ele é implementado de forma acessível e natural.

Conclusão

Agora vimos como o Scala compila vários recursos de programação implícitos e funcionais em sofisticadas estruturas de bytecode Java. Com este vislumbre do funcionamento interno do Scala, podemos obter uma apreciação mais profunda do poder do Scala, ajudando-nos a obter o máximo desta linguagem poderosa.

Agora também temos as ferramentas para explorar a linguagem por conta própria. Existem muitos recursos úteis da sintaxe Scala que não são abordados neste artigo, como classes de caso, currying e compreensões de lista. Eu o encorajo a investigar a implementação dessas estruturas em Scala, para que você possa aprender como ser um ninja Scala de próximo nível!


A máquina virtual Java: um curso intensivo

Assim como o compilador Java, o compilador Scala converte o código-fonte em .class arquivos, contendo bytecode Java a ser executado pela Java Virtual Machine. Para entender como os dois idiomas diferem nos bastidores, é necessário entender o sistema que ambos visam. Aqui, apresentamos uma breve visão geral de alguns dos principais elementos da arquitetura da Java Virtual Machine, da estrutura do arquivo de classe e dos fundamentos do assembler.

Observe que este guia cobrirá apenas o mínimo para permitir o acompanhamento do artigo acima. Embora muitos dos principais componentes da JVM não sejam discutidos aqui, detalhes completos podem ser encontrados nos documentos oficiais, Aqui .

Decompilando arquivos de classe com javap
Pool Constante
Tabelas de campo e método
Bytecode JVM
Chamadas de método e pilha de chamadas
Execução na pilha de operando
Variáveis ​​Locais
Voltar ao topo

Decompilando arquivos de classe com javap

Java é fornecido com javap utilitário de linha de comando, que descompila .class arquivos em um formato legível por humanos. Como os arquivos de classe Scala e Java têm como destino a mesma JVM, javap pode ser usado para examinar arquivos de classe compilados pelo Scala.

Vamos compilar o seguinte código-fonte:

// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }

Compilando isso com scalac RegularPolygon.scala irá produzir RegularPolygon.class. Se executarmos javap RegularPolygon.class veremos o seguinte:

$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Esta é uma divisão muito simples do arquivo de classe que simplesmente mostra os nomes e tipos dos membros públicos da classe. Adicionando o -p opção incluirá membros privados:

$ javap -p RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Isso ainda não é muita informação. Para ver como os métodos são implementados no bytecode Java, vamos adicionar o -c opção:

$ javap -p -c RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); Code: 0: aload_0 1: getfield #13 // Field numSides:I 4: ireturn public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn public RegularPolygon(int); Code: 0: aload_0 1: iload_1 2: putfield #13 // Field numSides:I 5: aload_0 6: invokespecial #38 // Method java/lang/Object.'':()V 9: return }

Isso é um pouco mais interessante. No entanto, para realmente obter a história toda, devemos usar o -v ou -verbose opção, como em javap -p -v RegularPolygon.class:

O conteúdo completo de um arquivo de classe Java.

Aqui finalmente vemos o que realmente está no arquivo da classe. O que tudo isso significa? Vamos dar uma olhada em algumas das partes mais importantes.

Pool Constante

O ciclo de desenvolvimento para aplicativos C ++ inclui estágios de compilação e vinculação. O ciclo de desenvolvimento para Java ignora um estágio de ligação explícita porque a ligação acontece no tempo de execução. O arquivo de classe deve oferecer suporte a esse link de tempo de execução. Isso significa que quando o código-fonte se refere a qualquer campo ou método, o bytecode resultante deve manter as referências relevantes em forma simbólica, prontas para serem desreferenciadas assim que o aplicativo for carregado na memória e os endereços reais puderem ser resolvidos pelo vinculador em tempo de execução. Esta forma simbólica deve conter:

A especificação do formato do arquivo de classe inclui uma seção do arquivo chamada de piscina constante , uma tabela de todas as referências necessárias para o vinculador. Ele contém entradas de diferentes tipos.

// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...

O primeiro byte de cada entrada é uma etiqueta numérica que indica o tipo de entrada. Os bytes restantes fornecem informações sobre o valor da entrada. O número de bytes e as regras para sua interpretação dependem do tipo indicado pelo primeiro byte.

Por exemplo, uma classe Java que usa um número inteiro constante 365 pode ter uma entrada de pool constante com o seguinte código de bytes:

x03 00 00 01 6D

O primeiro byte, x03, identifica o tipo de entrada, CONSTANT_Integer. Isso informa ao vinculador que os próximos quatro bytes contêm o valor do inteiro. (Observe que 365 em hexadecimal é x16D). Se esta for a 14ª entrada no pool constante, javap -v irá renderizá-lo assim:

#14 = Integer 365

Muitos tipos de constantes são compostos de referências a tipos de constantes mais “primitivos” em outras partes do pool de constantes. Por exemplo, nosso código de exemplo contém a instrução:

println( 'Calculating perimeter...' )

O uso de uma constante de string produzirá duas entradas no pool de constantes: uma entrada com tipo CONSTANT_String e outra entrada do tipo CONSTANT_Utf8. A entrada do tipo Constant_UTF8 contém a representação UTF8 real do valor da string. A entrada do tipo CONSTANT_String contém uma referência ao CONSTANT_Utf8 entrada:

#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...

Essa complicação é necessária porque existem outros tipos de entradas de pool constantes que se referem a entradas do tipo Utf8 e que não são entradas do tipo String. Por exemplo, qualquer referência a um atributo de classe produzirá um CONSTANT_Fieldref tipo, que contém uma série de referências ao nome da classe, nome do atributo e tipo de atributo:

#1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #9 = Utf8 numSides #10 = Utf8 I #12 = NameAndType #9:#10 // numSides:I #13 = Fieldref #2.#12 // RegularPolygon.numSides:I

Para obter mais detalhes sobre o pool constante, consulte a documentação JVM .

Tabelas de campo e método

Um arquivo de classe contém um tabela de campo que contém informações sobre cada campo (ou seja, atributo) definido na classe. Estas são referências a entradas de pool constantes que descrevem o nome e tipo do campo, bem como sinalizadores de controle de acesso e outros dados relevantes.

Um similar tabela de métodos está presente no arquivo de classe. No entanto, além das informações de nome e tipo, para cada método não abstrato, ele contém as instruções de bytecode reais a serem executadas pela JVM, bem como estruturas de dados usadas pelo frame de pilha do método, descrito abaixo.

Bytecode JVM

A JVM usa seu próprio conjunto de instruções internas para executar o código compilado. Em execução javap com o -c opção inclui as implementações do método compilado na saída. Se examinarmos nosso RegularPolygon.class dessa forma, veremos a seguinte saída para o nosso getPerimeter() método:

public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn

O bytecode real pode ser parecido com isto:

xB2 00 17 x12 19 xB6 00 1D x27 ...

Cada instrução começa com um byte Código de operação identificação da instrução JVM, seguida de zero ou mais operandos de instrução a serem operados, dependendo do formato da instrução específica. Normalmente, são valores constantes ou referências ao conjunto de constantes. javap traduz o bytecode em uma forma legível exibindo:

Operandos que são exibidos com um sinal de libra, como #23, são referências a entradas no conjunto de constantes. Como podemos ver, javap também produz comentários úteis na saída, identificando o que exatamente está sendo referenciado no pool.

Discutiremos algumas das instruções comuns abaixo. Para obter informações detalhadas sobre o conjunto completo de instruções JVM, consulte o documentação .

Chamadas de método e pilha de chamadas

Cada chamada de método deve ser capaz de rodar com seu próprio contexto, que inclui coisas como variáveis ​​declaradas localmente ou argumentos que foram passados ​​para o método. Juntos, eles formam um empilhar quadro . Após a invocação de um método, um novo quadro é criado e colocado no topo do pilha de chamadas . Quando o método retorna, o quadro atual é removido da pilha de chamadas e descartado, e o quadro que estava em vigor antes da chamada do método é restaurado.

Um quadro de pilha inclui algumas estruturas distintas. Dois importantes são os pilha de operandos e a tabela de variável local , discutido a seguir.

A pilha de chamadas JVM.

Execução na pilha de operando

Muitas instruções JVM operam em seus quadros pilha de operandos . Em vez de especificar um operando constante explicitamente no bytecode, essas instruções tomam os valores no topo da pilha de operandos como entrada. Normalmente, esses valores são removidos da pilha no processo. Algumas instruções também colocam novos valores no topo da pilha. Desta forma, as instruções JVM podem ser combinadas para executar operações complexas. Por exemplo, a expressão:

sideLength * this.numSides

é compilado da seguinte forma em nosso getPerimeter() método:

8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul

As instruções JVM podem operar na pilha de operandos para realizar funções complexas.

Quando um método é chamado, uma nova pilha de operandos é criada como parte de sua estrutura de pilha, onde as operações serão realizadas. Devemos ter cuidado com a terminologia aqui: a palavra 'pilha' pode se referir ao pilha de chamadas , a pilha de quadros fornecendo contexto para a execução do método ou para um determinado quadro pilha de operandos , sobre o qual as instruções JVM operam.

Variáveis ​​Locais

Cada frame de pilha mantém uma tabela de variáveis ​​locais . Isso normalmente inclui uma referência a this objeto, quaisquer argumentos que foram passados ​​quando o método foi chamado e quaisquer variáveis ​​locais declaradas dentro do corpo do método. Em execução javap com o -v opção incluirá informações sobre como o quadro de pilha de cada método deve ser configurado, incluindo sua tabela de variável local:

public double getPerimeter(double); // ... Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... // ... LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LRegularPolygon; 0 16 1 sideLength D

Neste exemplo, existem duas variáveis ​​locais. A variável no slot 0 é denominada this, com o tipo RegularPolygon. Esta é a referência à própria classe do método. A variável no slot 1 é denominada sideLength, com o tipo D (indicando um duplo). Este é o argumento que é passado para o nosso getPerimeter() método.

Instruções como iload_1, fstore_2 ou aload [n] transferem diferentes tipos de variáveis ​​locais entre a pilha de operandos e a tabela de variáveis ​​locais. Como o primeiro item da tabela é geralmente a referência a this, a instrução aload_0 é comumente visto em qualquer método que opere em sua própria classe.

Isso conclui nosso passo a passo dos fundamentos da JVM. Clique aqui para retornar ao artigo principal.

:Z // unlock the thread 24: getstatic #31 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 27: pop 28: aload_1 29: monitorexit // get the value of field m4 and return it 30: aload_0 31: getfield #25 // Field m4:I 34: ireturn // ... public int m4(); Code: // check the flag for whether this field has already been set 0: aload_0 1: getfield #20 // Field bitmap

Suje Suas Mãos Com Bytecode Scala JVM



A linguagem Scala continuou a ganhar popularidade nos últimos anos, graças à sua excelente combinação de princípios de desenvolvimento de software funcional e orientado a objetos , e sua implementação em cima da comprovada Java Virtual Machine (JVM).

Apesar Escada compila para bytecode Java, é projetado para melhorar muitas das deficiências percebidas da linguagem Java. Oferecendo suporte de programação funcional completo, a sintaxe central do Scala contém muitas estruturas implícitas que devem ser construídas explicitamente por programadores Java, algumas envolvendo considerável complexidade.



A criação de uma linguagem que compila para bytecode Java requer um conhecimento profundo do funcionamento interno da Java Virtual Machine. Para apreciar o que os desenvolvedores do Scala realizaram, é necessário ir nos bastidores e explorar como o código-fonte do Scala é interpretado pelo compilador para produzir bytecode JVM eficiente e eficaz.



Vamos dar uma olhada em como tudo isso é implementado.



Pré-requisitos

Ler este artigo requer algum conhecimento básico do bytecode da Java Virtual Machine. A especificação completa da máquina virtual pode ser obtida em Documentação oficial da Oracle . Ler toda a especificação não é crítica para a compreensão deste artigo, então, para uma rápida introdução ao básico, preparei um pequeno guia no final do artigo.

Clique aqui para ler um curso intensivo sobre noções básicas de JVM.

Um utilitário é necessário para desmontar o bytecode Java para reproduzir os exemplos fornecidos abaixo e para prosseguir com uma investigação adicional. O Java Development Kit fornece seu próprio utilitário de linha de comando, javap, que usaremos aqui. Uma rápida demonstração de como javap trabalhos estão incluídos no guia na parte inferior .



E, claro, uma instalação funcional do compilador Scala é necessária para os leitores que desejam acompanhar os exemplos. Este artigo foi escrito usando Escala 2.11.7 . Diferentes versões do Scala podem produzir bytecode ligeiramente diferente.

Getters e setters padrão

Embora a convenção Java sempre forneça métodos getter e setter para atributos públicos, os programadores Java são obrigados a escrevê-los, apesar do fato de que o padrão para cada um não mudou em décadas. O Scala, em contraste, fornece getters e setters padrão.



Vejamos o seguinte exemplo:

class Person(val name:String) { }

Vamos dar uma olhada dentro da classe Person. Se compilarmos este arquivo com scalac, então executar $ javap -p Person.class nos dá:



Compiled from 'Person.scala' public class Person { private final java.lang.String name; // field public java.lang.String name(); // getter method public Person(java.lang.String); // constructor }

Podemos ver que para cada campo da classe Scala, um campo e seu método getter são gerados. O campo é privado e final, enquanto o método é público.

Se substituirmos val com var no Person fonte e recompilar, então o campo final o modificador é eliminado e o método setter também é adicionado:



Compiled from 'Person.scala' public class Person { private java.lang.String name; // field public java.lang.String name(); // getter method public void name_$eq(java.lang.String); // setter method public Person(java.lang.String); // constructor }

Se houver val ou var é definido dentro do corpo da classe, então o campo privado correspondente e os métodos de acesso são criados e inicializados apropriadamente na criação da instância.

Observe que tal implementação de nível de classe val e var campos significa que se algumas variáveis ​​são usadas no nível da classe para armazenar valores intermediários e nunca são acessadas diretamente pelo programador, a inicialização de cada um desses campos adicionará um ou dois métodos ao espaço da classe. Adicionando um private modificador para tais campos não significa que os acessadores correspondentes serão descartados. Eles apenas se tornarão privados.



Definições de variáveis ​​e funções

Vamos supor que temos um método, m(), e criamos três referências diferentes no estilo Scala para esta função:

class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }

Como cada uma dessas referências a m construído? Quando m ser executado em cada caso? Vamos dar uma olhada no bytecode resultante. A saída a seguir mostra os resultados de javap -v Person.class (omitindo muita saída supérflua):

Constant pool: #22 = Fieldref #2.#21 // Person.m1:I #24 = Fieldref #2.#23 // Person.m2:I #30 = Methodref #2.#29 // Person.m:()I #35 = Methodref #4.#34 // java/lang/Object.'':()V // ... public int m(); Code: // other methods refer to this method // ... public int m1(); Code: // get the value of field m1 and return it 0: aload_0 1: getfield #22 // Field m1:I 4: ireturn public int m2(); Code: // get the value of field m2 and return it 0: aload_0 1: getfield #24 // Field m2:I 4: ireturn public void m2_$eq(int); Code: // get the value of this method's input argument 0: aload_0 1: iload_1 // write it to the field m2 and return 2: putfield #24 // Field m2:I 5: return public int m3(); Code: // execute the instance method m(), and return 0: aload_0 1: invokevirtual #30 // Method m:()I 4: ireturn public Person(java.lang.String); Code: // instance constructor ... // execute the instance method m(), and write the result to field m1 9: aload_0 10: aload_0 11: invokevirtual #30 // Method m:()I 14: putfield #22 // Field m1:I // execute the instance method m(), and write the result to field m2 17: aload_0 18: aload_0 19: invokevirtual #30 // Method m:()I 22: putfield #24 // Field m2:I 25: return

No pool constante, vemos que a referência ao método m() é armazenado no índice #30. No código do construtor, vemos que esse método é chamado duas vezes durante a inicialização, com a instrução invokevirtual #30 aparecendo primeiro no deslocamento de byte 11 e, em seguida, no deslocamento 19. A primeira chamada é seguida pela instrução putfield #22 que atribui o resultado deste método ao campo m1, referenciado pelo índice #22 na piscina constante. A segunda chamada é seguida pelo mesmo padrão, desta vez atribuindo o valor ao campo m2, indexado em #24 na piscina constante.

Em outras palavras, atribuir um método a uma variável definida com val ou var apenas atribui o resultado do método para essa variável. Podemos ver que os métodos m1() e m2() que são criados são simplesmente getters para essas variáveis. No caso de var m2, também vemos que o setter m2_$eq(int) é criado, que se comporta como qualquer outro configurador, sobrescrevendo o valor no campo.

No entanto, usando a palavra-chave def dá um resultado diferente. Em vez de buscar um valor de campo para retornar, o método m3() também inclui a instrução invokevirtual #30. Ou seja, cada vez que esse método é chamado, ele chama m() e retorna o resultado desse método.

Portanto, como podemos ver, Scala fornece três maneiras de trabalhar com campos de classe, e elas são facilmente especificadas por meio das palavras-chave val, var e def. Em Java, teríamos que implementar os setters e getters necessários explicitamente, e esse código clichê escrito manualmente seria muito menos expressivo e mais sujeito a erros.

Valores preguiçosos

Um código mais complicado é produzido ao declarar um valor lento. Suponha que adicionamos o seguinte campo à classe definida anteriormente:

lazy val m4 = m

Em execução javap -p -v Person.class irá agora revelar o seguinte:

Constant pool: #20 = Fieldref #2.#19 // Person.bitmap$0:Z #23 = Methodref #2.#22 // Person.m:()I #25 = Fieldref #2.#24 // Person.m4:I #31 = Fieldref #27.#30 // scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; #48 = Methodref #2.#47 // Person.m4$lzycompute:()I // ... private volatile boolean bitmap$0; private int m4$lzycompute(); Code: // lock the thread 0: aload_0 1: dup 2: astore_1 3: monitorenter // check the flag for whether this field has already been set 4: aload_0 5: getfield #20 // Field bitmap$0:Z // if it has, skip to position 24 (unlock the thread and return) 8: ifne 24 // if it hasn't, execute the method m() 11: aload_0 12: aload_0 13: invokevirtual #23 // Method m:()I // write the method to the field m4 16: putfield #25 // Field m4:I // set the flag indicating the field has been set 19: aload_0 20: iconst_1 21: putfield #20 // Field bitmap$0:Z // unlock the thread 24: getstatic #31 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 27: pop 28: aload_1 29: monitorexit // get the value of field m4 and return it 30: aload_0 31: getfield #25 // Field m4:I 34: ireturn // ... public int m4(); Code: // check the flag for whether this field has already been set 0: aload_0 1: getfield #20 // Field bitmap$0:Z // if it hasn't, skip to position 14 (invoke lazy method and return) 4: ifeq 14 // if it has, get the value of field m4, then skip to position 18 (return) 7: aload_0 8: getfield #25 // Field m4:I 11: goto 18 // execute the method m4$lzycompute() to set the field 14: aload_0 15: invokespecial #48 // Method m4$lzycompute:()I // return 18: ireturn

Nesse caso, o valor do campo m4 não é calculado até que seja necessário. O método especial privado m4$lzycompute() é produzido para calcular o valor lento e o campo bitmap$0 para rastrear seu estado. Método m4() verifica se o valor deste campo é 0, indicando que m4 ainda não foi inicializado; nesse caso, m4$lzycompute() é invocado, populando m4 e retornando seu valor. Este método privado também define o valor de bitmap$0 para 1, de modo que da próxima vez m4() for chamado, ele ignorará a chamada do método de inicialização e, em vez disso, simplesmente retornará o valor de m4.

Os resultados da primeira chamada para um valor lento Scala.

O bytecode que Scala produz aqui é projetado para ser seguro para thread e eficaz. Para ser seguro para threads, o método de computação lenta usa o método monitorenter / monitorexit par de instruções. O método permanece efetivo, pois a sobrecarga de desempenho dessa sincronização ocorre apenas na primeira leitura do valor lento.

Apenas um bit é necessário para indicar o estado do valor lento. Portanto, se não houver mais do que 32 valores lazy, um único campo int pode rastrear todos eles. Se mais de um valor lento for definido no código-fonte, o bytecode acima será modificado pelo compilador para implementar uma máscara de bits para esse propósito.

Novamente, Scala nos permite tirar vantagem facilmente de um tipo específico de comportamento que deveria ser implementado explicitamente em Java, economizando esforços e reduzindo o risco de erros de digitação.

Função como valor

Agora vamos dar uma olhada no seguinte código-fonte do Scala:

class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }

O Printer classe tem um campo, output, com o tipo String => Unit: uma função que leva um String e retorna um objeto do tipo Unit (semelhante a void em Java). No método principal, criamos um desses objetos e atribuímos a esse campo uma função anônima que imprime uma determinada string.

Compilar este código gera quatro arquivos de classe:

O código-fonte é compilado em quatro arquivos de classe.

Hello.class é uma classe wrapper cujo método principal simplesmente chama Hello$.main():

public final class Hello // ... public static void main(java.lang.String[]); Code: 0: getstatic #16 // Field Hello$.MODULE$:LHello$; 3: aload_0 4: invokevirtual #18 // Method Hello$.main:([Ljava/lang/String;)V 7: return

O oculto Hello$.class contém a implementação real do método principal. Para dar uma olhada em seu bytecode, certifique-se de escapar corretamente de $ de acordo com as regras do seu shell de comando, para evitar sua interpretação como caractere especial:

public final class Hello$ // ... public void main(java.lang.String[]); Code: // initialize Printer and anonymous function 0: new #16 // class Printer 3: dup 4: new #18 // class Hello$$anonfun$1 7: dup 8: invokespecial #19 // Method Hello$$anonfun$1.'':()V 11: invokespecial #22 // Method Printer.'':(Lscala/Function1;)V 14: astore_2 // load the anonymous function onto the stack 15: aload_2 16: invokevirtual #26 // Method Printer.output:()Lscala/Function1; // execute the anonymous function, passing the string 'Hello' 19: ldc #28 // String Hello 21: invokeinterface #34, 2 // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object; // return 26: pop 27: return

O método cria um Printer. Em seguida, ele cria um Hello$$anonfun$1, que contém nossa função anônima s => println(s). O Printer é inicializado com este objeto como output campo. Este campo é então carregado na pilha e executado com o operando 'Hello'.

Vamos dar uma olhada na classe de função anônima, Hello$$anonfun$1.class, abaixo. Podemos ver que ele estende Function1 de Scala | (como AbstractFunction1) implementando apply() método. Na verdade, ele cria dois apply() métodos, um envolvendo o outro, que juntos executam a verificação de tipo (neste caso, se a entrada é um String), e executam a função anônima (imprimindo a entrada com println()).

public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 implements scala.Serializable // ... // Takes an argument of type String. Invoked second. public final void apply(java.lang.String); Code: // execute Scala's built-in method println(), passing the input argument 0: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: aload_1 4: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 7: return // Takes an argument of type Object. Invoked first. public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 // check that the input argument is a String (throws exception if not) 1: aload_1 2: checkcast #36 // class java/lang/String // invoke the method apply( String ), passing the input argument 5: invokevirtual #38 // Method apply:(Ljava/lang/String;)V // return the void type 8: getstatic #44 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 11: areturn

Olhando para trás, para Hello$.main() acima, podemos ver que, no deslocamento 21, a execução da função anônima é disparada por uma chamada a seu apply( Object ) método.

Finalmente, para completar, vamos dar uma olhada no bytecode para Printer.class:

public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return

Podemos ver que a função anônima aqui é tratada como qualquer val variável. Ele é armazenado no campo de classe output e no getter output() é criado. A única diferença é que essa variável deve agora implementar a interface Scala scala.Function1 (o que AbstractFunction1 faz).

Portanto, o custo desse recurso elegante do Scala são as classes de utilitário subjacentes, criadas para representar e executar uma única função anônima que pode ser usada como um valor. Você deve levar em consideração o número de tais funções, bem como os detalhes de sua implementação de VM, para descobrir o que isso significa para seu aplicativo específico.

Explorando os bastidores com Scala: Explore como essa linguagem poderosa é implementada no bytecode JVM. Tweet

Traços de Escala

As características do Scala são semelhantes às interfaces em Java. A característica a seguir define duas assinaturas de método e fornece uma implementação padrão da segunda. Vamos ver como isso é implementado:

trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }

O código-fonte é compilado em dois arquivos de classe.

Duas entidades são produzidas: Similarity.class, a interface declarando ambos os métodos, e a classe sintética, Similarity$class.class, fornecendo a implementação padrão:

public interface Similarity { public abstract boolean isSimilar(java.lang.Object); public abstract boolean isNotSimilar(java.lang.Object); } public abstract class Similarity$class public static boolean isNotSimilar(Similarity, java.lang.Object); Code: 0: aload_0 // execute the instance method isSimilar() 1: aload_1 2: invokeinterface #13, 2 // InterfaceMethod Similarity.isSimilar:(Ljava/lang/Object;)Z // if the returned value is 0, skip to position 14 (return with value 1) 7: ifeq 14 // otherwise, return with value 0 10: iconst_0 11: goto 15 // return the value 1 14: iconst_1 15: ireturn public static void $init$(Similarity); Code: 0: return

Quando uma classe implementa essa característica e chama o método isNotSimilar, o compilador Scala gera a instrução bytecode invokestatic para chamar o método estático fornecido pela classe que o acompanha.

Polimorfismo complexo e estruturas de herança podem ser criados a partir de características. Por exemplo, vários traços, bem como a classe de implementação, podem substituir um método com a mesma assinatura, chamando super.methodName() para passar o controle para a próxima característica. Quando o compilador Scala encontra essas chamadas, ele:

Portanto, podemos ver que o poderoso conceito de características é implementado no nível da JVM de uma forma que não leva a uma sobrecarga significativa, e os programadores de Scala podem aproveitar esse recurso sem se preocupar se ele será muito caro em tempo de execução.

Singletons

Scala fornece a definição explícita de classes singleton usando a palavra-chave object. Vamos considerar a seguinte classe única:

object Config { val home_dir = '/home/user' }

O compilador produz dois arquivos de classe:

O código-fonte é compilado em dois arquivos de classe.

Config.class é muito simples:

public final class Config public static java.lang.String home_dir(); Code: // execute the method Config$.home_dir() 0: getstatic #16 // Field Config$.MODULE$:LConfig$; 3: invokevirtual #18 // Method Config$.home_dir:()Ljava/lang/String; 6: areturn

Este é apenas um decorador para o material sintético Config$ classe que incorpora a funcionalidade do singleton. Examinando essa classe com javap -p -c produz o seguinte bytecode:

public final class Config$ public static final Config$ MODULE$; // a public reference to the singleton object private final java.lang.String home_dir; // static initializer public static {}; Code: 0: new #2 // class Config$ 3: invokespecial #12 // Method '':()V 6: return public java.lang.String home_dir(); Code: // get the value of field home_dir and return it 0: aload_0 1: getfield #17 // Field home_dir:Ljava/lang/String; 4: areturn private Config$(); Code: // initialize the object 0: aload_0 1: invokespecial #19 // Method java/lang/Object.'':()V // expose a public reference to this object in the synthetic variable MODULE$ 4: aload_0 5: putstatic #21 // Field MODULE$:LConfig$; // load the value '/home/user' and write it to the field home_dir 8: aload_0 9: ldc #23 // String /home/user 11: putfield #17 // Field home_dir:Ljava/lang/String; 14: return

Consiste no seguinte:

O singleton é um padrão de design popular e útil. A linguagem Java não fornece uma maneira direta de especificá-la no nível da linguagem; em vez disso, é responsabilidade do desenvolvedor implementá-lo no código-fonte Java. Scala, por outro lado, fornece uma maneira clara e conveniente de declarar um singleton explicitamente usando object palavra-chave. Como podemos ver olhando por baixo do capô, ele é implementado de forma acessível e natural.

Conclusão

Agora vimos como o Scala compila vários recursos de programação implícitos e funcionais em sofisticadas estruturas de bytecode Java. Com este vislumbre do funcionamento interno do Scala, podemos obter uma apreciação mais profunda do poder do Scala, ajudando-nos a obter o máximo desta linguagem poderosa.

Agora também temos as ferramentas para explorar a linguagem por conta própria. Existem muitos recursos úteis da sintaxe Scala que não são abordados neste artigo, como classes de caso, currying e compreensões de lista. Eu o encorajo a investigar a implementação dessas estruturas em Scala, para que você possa aprender como ser um ninja Scala de próximo nível!


A máquina virtual Java: um curso intensivo

Assim como o compilador Java, o compilador Scala converte o código-fonte em .class arquivos, contendo bytecode Java a ser executado pela Java Virtual Machine. Para entender como os dois idiomas diferem nos bastidores, é necessário entender o sistema que ambos visam. Aqui, apresentamos uma breve visão geral de alguns dos principais elementos da arquitetura da Java Virtual Machine, da estrutura do arquivo de classe e dos fundamentos do assembler.

Observe que este guia cobrirá apenas o mínimo para permitir o acompanhamento do artigo acima. Embora muitos dos principais componentes da JVM não sejam discutidos aqui, detalhes completos podem ser encontrados nos documentos oficiais, Aqui .

Decompilando arquivos de classe com javap
Pool Constante
Tabelas de campo e método
Bytecode JVM
Chamadas de método e pilha de chamadas
Execução na pilha de operando
Variáveis ​​Locais
Voltar ao topo

Decompilando arquivos de classe com javap

Java é fornecido com javap utilitário de linha de comando, que descompila .class arquivos em um formato legível por humanos. Como os arquivos de classe Scala e Java têm como destino a mesma JVM, javap pode ser usado para examinar arquivos de classe compilados pelo Scala.

Vamos compilar o seguinte código-fonte:

// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }

Compilando isso com scalac RegularPolygon.scala irá produzir RegularPolygon.class. Se executarmos javap RegularPolygon.class veremos o seguinte:

$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Esta é uma divisão muito simples do arquivo de classe que simplesmente mostra os nomes e tipos dos membros públicos da classe. Adicionando o -p opção incluirá membros privados:

$ javap -p RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Isso ainda não é muita informação. Para ver como os métodos são implementados no bytecode Java, vamos adicionar o -c opção:

$ javap -p -c RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); Code: 0: aload_0 1: getfield #13 // Field numSides:I 4: ireturn public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn public RegularPolygon(int); Code: 0: aload_0 1: iload_1 2: putfield #13 // Field numSides:I 5: aload_0 6: invokespecial #38 // Method java/lang/Object.'':()V 9: return }

Isso é um pouco mais interessante. No entanto, para realmente obter a história toda, devemos usar o -v ou -verbose opção, como em javap -p -v RegularPolygon.class:

O conteúdo completo de um arquivo de classe Java.

Aqui finalmente vemos o que realmente está no arquivo da classe. O que tudo isso significa? Vamos dar uma olhada em algumas das partes mais importantes.

Pool Constante

O ciclo de desenvolvimento para aplicativos C ++ inclui estágios de compilação e vinculação. O ciclo de desenvolvimento para Java ignora um estágio de ligação explícita porque a ligação acontece no tempo de execução. O arquivo de classe deve oferecer suporte a esse link de tempo de execução. Isso significa que quando o código-fonte se refere a qualquer campo ou método, o bytecode resultante deve manter as referências relevantes em forma simbólica, prontas para serem desreferenciadas assim que o aplicativo for carregado na memória e os endereços reais puderem ser resolvidos pelo vinculador em tempo de execução. Esta forma simbólica deve conter:

A especificação do formato do arquivo de classe inclui uma seção do arquivo chamada de piscina constante , uma tabela de todas as referências necessárias para o vinculador. Ele contém entradas de diferentes tipos.

// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...

O primeiro byte de cada entrada é uma etiqueta numérica que indica o tipo de entrada. Os bytes restantes fornecem informações sobre o valor da entrada. O número de bytes e as regras para sua interpretação dependem do tipo indicado pelo primeiro byte.

Por exemplo, uma classe Java que usa um número inteiro constante 365 pode ter uma entrada de pool constante com o seguinte código de bytes:

x03 00 00 01 6D

O primeiro byte, x03, identifica o tipo de entrada, CONSTANT_Integer. Isso informa ao vinculador que os próximos quatro bytes contêm o valor do inteiro. (Observe que 365 em hexadecimal é x16D). Se esta for a 14ª entrada no pool constante, javap -v irá renderizá-lo assim:

#14 = Integer 365

Muitos tipos de constantes são compostos de referências a tipos de constantes mais “primitivos” em outras partes do pool de constantes. Por exemplo, nosso código de exemplo contém a instrução:

println( 'Calculating perimeter...' )

O uso de uma constante de string produzirá duas entradas no pool de constantes: uma entrada com tipo CONSTANT_String e outra entrada do tipo CONSTANT_Utf8. A entrada do tipo Constant_UTF8 contém a representação UTF8 real do valor da string. A entrada do tipo CONSTANT_String contém uma referência ao CONSTANT_Utf8 entrada:

#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...

Essa complicação é necessária porque existem outros tipos de entradas de pool constantes que se referem a entradas do tipo Utf8 e que não são entradas do tipo String. Por exemplo, qualquer referência a um atributo de classe produzirá um CONSTANT_Fieldref tipo, que contém uma série de referências ao nome da classe, nome do atributo e tipo de atributo:

#1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #9 = Utf8 numSides #10 = Utf8 I #12 = NameAndType #9:#10 // numSides:I #13 = Fieldref #2.#12 // RegularPolygon.numSides:I

Para obter mais detalhes sobre o pool constante, consulte a documentação JVM .

Tabelas de campo e método

Um arquivo de classe contém um tabela de campo que contém informações sobre cada campo (ou seja, atributo) definido na classe. Estas são referências a entradas de pool constantes que descrevem o nome e tipo do campo, bem como sinalizadores de controle de acesso e outros dados relevantes.

Um similar tabela de métodos está presente no arquivo de classe. No entanto, além das informações de nome e tipo, para cada método não abstrato, ele contém as instruções de bytecode reais a serem executadas pela JVM, bem como estruturas de dados usadas pelo frame de pilha do método, descrito abaixo.

Bytecode JVM

A JVM usa seu próprio conjunto de instruções internas para executar o código compilado. Em execução javap com o -c opção inclui as implementações do método compilado na saída. Se examinarmos nosso RegularPolygon.class dessa forma, veremos a seguinte saída para o nosso getPerimeter() método:

public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn

O bytecode real pode ser parecido com isto:

xB2 00 17 x12 19 xB6 00 1D x27 ...

Cada instrução começa com um byte Código de operação identificação da instrução JVM, seguida de zero ou mais operandos de instrução a serem operados, dependendo do formato da instrução específica. Normalmente, são valores constantes ou referências ao conjunto de constantes. javap traduz o bytecode em uma forma legível exibindo:

Operandos que são exibidos com um sinal de libra, como #23, são referências a entradas no conjunto de constantes. Como podemos ver, javap também produz comentários úteis na saída, identificando o que exatamente está sendo referenciado no pool.

Discutiremos algumas das instruções comuns abaixo. Para obter informações detalhadas sobre o conjunto completo de instruções JVM, consulte o documentação .

Chamadas de método e pilha de chamadas

Cada chamada de método deve ser capaz de rodar com seu próprio contexto, que inclui coisas como variáveis ​​declaradas localmente ou argumentos que foram passados ​​para o método. Juntos, eles formam um empilhar quadro . Após a invocação de um método, um novo quadro é criado e colocado no topo do pilha de chamadas . Quando o método retorna, o quadro atual é removido da pilha de chamadas e descartado, e o quadro que estava em vigor antes da chamada do método é restaurado.

Um quadro de pilha inclui algumas estruturas distintas. Dois importantes são os pilha de operandos e a tabela de variável local , discutido a seguir.

A pilha de chamadas JVM.

Execução na pilha de operando

Muitas instruções JVM operam em seus quadros pilha de operandos . Em vez de especificar um operando constante explicitamente no bytecode, essas instruções tomam os valores no topo da pilha de operandos como entrada. Normalmente, esses valores são removidos da pilha no processo. Algumas instruções também colocam novos valores no topo da pilha. Desta forma, as instruções JVM podem ser combinadas para executar operações complexas. Por exemplo, a expressão:

sideLength * this.numSides

é compilado da seguinte forma em nosso getPerimeter() método:

8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul

As instruções JVM podem operar na pilha de operandos para realizar funções complexas.

Quando um método é chamado, uma nova pilha de operandos é criada como parte de sua estrutura de pilha, onde as operações serão realizadas. Devemos ter cuidado com a terminologia aqui: a palavra 'pilha' pode se referir ao pilha de chamadas , a pilha de quadros fornecendo contexto para a execução do método ou para um determinado quadro pilha de operandos , sobre o qual as instruções JVM operam.

Variáveis ​​Locais

Cada frame de pilha mantém uma tabela de variáveis ​​locais . Isso normalmente inclui uma referência a this objeto, quaisquer argumentos que foram passados ​​quando o método foi chamado e quaisquer variáveis ​​locais declaradas dentro do corpo do método. Em execução javap com o -v opção incluirá informações sobre como o quadro de pilha de cada método deve ser configurado, incluindo sua tabela de variável local:

public double getPerimeter(double); // ... Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... // ... LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LRegularPolygon; 0 16 1 sideLength D

Neste exemplo, existem duas variáveis ​​locais. A variável no slot 0 é denominada this, com o tipo RegularPolygon. Esta é a referência à própria classe do método. A variável no slot 1 é denominada sideLength, com o tipo D (indicando um duplo). Este é o argumento que é passado para o nosso getPerimeter() método.

Instruções como iload_1, fstore_2 ou aload [n] transferem diferentes tipos de variáveis ​​locais entre a pilha de operandos e a tabela de variáveis ​​locais. Como o primeiro item da tabela é geralmente a referência a this, a instrução aload_0 é comumente visto em qualquer método que opere em sua própria classe.

Isso conclui nosso passo a passo dos fundamentos da JVM. Clique aqui para retornar ao artigo principal.

:Z // if it hasn't, skip to position 14 (invoke lazy method and return) 4: ifeq 14 // if it has, get the value of field m4, then skip to position 18 (return) 7: aload_0 8: getfield #25 // Field m4:I 11: goto 18 // execute the method m4$lzycompute() to set the field 14: aload_0 15: invokespecial #48 // Method m4$lzycompute:()I // return 18: ireturn

Nesse caso, o valor do campo m4 não é calculado até que seja necessário. O método especial privado m4$lzycompute() é produzido para calcular o valor lento e o campo bitmap

Suje Suas Mãos Com Bytecode Scala JVM



A linguagem Scala continuou a ganhar popularidade nos últimos anos, graças à sua excelente combinação de princípios de desenvolvimento de software funcional e orientado a objetos , e sua implementação em cima da comprovada Java Virtual Machine (JVM).

Apesar Escada compila para bytecode Java, é projetado para melhorar muitas das deficiências percebidas da linguagem Java. Oferecendo suporte de programação funcional completo, a sintaxe central do Scala contém muitas estruturas implícitas que devem ser construídas explicitamente por programadores Java, algumas envolvendo considerável complexidade.



A criação de uma linguagem que compila para bytecode Java requer um conhecimento profundo do funcionamento interno da Java Virtual Machine. Para apreciar o que os desenvolvedores do Scala realizaram, é necessário ir nos bastidores e explorar como o código-fonte do Scala é interpretado pelo compilador para produzir bytecode JVM eficiente e eficaz.



Vamos dar uma olhada em como tudo isso é implementado.



Pré-requisitos

Ler este artigo requer algum conhecimento básico do bytecode da Java Virtual Machine. A especificação completa da máquina virtual pode ser obtida em Documentação oficial da Oracle . Ler toda a especificação não é crítica para a compreensão deste artigo, então, para uma rápida introdução ao básico, preparei um pequeno guia no final do artigo.

Clique aqui para ler um curso intensivo sobre noções básicas de JVM.

Um utilitário é necessário para desmontar o bytecode Java para reproduzir os exemplos fornecidos abaixo e para prosseguir com uma investigação adicional. O Java Development Kit fornece seu próprio utilitário de linha de comando, javap, que usaremos aqui. Uma rápida demonstração de como javap trabalhos estão incluídos no guia na parte inferior .



E, claro, uma instalação funcional do compilador Scala é necessária para os leitores que desejam acompanhar os exemplos. Este artigo foi escrito usando Escala 2.11.7 . Diferentes versões do Scala podem produzir bytecode ligeiramente diferente.

Getters e setters padrão

Embora a convenção Java sempre forneça métodos getter e setter para atributos públicos, os programadores Java são obrigados a escrevê-los, apesar do fato de que o padrão para cada um não mudou em décadas. O Scala, em contraste, fornece getters e setters padrão.



Vejamos o seguinte exemplo:

class Person(val name:String) { }

Vamos dar uma olhada dentro da classe Person. Se compilarmos este arquivo com scalac, então executar $ javap -p Person.class nos dá:



Compiled from 'Person.scala' public class Person { private final java.lang.String name; // field public java.lang.String name(); // getter method public Person(java.lang.String); // constructor }

Podemos ver que para cada campo da classe Scala, um campo e seu método getter são gerados. O campo é privado e final, enquanto o método é público.

Se substituirmos val com var no Person fonte e recompilar, então o campo final o modificador é eliminado e o método setter também é adicionado:



Compiled from 'Person.scala' public class Person { private java.lang.String name; // field public java.lang.String name(); // getter method public void name_$eq(java.lang.String); // setter method public Person(java.lang.String); // constructor }

Se houver val ou var é definido dentro do corpo da classe, então o campo privado correspondente e os métodos de acesso são criados e inicializados apropriadamente na criação da instância.

Observe que tal implementação de nível de classe val e var campos significa que se algumas variáveis ​​são usadas no nível da classe para armazenar valores intermediários e nunca são acessadas diretamente pelo programador, a inicialização de cada um desses campos adicionará um ou dois métodos ao espaço da classe. Adicionando um private modificador para tais campos não significa que os acessadores correspondentes serão descartados. Eles apenas se tornarão privados.



Definições de variáveis ​​e funções

Vamos supor que temos um método, m(), e criamos três referências diferentes no estilo Scala para esta função:

class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }

Como cada uma dessas referências a m construído? Quando m ser executado em cada caso? Vamos dar uma olhada no bytecode resultante. A saída a seguir mostra os resultados de javap -v Person.class (omitindo muita saída supérflua):

Constant pool: #22 = Fieldref #2.#21 // Person.m1:I #24 = Fieldref #2.#23 // Person.m2:I #30 = Methodref #2.#29 // Person.m:()I #35 = Methodref #4.#34 // java/lang/Object.'':()V // ... public int m(); Code: // other methods refer to this method // ... public int m1(); Code: // get the value of field m1 and return it 0: aload_0 1: getfield #22 // Field m1:I 4: ireturn public int m2(); Code: // get the value of field m2 and return it 0: aload_0 1: getfield #24 // Field m2:I 4: ireturn public void m2_$eq(int); Code: // get the value of this method's input argument 0: aload_0 1: iload_1 // write it to the field m2 and return 2: putfield #24 // Field m2:I 5: return public int m3(); Code: // execute the instance method m(), and return 0: aload_0 1: invokevirtual #30 // Method m:()I 4: ireturn public Person(java.lang.String); Code: // instance constructor ... // execute the instance method m(), and write the result to field m1 9: aload_0 10: aload_0 11: invokevirtual #30 // Method m:()I 14: putfield #22 // Field m1:I // execute the instance method m(), and write the result to field m2 17: aload_0 18: aload_0 19: invokevirtual #30 // Method m:()I 22: putfield #24 // Field m2:I 25: return

No pool constante, vemos que a referência ao método m() é armazenado no índice #30. No código do construtor, vemos que esse método é chamado duas vezes durante a inicialização, com a instrução invokevirtual #30 aparecendo primeiro no deslocamento de byte 11 e, em seguida, no deslocamento 19. A primeira chamada é seguida pela instrução putfield #22 que atribui o resultado deste método ao campo m1, referenciado pelo índice #22 na piscina constante. A segunda chamada é seguida pelo mesmo padrão, desta vez atribuindo o valor ao campo m2, indexado em #24 na piscina constante.

Em outras palavras, atribuir um método a uma variável definida com val ou var apenas atribui o resultado do método para essa variável. Podemos ver que os métodos m1() e m2() que são criados são simplesmente getters para essas variáveis. No caso de var m2, também vemos que o setter m2_$eq(int) é criado, que se comporta como qualquer outro configurador, sobrescrevendo o valor no campo.

No entanto, usando a palavra-chave def dá um resultado diferente. Em vez de buscar um valor de campo para retornar, o método m3() também inclui a instrução invokevirtual #30. Ou seja, cada vez que esse método é chamado, ele chama m() e retorna o resultado desse método.

Portanto, como podemos ver, Scala fornece três maneiras de trabalhar com campos de classe, e elas são facilmente especificadas por meio das palavras-chave val, var e def. Em Java, teríamos que implementar os setters e getters necessários explicitamente, e esse código clichê escrito manualmente seria muito menos expressivo e mais sujeito a erros.

Valores preguiçosos

Um código mais complicado é produzido ao declarar um valor lento. Suponha que adicionamos o seguinte campo à classe definida anteriormente:

lazy val m4 = m

Em execução javap -p -v Person.class irá agora revelar o seguinte:

Constant pool: #20 = Fieldref #2.#19 // Person.bitmap$0:Z #23 = Methodref #2.#22 // Person.m:()I #25 = Fieldref #2.#24 // Person.m4:I #31 = Fieldref #27.#30 // scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; #48 = Methodref #2.#47 // Person.m4$lzycompute:()I // ... private volatile boolean bitmap$0; private int m4$lzycompute(); Code: // lock the thread 0: aload_0 1: dup 2: astore_1 3: monitorenter // check the flag for whether this field has already been set 4: aload_0 5: getfield #20 // Field bitmap$0:Z // if it has, skip to position 24 (unlock the thread and return) 8: ifne 24 // if it hasn't, execute the method m() 11: aload_0 12: aload_0 13: invokevirtual #23 // Method m:()I // write the method to the field m4 16: putfield #25 // Field m4:I // set the flag indicating the field has been set 19: aload_0 20: iconst_1 21: putfield #20 // Field bitmap$0:Z // unlock the thread 24: getstatic #31 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 27: pop 28: aload_1 29: monitorexit // get the value of field m4 and return it 30: aload_0 31: getfield #25 // Field m4:I 34: ireturn // ... public int m4(); Code: // check the flag for whether this field has already been set 0: aload_0 1: getfield #20 // Field bitmap$0:Z // if it hasn't, skip to position 14 (invoke lazy method and return) 4: ifeq 14 // if it has, get the value of field m4, then skip to position 18 (return) 7: aload_0 8: getfield #25 // Field m4:I 11: goto 18 // execute the method m4$lzycompute() to set the field 14: aload_0 15: invokespecial #48 // Method m4$lzycompute:()I // return 18: ireturn

Nesse caso, o valor do campo m4 não é calculado até que seja necessário. O método especial privado m4$lzycompute() é produzido para calcular o valor lento e o campo bitmap$0 para rastrear seu estado. Método m4() verifica se o valor deste campo é 0, indicando que m4 ainda não foi inicializado; nesse caso, m4$lzycompute() é invocado, populando m4 e retornando seu valor. Este método privado também define o valor de bitmap$0 para 1, de modo que da próxima vez m4() for chamado, ele ignorará a chamada do método de inicialização e, em vez disso, simplesmente retornará o valor de m4.

Os resultados da primeira chamada para um valor lento Scala.

O bytecode que Scala produz aqui é projetado para ser seguro para thread e eficaz. Para ser seguro para threads, o método de computação lenta usa o método monitorenter / monitorexit par de instruções. O método permanece efetivo, pois a sobrecarga de desempenho dessa sincronização ocorre apenas na primeira leitura do valor lento.

Apenas um bit é necessário para indicar o estado do valor lento. Portanto, se não houver mais do que 32 valores lazy, um único campo int pode rastrear todos eles. Se mais de um valor lento for definido no código-fonte, o bytecode acima será modificado pelo compilador para implementar uma máscara de bits para esse propósito.

Novamente, Scala nos permite tirar vantagem facilmente de um tipo específico de comportamento que deveria ser implementado explicitamente em Java, economizando esforços e reduzindo o risco de erros de digitação.

Função como valor

Agora vamos dar uma olhada no seguinte código-fonte do Scala:

class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }

O Printer classe tem um campo, output, com o tipo String => Unit: uma função que leva um String e retorna um objeto do tipo Unit (semelhante a void em Java). No método principal, criamos um desses objetos e atribuímos a esse campo uma função anônima que imprime uma determinada string.

Compilar este código gera quatro arquivos de classe:

O código-fonte é compilado em quatro arquivos de classe.

Hello.class é uma classe wrapper cujo método principal simplesmente chama Hello$.main():

public final class Hello // ... public static void main(java.lang.String[]); Code: 0: getstatic #16 // Field Hello$.MODULE$:LHello$; 3: aload_0 4: invokevirtual #18 // Method Hello$.main:([Ljava/lang/String;)V 7: return

O oculto Hello$.class contém a implementação real do método principal. Para dar uma olhada em seu bytecode, certifique-se de escapar corretamente de $ de acordo com as regras do seu shell de comando, para evitar sua interpretação como caractere especial:

public final class Hello$ // ... public void main(java.lang.String[]); Code: // initialize Printer and anonymous function 0: new #16 // class Printer 3: dup 4: new #18 // class Hello$$anonfun$1 7: dup 8: invokespecial #19 // Method Hello$$anonfun$1.'':()V 11: invokespecial #22 // Method Printer.'':(Lscala/Function1;)V 14: astore_2 // load the anonymous function onto the stack 15: aload_2 16: invokevirtual #26 // Method Printer.output:()Lscala/Function1; // execute the anonymous function, passing the string 'Hello' 19: ldc #28 // String Hello 21: invokeinterface #34, 2 // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object; // return 26: pop 27: return

O método cria um Printer. Em seguida, ele cria um Hello$$anonfun$1, que contém nossa função anônima s => println(s). O Printer é inicializado com este objeto como output campo. Este campo é então carregado na pilha e executado com o operando 'Hello'.

Vamos dar uma olhada na classe de função anônima, Hello$$anonfun$1.class, abaixo. Podemos ver que ele estende Function1 de Scala | (como AbstractFunction1) implementando apply() método. Na verdade, ele cria dois apply() métodos, um envolvendo o outro, que juntos executam a verificação de tipo (neste caso, se a entrada é um String), e executam a função anônima (imprimindo a entrada com println()).

public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 implements scala.Serializable // ... // Takes an argument of type String. Invoked second. public final void apply(java.lang.String); Code: // execute Scala's built-in method println(), passing the input argument 0: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: aload_1 4: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 7: return // Takes an argument of type Object. Invoked first. public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 // check that the input argument is a String (throws exception if not) 1: aload_1 2: checkcast #36 // class java/lang/String // invoke the method apply( String ), passing the input argument 5: invokevirtual #38 // Method apply:(Ljava/lang/String;)V // return the void type 8: getstatic #44 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 11: areturn

Olhando para trás, para Hello$.main() acima, podemos ver que, no deslocamento 21, a execução da função anônima é disparada por uma chamada a seu apply( Object ) método.

Finalmente, para completar, vamos dar uma olhada no bytecode para Printer.class:

public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return

Podemos ver que a função anônima aqui é tratada como qualquer val variável. Ele é armazenado no campo de classe output e no getter output() é criado. A única diferença é que essa variável deve agora implementar a interface Scala scala.Function1 (o que AbstractFunction1 faz).

Portanto, o custo desse recurso elegante do Scala são as classes de utilitário subjacentes, criadas para representar e executar uma única função anônima que pode ser usada como um valor. Você deve levar em consideração o número de tais funções, bem como os detalhes de sua implementação de VM, para descobrir o que isso significa para seu aplicativo específico.

Explorando os bastidores com Scala: Explore como essa linguagem poderosa é implementada no bytecode JVM. Tweet

Traços de Escala

As características do Scala são semelhantes às interfaces em Java. A característica a seguir define duas assinaturas de método e fornece uma implementação padrão da segunda. Vamos ver como isso é implementado:

trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }

O código-fonte é compilado em dois arquivos de classe.

Duas entidades são produzidas: Similarity.class, a interface declarando ambos os métodos, e a classe sintética, Similarity$class.class, fornecendo a implementação padrão:

public interface Similarity { public abstract boolean isSimilar(java.lang.Object); public abstract boolean isNotSimilar(java.lang.Object); } public abstract class Similarity$class public static boolean isNotSimilar(Similarity, java.lang.Object); Code: 0: aload_0 // execute the instance method isSimilar() 1: aload_1 2: invokeinterface #13, 2 // InterfaceMethod Similarity.isSimilar:(Ljava/lang/Object;)Z // if the returned value is 0, skip to position 14 (return with value 1) 7: ifeq 14 // otherwise, return with value 0 10: iconst_0 11: goto 15 // return the value 1 14: iconst_1 15: ireturn public static void $init$(Similarity); Code: 0: return

Quando uma classe implementa essa característica e chama o método isNotSimilar, o compilador Scala gera a instrução bytecode invokestatic para chamar o método estático fornecido pela classe que o acompanha.

Polimorfismo complexo e estruturas de herança podem ser criados a partir de características. Por exemplo, vários traços, bem como a classe de implementação, podem substituir um método com a mesma assinatura, chamando super.methodName() para passar o controle para a próxima característica. Quando o compilador Scala encontra essas chamadas, ele:

Portanto, podemos ver que o poderoso conceito de características é implementado no nível da JVM de uma forma que não leva a uma sobrecarga significativa, e os programadores de Scala podem aproveitar esse recurso sem se preocupar se ele será muito caro em tempo de execução.

Singletons

Scala fornece a definição explícita de classes singleton usando a palavra-chave object. Vamos considerar a seguinte classe única:

object Config { val home_dir = '/home/user' }

O compilador produz dois arquivos de classe:

O código-fonte é compilado em dois arquivos de classe.

Config.class é muito simples:

public final class Config public static java.lang.String home_dir(); Code: // execute the method Config$.home_dir() 0: getstatic #16 // Field Config$.MODULE$:LConfig$; 3: invokevirtual #18 // Method Config$.home_dir:()Ljava/lang/String; 6: areturn

Este é apenas um decorador para o material sintético Config$ classe que incorpora a funcionalidade do singleton. Examinando essa classe com javap -p -c produz o seguinte bytecode:

public final class Config$ public static final Config$ MODULE$; // a public reference to the singleton object private final java.lang.String home_dir; // static initializer public static {}; Code: 0: new #2 // class Config$ 3: invokespecial #12 // Method '':()V 6: return public java.lang.String home_dir(); Code: // get the value of field home_dir and return it 0: aload_0 1: getfield #17 // Field home_dir:Ljava/lang/String; 4: areturn private Config$(); Code: // initialize the object 0: aload_0 1: invokespecial #19 // Method java/lang/Object.'':()V // expose a public reference to this object in the synthetic variable MODULE$ 4: aload_0 5: putstatic #21 // Field MODULE$:LConfig$; // load the value '/home/user' and write it to the field home_dir 8: aload_0 9: ldc #23 // String /home/user 11: putfield #17 // Field home_dir:Ljava/lang/String; 14: return

Consiste no seguinte:

O singleton é um padrão de design popular e útil. A linguagem Java não fornece uma maneira direta de especificá-la no nível da linguagem; em vez disso, é responsabilidade do desenvolvedor implementá-lo no código-fonte Java. Scala, por outro lado, fornece uma maneira clara e conveniente de declarar um singleton explicitamente usando object palavra-chave. Como podemos ver olhando por baixo do capô, ele é implementado de forma acessível e natural.

Conclusão

Agora vimos como o Scala compila vários recursos de programação implícitos e funcionais em sofisticadas estruturas de bytecode Java. Com este vislumbre do funcionamento interno do Scala, podemos obter uma apreciação mais profunda do poder do Scala, ajudando-nos a obter o máximo desta linguagem poderosa.

Agora também temos as ferramentas para explorar a linguagem por conta própria. Existem muitos recursos úteis da sintaxe Scala que não são abordados neste artigo, como classes de caso, currying e compreensões de lista. Eu o encorajo a investigar a implementação dessas estruturas em Scala, para que você possa aprender como ser um ninja Scala de próximo nível!


A máquina virtual Java: um curso intensivo

Assim como o compilador Java, o compilador Scala converte o código-fonte em .class arquivos, contendo bytecode Java a ser executado pela Java Virtual Machine. Para entender como os dois idiomas diferem nos bastidores, é necessário entender o sistema que ambos visam. Aqui, apresentamos uma breve visão geral de alguns dos principais elementos da arquitetura da Java Virtual Machine, da estrutura do arquivo de classe e dos fundamentos do assembler.

Observe que este guia cobrirá apenas o mínimo para permitir o acompanhamento do artigo acima. Embora muitos dos principais componentes da JVM não sejam discutidos aqui, detalhes completos podem ser encontrados nos documentos oficiais, Aqui .

Decompilando arquivos de classe com javap
Pool Constante
Tabelas de campo e método
Bytecode JVM
Chamadas de método e pilha de chamadas
Execução na pilha de operando
Variáveis ​​Locais
Voltar ao topo

Decompilando arquivos de classe com javap

Java é fornecido com javap utilitário de linha de comando, que descompila .class arquivos em um formato legível por humanos. Como os arquivos de classe Scala e Java têm como destino a mesma JVM, javap pode ser usado para examinar arquivos de classe compilados pelo Scala.

Vamos compilar o seguinte código-fonte:

// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }

Compilando isso com scalac RegularPolygon.scala irá produzir RegularPolygon.class. Se executarmos javap RegularPolygon.class veremos o seguinte:

$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Esta é uma divisão muito simples do arquivo de classe que simplesmente mostra os nomes e tipos dos membros públicos da classe. Adicionando o -p opção incluirá membros privados:

$ javap -p RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Isso ainda não é muita informação. Para ver como os métodos são implementados no bytecode Java, vamos adicionar o -c opção:

$ javap -p -c RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); Code: 0: aload_0 1: getfield #13 // Field numSides:I 4: ireturn public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn public RegularPolygon(int); Code: 0: aload_0 1: iload_1 2: putfield #13 // Field numSides:I 5: aload_0 6: invokespecial #38 // Method java/lang/Object.'':()V 9: return }

Isso é um pouco mais interessante. No entanto, para realmente obter a história toda, devemos usar o -v ou -verbose opção, como em javap -p -v RegularPolygon.class:

O conteúdo completo de um arquivo de classe Java.

Aqui finalmente vemos o que realmente está no arquivo da classe. O que tudo isso significa? Vamos dar uma olhada em algumas das partes mais importantes.

Pool Constante

O ciclo de desenvolvimento para aplicativos C ++ inclui estágios de compilação e vinculação. O ciclo de desenvolvimento para Java ignora um estágio de ligação explícita porque a ligação acontece no tempo de execução. O arquivo de classe deve oferecer suporte a esse link de tempo de execução. Isso significa que quando o código-fonte se refere a qualquer campo ou método, o bytecode resultante deve manter as referências relevantes em forma simbólica, prontas para serem desreferenciadas assim que o aplicativo for carregado na memória e os endereços reais puderem ser resolvidos pelo vinculador em tempo de execução. Esta forma simbólica deve conter:

A especificação do formato do arquivo de classe inclui uma seção do arquivo chamada de piscina constante , uma tabela de todas as referências necessárias para o vinculador. Ele contém entradas de diferentes tipos.

// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...

O primeiro byte de cada entrada é uma etiqueta numérica que indica o tipo de entrada. Os bytes restantes fornecem informações sobre o valor da entrada. O número de bytes e as regras para sua interpretação dependem do tipo indicado pelo primeiro byte.

Por exemplo, uma classe Java que usa um número inteiro constante 365 pode ter uma entrada de pool constante com o seguinte código de bytes:

x03 00 00 01 6D

O primeiro byte, x03, identifica o tipo de entrada, CONSTANT_Integer. Isso informa ao vinculador que os próximos quatro bytes contêm o valor do inteiro. (Observe que 365 em hexadecimal é x16D). Se esta for a 14ª entrada no pool constante, javap -v irá renderizá-lo assim:

#14 = Integer 365

Muitos tipos de constantes são compostos de referências a tipos de constantes mais “primitivos” em outras partes do pool de constantes. Por exemplo, nosso código de exemplo contém a instrução:

println( 'Calculating perimeter...' )

O uso de uma constante de string produzirá duas entradas no pool de constantes: uma entrada com tipo CONSTANT_String e outra entrada do tipo CONSTANT_Utf8. A entrada do tipo Constant_UTF8 contém a representação UTF8 real do valor da string. A entrada do tipo CONSTANT_String contém uma referência ao CONSTANT_Utf8 entrada:

#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...

Essa complicação é necessária porque existem outros tipos de entradas de pool constantes que se referem a entradas do tipo Utf8 e que não são entradas do tipo String. Por exemplo, qualquer referência a um atributo de classe produzirá um CONSTANT_Fieldref tipo, que contém uma série de referências ao nome da classe, nome do atributo e tipo de atributo:

#1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #9 = Utf8 numSides #10 = Utf8 I #12 = NameAndType #9:#10 // numSides:I #13 = Fieldref #2.#12 // RegularPolygon.numSides:I

Para obter mais detalhes sobre o pool constante, consulte a documentação JVM .

Tabelas de campo e método

Um arquivo de classe contém um tabela de campo que contém informações sobre cada campo (ou seja, atributo) definido na classe. Estas são referências a entradas de pool constantes que descrevem o nome e tipo do campo, bem como sinalizadores de controle de acesso e outros dados relevantes.

Um similar tabela de métodos está presente no arquivo de classe. No entanto, além das informações de nome e tipo, para cada método não abstrato, ele contém as instruções de bytecode reais a serem executadas pela JVM, bem como estruturas de dados usadas pelo frame de pilha do método, descrito abaixo.

Bytecode JVM

A JVM usa seu próprio conjunto de instruções internas para executar o código compilado. Em execução javap com o -c opção inclui as implementações do método compilado na saída. Se examinarmos nosso RegularPolygon.class dessa forma, veremos a seguinte saída para o nosso getPerimeter() método:

public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn

O bytecode real pode ser parecido com isto:

xB2 00 17 x12 19 xB6 00 1D x27 ...

Cada instrução começa com um byte Código de operação identificação da instrução JVM, seguida de zero ou mais operandos de instrução a serem operados, dependendo do formato da instrução específica. Normalmente, são valores constantes ou referências ao conjunto de constantes. javap traduz o bytecode em uma forma legível exibindo:

Operandos que são exibidos com um sinal de libra, como #23, são referências a entradas no conjunto de constantes. Como podemos ver, javap também produz comentários úteis na saída, identificando o que exatamente está sendo referenciado no pool.

Discutiremos algumas das instruções comuns abaixo. Para obter informações detalhadas sobre o conjunto completo de instruções JVM, consulte o documentação .

Chamadas de método e pilha de chamadas

Cada chamada de método deve ser capaz de rodar com seu próprio contexto, que inclui coisas como variáveis ​​declaradas localmente ou argumentos que foram passados ​​para o método. Juntos, eles formam um empilhar quadro . Após a invocação de um método, um novo quadro é criado e colocado no topo do pilha de chamadas . Quando o método retorna, o quadro atual é removido da pilha de chamadas e descartado, e o quadro que estava em vigor antes da chamada do método é restaurado.

Um quadro de pilha inclui algumas estruturas distintas. Dois importantes são os pilha de operandos e a tabela de variável local , discutido a seguir.

A pilha de chamadas JVM.

Execução na pilha de operando

Muitas instruções JVM operam em seus quadros pilha de operandos . Em vez de especificar um operando constante explicitamente no bytecode, essas instruções tomam os valores no topo da pilha de operandos como entrada. Normalmente, esses valores são removidos da pilha no processo. Algumas instruções também colocam novos valores no topo da pilha. Desta forma, as instruções JVM podem ser combinadas para executar operações complexas. Por exemplo, a expressão:

sideLength * this.numSides

é compilado da seguinte forma em nosso getPerimeter() método:

8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul

As instruções JVM podem operar na pilha de operandos para realizar funções complexas.

Quando um método é chamado, uma nova pilha de operandos é criada como parte de sua estrutura de pilha, onde as operações serão realizadas. Devemos ter cuidado com a terminologia aqui: a palavra 'pilha' pode se referir ao pilha de chamadas , a pilha de quadros fornecendo contexto para a execução do método ou para um determinado quadro pilha de operandos , sobre o qual as instruções JVM operam.

Variáveis ​​Locais

Cada frame de pilha mantém uma tabela de variáveis ​​locais . Isso normalmente inclui uma referência a this objeto, quaisquer argumentos que foram passados ​​quando o método foi chamado e quaisquer variáveis ​​locais declaradas dentro do corpo do método. Em execução javap com o -v opção incluirá informações sobre como o quadro de pilha de cada método deve ser configurado, incluindo sua tabela de variável local:

public double getPerimeter(double); // ... Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... // ... LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LRegularPolygon; 0 16 1 sideLength D

Neste exemplo, existem duas variáveis ​​locais. A variável no slot 0 é denominada this, com o tipo RegularPolygon. Esta é a referência à própria classe do método. A variável no slot 1 é denominada sideLength, com o tipo D (indicando um duplo). Este é o argumento que é passado para o nosso getPerimeter() método.

Instruções como iload_1, fstore_2 ou aload [n] transferem diferentes tipos de variáveis ​​locais entre a pilha de operandos e a tabela de variáveis ​​locais. Como o primeiro item da tabela é geralmente a referência a this, a instrução aload_0 é comumente visto em qualquer método que opere em sua própria classe.

Isso conclui nosso passo a passo dos fundamentos da JVM. Clique aqui para retornar ao artigo principal.

para rastrear seu estado. Método m4() verifica se o valor deste campo é 0, indicando que m4 ainda não foi inicializado; nesse caso, m4$lzycompute() é invocado, populando m4 e retornando seu valor. Este método privado também define o valor de bitmap

Suje Suas Mãos Com Bytecode Scala JVM



A linguagem Scala continuou a ganhar popularidade nos últimos anos, graças à sua excelente combinação de princípios de desenvolvimento de software funcional e orientado a objetos , e sua implementação em cima da comprovada Java Virtual Machine (JVM).

Apesar Escada compila para bytecode Java, é projetado para melhorar muitas das deficiências percebidas da linguagem Java. Oferecendo suporte de programação funcional completo, a sintaxe central do Scala contém muitas estruturas implícitas que devem ser construídas explicitamente por programadores Java, algumas envolvendo considerável complexidade.



A criação de uma linguagem que compila para bytecode Java requer um conhecimento profundo do funcionamento interno da Java Virtual Machine. Para apreciar o que os desenvolvedores do Scala realizaram, é necessário ir nos bastidores e explorar como o código-fonte do Scala é interpretado pelo compilador para produzir bytecode JVM eficiente e eficaz.



Vamos dar uma olhada em como tudo isso é implementado.



Pré-requisitos

Ler este artigo requer algum conhecimento básico do bytecode da Java Virtual Machine. A especificação completa da máquina virtual pode ser obtida em Documentação oficial da Oracle . Ler toda a especificação não é crítica para a compreensão deste artigo, então, para uma rápida introdução ao básico, preparei um pequeno guia no final do artigo.

Clique aqui para ler um curso intensivo sobre noções básicas de JVM.

Um utilitário é necessário para desmontar o bytecode Java para reproduzir os exemplos fornecidos abaixo e para prosseguir com uma investigação adicional. O Java Development Kit fornece seu próprio utilitário de linha de comando, javap, que usaremos aqui. Uma rápida demonstração de como javap trabalhos estão incluídos no guia na parte inferior .



E, claro, uma instalação funcional do compilador Scala é necessária para os leitores que desejam acompanhar os exemplos. Este artigo foi escrito usando Escala 2.11.7 . Diferentes versões do Scala podem produzir bytecode ligeiramente diferente.

Getters e setters padrão

Embora a convenção Java sempre forneça métodos getter e setter para atributos públicos, os programadores Java são obrigados a escrevê-los, apesar do fato de que o padrão para cada um não mudou em décadas. O Scala, em contraste, fornece getters e setters padrão.



Vejamos o seguinte exemplo:

class Person(val name:String) { }

Vamos dar uma olhada dentro da classe Person. Se compilarmos este arquivo com scalac, então executar $ javap -p Person.class nos dá:



Compiled from 'Person.scala' public class Person { private final java.lang.String name; // field public java.lang.String name(); // getter method public Person(java.lang.String); // constructor }

Podemos ver que para cada campo da classe Scala, um campo e seu método getter são gerados. O campo é privado e final, enquanto o método é público.

Se substituirmos val com var no Person fonte e recompilar, então o campo final o modificador é eliminado e o método setter também é adicionado:



Compiled from 'Person.scala' public class Person { private java.lang.String name; // field public java.lang.String name(); // getter method public void name_$eq(java.lang.String); // setter method public Person(java.lang.String); // constructor }

Se houver val ou var é definido dentro do corpo da classe, então o campo privado correspondente e os métodos de acesso são criados e inicializados apropriadamente na criação da instância.

Observe que tal implementação de nível de classe val e var campos significa que se algumas variáveis ​​são usadas no nível da classe para armazenar valores intermediários e nunca são acessadas diretamente pelo programador, a inicialização de cada um desses campos adicionará um ou dois métodos ao espaço da classe. Adicionando um private modificador para tais campos não significa que os acessadores correspondentes serão descartados. Eles apenas se tornarão privados.



Definições de variáveis ​​e funções

Vamos supor que temos um método, m(), e criamos três referências diferentes no estilo Scala para esta função:

class Person(val name:String) { def m(): Int = { // ... return 0 } val m1 = m var m2 = m def m3 = m }

Como cada uma dessas referências a m construído? Quando m ser executado em cada caso? Vamos dar uma olhada no bytecode resultante. A saída a seguir mostra os resultados de javap -v Person.class (omitindo muita saída supérflua):

Constant pool: #22 = Fieldref #2.#21 // Person.m1:I #24 = Fieldref #2.#23 // Person.m2:I #30 = Methodref #2.#29 // Person.m:()I #35 = Methodref #4.#34 // java/lang/Object.'':()V // ... public int m(); Code: // other methods refer to this method // ... public int m1(); Code: // get the value of field m1 and return it 0: aload_0 1: getfield #22 // Field m1:I 4: ireturn public int m2(); Code: // get the value of field m2 and return it 0: aload_0 1: getfield #24 // Field m2:I 4: ireturn public void m2_$eq(int); Code: // get the value of this method's input argument 0: aload_0 1: iload_1 // write it to the field m2 and return 2: putfield #24 // Field m2:I 5: return public int m3(); Code: // execute the instance method m(), and return 0: aload_0 1: invokevirtual #30 // Method m:()I 4: ireturn public Person(java.lang.String); Code: // instance constructor ... // execute the instance method m(), and write the result to field m1 9: aload_0 10: aload_0 11: invokevirtual #30 // Method m:()I 14: putfield #22 // Field m1:I // execute the instance method m(), and write the result to field m2 17: aload_0 18: aload_0 19: invokevirtual #30 // Method m:()I 22: putfield #24 // Field m2:I 25: return

No pool constante, vemos que a referência ao método m() é armazenado no índice #30. No código do construtor, vemos que esse método é chamado duas vezes durante a inicialização, com a instrução invokevirtual #30 aparecendo primeiro no deslocamento de byte 11 e, em seguida, no deslocamento 19. A primeira chamada é seguida pela instrução putfield #22 que atribui o resultado deste método ao campo m1, referenciado pelo índice #22 na piscina constante. A segunda chamada é seguida pelo mesmo padrão, desta vez atribuindo o valor ao campo m2, indexado em #24 na piscina constante.

Em outras palavras, atribuir um método a uma variável definida com val ou var apenas atribui o resultado do método para essa variável. Podemos ver que os métodos m1() e m2() que são criados são simplesmente getters para essas variáveis. No caso de var m2, também vemos que o setter m2_$eq(int) é criado, que se comporta como qualquer outro configurador, sobrescrevendo o valor no campo.

No entanto, usando a palavra-chave def dá um resultado diferente. Em vez de buscar um valor de campo para retornar, o método m3() também inclui a instrução invokevirtual #30. Ou seja, cada vez que esse método é chamado, ele chama m() e retorna o resultado desse método.

Portanto, como podemos ver, Scala fornece três maneiras de trabalhar com campos de classe, e elas são facilmente especificadas por meio das palavras-chave val, var e def. Em Java, teríamos que implementar os setters e getters necessários explicitamente, e esse código clichê escrito manualmente seria muito menos expressivo e mais sujeito a erros.

Valores preguiçosos

Um código mais complicado é produzido ao declarar um valor lento. Suponha que adicionamos o seguinte campo à classe definida anteriormente:

lazy val m4 = m

Em execução javap -p -v Person.class irá agora revelar o seguinte:

Constant pool: #20 = Fieldref #2.#19 // Person.bitmap$0:Z #23 = Methodref #2.#22 // Person.m:()I #25 = Fieldref #2.#24 // Person.m4:I #31 = Fieldref #27.#30 // scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; #48 = Methodref #2.#47 // Person.m4$lzycompute:()I // ... private volatile boolean bitmap$0; private int m4$lzycompute(); Code: // lock the thread 0: aload_0 1: dup 2: astore_1 3: monitorenter // check the flag for whether this field has already been set 4: aload_0 5: getfield #20 // Field bitmap$0:Z // if it has, skip to position 24 (unlock the thread and return) 8: ifne 24 // if it hasn't, execute the method m() 11: aload_0 12: aload_0 13: invokevirtual #23 // Method m:()I // write the method to the field m4 16: putfield #25 // Field m4:I // set the flag indicating the field has been set 19: aload_0 20: iconst_1 21: putfield #20 // Field bitmap$0:Z // unlock the thread 24: getstatic #31 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 27: pop 28: aload_1 29: monitorexit // get the value of field m4 and return it 30: aload_0 31: getfield #25 // Field m4:I 34: ireturn // ... public int m4(); Code: // check the flag for whether this field has already been set 0: aload_0 1: getfield #20 // Field bitmap$0:Z // if it hasn't, skip to position 14 (invoke lazy method and return) 4: ifeq 14 // if it has, get the value of field m4, then skip to position 18 (return) 7: aload_0 8: getfield #25 // Field m4:I 11: goto 18 // execute the method m4$lzycompute() to set the field 14: aload_0 15: invokespecial #48 // Method m4$lzycompute:()I // return 18: ireturn

Nesse caso, o valor do campo m4 não é calculado até que seja necessário. O método especial privado m4$lzycompute() é produzido para calcular o valor lento e o campo bitmap$0 para rastrear seu estado. Método m4() verifica se o valor deste campo é 0, indicando que m4 ainda não foi inicializado; nesse caso, m4$lzycompute() é invocado, populando m4 e retornando seu valor. Este método privado também define o valor de bitmap$0 para 1, de modo que da próxima vez m4() for chamado, ele ignorará a chamada do método de inicialização e, em vez disso, simplesmente retornará o valor de m4.

Os resultados da primeira chamada para um valor lento Scala.

O bytecode que Scala produz aqui é projetado para ser seguro para thread e eficaz. Para ser seguro para threads, o método de computação lenta usa o método monitorenter / monitorexit par de instruções. O método permanece efetivo, pois a sobrecarga de desempenho dessa sincronização ocorre apenas na primeira leitura do valor lento.

Apenas um bit é necessário para indicar o estado do valor lento. Portanto, se não houver mais do que 32 valores lazy, um único campo int pode rastrear todos eles. Se mais de um valor lento for definido no código-fonte, o bytecode acima será modificado pelo compilador para implementar uma máscara de bits para esse propósito.

Novamente, Scala nos permite tirar vantagem facilmente de um tipo específico de comportamento que deveria ser implementado explicitamente em Java, economizando esforços e reduzindo o risco de erros de digitação.

Função como valor

Agora vamos dar uma olhada no seguinte código-fonte do Scala:

class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }

O Printer classe tem um campo, output, com o tipo String => Unit: uma função que leva um String e retorna um objeto do tipo Unit (semelhante a void em Java). No método principal, criamos um desses objetos e atribuímos a esse campo uma função anônima que imprime uma determinada string.

Compilar este código gera quatro arquivos de classe:

O código-fonte é compilado em quatro arquivos de classe.

Hello.class é uma classe wrapper cujo método principal simplesmente chama Hello$.main():

public final class Hello // ... public static void main(java.lang.String[]); Code: 0: getstatic #16 // Field Hello$.MODULE$:LHello$; 3: aload_0 4: invokevirtual #18 // Method Hello$.main:([Ljava/lang/String;)V 7: return

O oculto Hello$.class contém a implementação real do método principal. Para dar uma olhada em seu bytecode, certifique-se de escapar corretamente de $ de acordo com as regras do seu shell de comando, para evitar sua interpretação como caractere especial:

public final class Hello$ // ... public void main(java.lang.String[]); Code: // initialize Printer and anonymous function 0: new #16 // class Printer 3: dup 4: new #18 // class Hello$$anonfun$1 7: dup 8: invokespecial #19 // Method Hello$$anonfun$1.'':()V 11: invokespecial #22 // Method Printer.'':(Lscala/Function1;)V 14: astore_2 // load the anonymous function onto the stack 15: aload_2 16: invokevirtual #26 // Method Printer.output:()Lscala/Function1; // execute the anonymous function, passing the string 'Hello' 19: ldc #28 // String Hello 21: invokeinterface #34, 2 // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object; // return 26: pop 27: return

O método cria um Printer. Em seguida, ele cria um Hello$$anonfun$1, que contém nossa função anônima s => println(s). O Printer é inicializado com este objeto como output campo. Este campo é então carregado na pilha e executado com o operando 'Hello'.

Vamos dar uma olhada na classe de função anônima, Hello$$anonfun$1.class, abaixo. Podemos ver que ele estende Function1 de Scala | (como AbstractFunction1) implementando apply() método. Na verdade, ele cria dois apply() métodos, um envolvendo o outro, que juntos executam a verificação de tipo (neste caso, se a entrada é um String), e executam a função anônima (imprimindo a entrada com println()).

public final class Hello$$anonfun$1 extends scala.runtime.AbstractFunction1 implements scala.Serializable // ... // Takes an argument of type String. Invoked second. public final void apply(java.lang.String); Code: // execute Scala's built-in method println(), passing the input argument 0: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: aload_1 4: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 7: return // Takes an argument of type Object. Invoked first. public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 // check that the input argument is a String (throws exception if not) 1: aload_1 2: checkcast #36 // class java/lang/String // invoke the method apply( String ), passing the input argument 5: invokevirtual #38 // Method apply:(Ljava/lang/String;)V // return the void type 8: getstatic #44 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 11: areturn

Olhando para trás, para Hello$.main() acima, podemos ver que, no deslocamento 21, a execução da função anônima é disparada por uma chamada a seu apply( Object ) método.

Finalmente, para completar, vamos dar uma olhada no bytecode para Printer.class:

public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return

Podemos ver que a função anônima aqui é tratada como qualquer val variável. Ele é armazenado no campo de classe output e no getter output() é criado. A única diferença é que essa variável deve agora implementar a interface Scala scala.Function1 (o que AbstractFunction1 faz).

Portanto, o custo desse recurso elegante do Scala são as classes de utilitário subjacentes, criadas para representar e executar uma única função anônima que pode ser usada como um valor. Você deve levar em consideração o número de tais funções, bem como os detalhes de sua implementação de VM, para descobrir o que isso significa para seu aplicativo específico.

Explorando os bastidores com Scala: Explore como essa linguagem poderosa é implementada no bytecode JVM. Tweet

Traços de Escala

As características do Scala são semelhantes às interfaces em Java. A característica a seguir define duas assinaturas de método e fornece uma implementação padrão da segunda. Vamos ver como isso é implementado:

trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }

O código-fonte é compilado em dois arquivos de classe.

Duas entidades são produzidas: Similarity.class, a interface declarando ambos os métodos, e a classe sintética, Similarity$class.class, fornecendo a implementação padrão:

public interface Similarity { public abstract boolean isSimilar(java.lang.Object); public abstract boolean isNotSimilar(java.lang.Object); } public abstract class Similarity$class public static boolean isNotSimilar(Similarity, java.lang.Object); Code: 0: aload_0 // execute the instance method isSimilar() 1: aload_1 2: invokeinterface #13, 2 // InterfaceMethod Similarity.isSimilar:(Ljava/lang/Object;)Z // if the returned value is 0, skip to position 14 (return with value 1) 7: ifeq 14 // otherwise, return with value 0 10: iconst_0 11: goto 15 // return the value 1 14: iconst_1 15: ireturn public static void $init$(Similarity); Code: 0: return

Quando uma classe implementa essa característica e chama o método isNotSimilar, o compilador Scala gera a instrução bytecode invokestatic para chamar o método estático fornecido pela classe que o acompanha.

Polimorfismo complexo e estruturas de herança podem ser criados a partir de características. Por exemplo, vários traços, bem como a classe de implementação, podem substituir um método com a mesma assinatura, chamando super.methodName() para passar o controle para a próxima característica. Quando o compilador Scala encontra essas chamadas, ele:

Portanto, podemos ver que o poderoso conceito de características é implementado no nível da JVM de uma forma que não leva a uma sobrecarga significativa, e os programadores de Scala podem aproveitar esse recurso sem se preocupar se ele será muito caro em tempo de execução.

Singletons

Scala fornece a definição explícita de classes singleton usando a palavra-chave object. Vamos considerar a seguinte classe única:

object Config { val home_dir = '/home/user' }

O compilador produz dois arquivos de classe:

O código-fonte é compilado em dois arquivos de classe.

Config.class é muito simples:

public final class Config public static java.lang.String home_dir(); Code: // execute the method Config$.home_dir() 0: getstatic #16 // Field Config$.MODULE$:LConfig$; 3: invokevirtual #18 // Method Config$.home_dir:()Ljava/lang/String; 6: areturn

Este é apenas um decorador para o material sintético Config$ classe que incorpora a funcionalidade do singleton. Examinando essa classe com javap -p -c produz o seguinte bytecode:

public final class Config$ public static final Config$ MODULE$; // a public reference to the singleton object private final java.lang.String home_dir; // static initializer public static {}; Code: 0: new #2 // class Config$ 3: invokespecial #12 // Method '':()V 6: return public java.lang.String home_dir(); Code: // get the value of field home_dir and return it 0: aload_0 1: getfield #17 // Field home_dir:Ljava/lang/String; 4: areturn private Config$(); Code: // initialize the object 0: aload_0 1: invokespecial #19 // Method java/lang/Object.'':()V // expose a public reference to this object in the synthetic variable MODULE$ 4: aload_0 5: putstatic #21 // Field MODULE$:LConfig$; // load the value '/home/user' and write it to the field home_dir 8: aload_0 9: ldc #23 // String /home/user 11: putfield #17 // Field home_dir:Ljava/lang/String; 14: return

Consiste no seguinte:

O singleton é um padrão de design popular e útil. A linguagem Java não fornece uma maneira direta de especificá-la no nível da linguagem; em vez disso, é responsabilidade do desenvolvedor implementá-lo no código-fonte Java. Scala, por outro lado, fornece uma maneira clara e conveniente de declarar um singleton explicitamente usando object palavra-chave. Como podemos ver olhando por baixo do capô, ele é implementado de forma acessível e natural.

Conclusão

Agora vimos como o Scala compila vários recursos de programação implícitos e funcionais em sofisticadas estruturas de bytecode Java. Com este vislumbre do funcionamento interno do Scala, podemos obter uma apreciação mais profunda do poder do Scala, ajudando-nos a obter o máximo desta linguagem poderosa.

Agora também temos as ferramentas para explorar a linguagem por conta própria. Existem muitos recursos úteis da sintaxe Scala que não são abordados neste artigo, como classes de caso, currying e compreensões de lista. Eu o encorajo a investigar a implementação dessas estruturas em Scala, para que você possa aprender como ser um ninja Scala de próximo nível!


A máquina virtual Java: um curso intensivo

Assim como o compilador Java, o compilador Scala converte o código-fonte em .class arquivos, contendo bytecode Java a ser executado pela Java Virtual Machine. Para entender como os dois idiomas diferem nos bastidores, é necessário entender o sistema que ambos visam. Aqui, apresentamos uma breve visão geral de alguns dos principais elementos da arquitetura da Java Virtual Machine, da estrutura do arquivo de classe e dos fundamentos do assembler.

Observe que este guia cobrirá apenas o mínimo para permitir o acompanhamento do artigo acima. Embora muitos dos principais componentes da JVM não sejam discutidos aqui, detalhes completos podem ser encontrados nos documentos oficiais, Aqui .

Decompilando arquivos de classe com javap
Pool Constante
Tabelas de campo e método
Bytecode JVM
Chamadas de método e pilha de chamadas
Execução na pilha de operando
Variáveis ​​Locais
Voltar ao topo

Decompilando arquivos de classe com javap

Java é fornecido com javap utilitário de linha de comando, que descompila .class arquivos em um formato legível por humanos. Como os arquivos de classe Scala e Java têm como destino a mesma JVM, javap pode ser usado para examinar arquivos de classe compilados pelo Scala.

Vamos compilar o seguinte código-fonte:

// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }

Compilando isso com scalac RegularPolygon.scala irá produzir RegularPolygon.class. Se executarmos javap RegularPolygon.class veremos o seguinte:

$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Esta é uma divisão muito simples do arquivo de classe que simplesmente mostra os nomes e tipos dos membros públicos da classe. Adicionando o -p opção incluirá membros privados:

$ javap -p RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Isso ainda não é muita informação. Para ver como os métodos são implementados no bytecode Java, vamos adicionar o -c opção:

$ javap -p -c RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); Code: 0: aload_0 1: getfield #13 // Field numSides:I 4: ireturn public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn public RegularPolygon(int); Code: 0: aload_0 1: iload_1 2: putfield #13 // Field numSides:I 5: aload_0 6: invokespecial #38 // Method java/lang/Object.'':()V 9: return }

Isso é um pouco mais interessante. No entanto, para realmente obter a história toda, devemos usar o -v ou -verbose opção, como em javap -p -v RegularPolygon.class:

O conteúdo completo de um arquivo de classe Java.

Aqui finalmente vemos o que realmente está no arquivo da classe. O que tudo isso significa? Vamos dar uma olhada em algumas das partes mais importantes.

Pool Constante

O ciclo de desenvolvimento para aplicativos C ++ inclui estágios de compilação e vinculação. O ciclo de desenvolvimento para Java ignora um estágio de ligação explícita porque a ligação acontece no tempo de execução. O arquivo de classe deve oferecer suporte a esse link de tempo de execução. Isso significa que quando o código-fonte se refere a qualquer campo ou método, o bytecode resultante deve manter as referências relevantes em forma simbólica, prontas para serem desreferenciadas assim que o aplicativo for carregado na memória e os endereços reais puderem ser resolvidos pelo vinculador em tempo de execução. Esta forma simbólica deve conter:

A especificação do formato do arquivo de classe inclui uma seção do arquivo chamada de piscina constante , uma tabela de todas as referências necessárias para o vinculador. Ele contém entradas de diferentes tipos.

// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...

O primeiro byte de cada entrada é uma etiqueta numérica que indica o tipo de entrada. Os bytes restantes fornecem informações sobre o valor da entrada. O número de bytes e as regras para sua interpretação dependem do tipo indicado pelo primeiro byte.

Por exemplo, uma classe Java que usa um número inteiro constante 365 pode ter uma entrada de pool constante com o seguinte código de bytes:

x03 00 00 01 6D

O primeiro byte, x03, identifica o tipo de entrada, CONSTANT_Integer. Isso informa ao vinculador que os próximos quatro bytes contêm o valor do inteiro. (Observe que 365 em hexadecimal é x16D). Se esta for a 14ª entrada no pool constante, javap -v irá renderizá-lo assim:

#14 = Integer 365

Muitos tipos de constantes são compostos de referências a tipos de constantes mais “primitivos” em outras partes do pool de constantes. Por exemplo, nosso código de exemplo contém a instrução:

println( 'Calculating perimeter...' )

O uso de uma constante de string produzirá duas entradas no pool de constantes: uma entrada com tipo CONSTANT_String e outra entrada do tipo CONSTANT_Utf8. A entrada do tipo Constant_UTF8 contém a representação UTF8 real do valor da string. A entrada do tipo CONSTANT_String contém uma referência ao CONSTANT_Utf8 entrada:

#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...

Essa complicação é necessária porque existem outros tipos de entradas de pool constantes que se referem a entradas do tipo Utf8 e que não são entradas do tipo String. Por exemplo, qualquer referência a um atributo de classe produzirá um CONSTANT_Fieldref tipo, que contém uma série de referências ao nome da classe, nome do atributo e tipo de atributo:

#1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #9 = Utf8 numSides #10 = Utf8 I #12 = NameAndType #9:#10 // numSides:I #13 = Fieldref #2.#12 // RegularPolygon.numSides:I

Para obter mais detalhes sobre o pool constante, consulte a documentação JVM .

Tabelas de campo e método

Um arquivo de classe contém um tabela de campo que contém informações sobre cada campo (ou seja, atributo) definido na classe. Estas são referências a entradas de pool constantes que descrevem o nome e tipo do campo, bem como sinalizadores de controle de acesso e outros dados relevantes.

Um similar tabela de métodos está presente no arquivo de classe. No entanto, além das informações de nome e tipo, para cada método não abstrato, ele contém as instruções de bytecode reais a serem executadas pela JVM, bem como estruturas de dados usadas pelo frame de pilha do método, descrito abaixo.

Bytecode JVM

A JVM usa seu próprio conjunto de instruções internas para executar o código compilado. Em execução javap com o -c opção inclui as implementações do método compilado na saída. Se examinarmos nosso RegularPolygon.class dessa forma, veremos a seguinte saída para o nosso getPerimeter() método:

public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn

O bytecode real pode ser parecido com isto:

xB2 00 17 x12 19 xB6 00 1D x27 ...

Cada instrução começa com um byte Código de operação identificação da instrução JVM, seguida de zero ou mais operandos de instrução a serem operados, dependendo do formato da instrução específica. Normalmente, são valores constantes ou referências ao conjunto de constantes. javap traduz o bytecode em uma forma legível exibindo:

Operandos que são exibidos com um sinal de libra, como #23, são referências a entradas no conjunto de constantes. Como podemos ver, javap também produz comentários úteis na saída, identificando o que exatamente está sendo referenciado no pool.

Discutiremos algumas das instruções comuns abaixo. Para obter informações detalhadas sobre o conjunto completo de instruções JVM, consulte o documentação .

Chamadas de método e pilha de chamadas

Cada chamada de método deve ser capaz de rodar com seu próprio contexto, que inclui coisas como variáveis ​​declaradas localmente ou argumentos que foram passados ​​para o método. Juntos, eles formam um empilhar quadro . Após a invocação de um método, um novo quadro é criado e colocado no topo do pilha de chamadas . Quando o método retorna, o quadro atual é removido da pilha de chamadas e descartado, e o quadro que estava em vigor antes da chamada do método é restaurado.

Um quadro de pilha inclui algumas estruturas distintas. Dois importantes são os pilha de operandos e a tabela de variável local , discutido a seguir.

A pilha de chamadas JVM.

Execução na pilha de operando

Muitas instruções JVM operam em seus quadros pilha de operandos . Em vez de especificar um operando constante explicitamente no bytecode, essas instruções tomam os valores no topo da pilha de operandos como entrada. Normalmente, esses valores são removidos da pilha no processo. Algumas instruções também colocam novos valores no topo da pilha. Desta forma, as instruções JVM podem ser combinadas para executar operações complexas. Por exemplo, a expressão:

sideLength * this.numSides

é compilado da seguinte forma em nosso getPerimeter() método:

8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul

As instruções JVM podem operar na pilha de operandos para realizar funções complexas.

Quando um método é chamado, uma nova pilha de operandos é criada como parte de sua estrutura de pilha, onde as operações serão realizadas. Devemos ter cuidado com a terminologia aqui: a palavra 'pilha' pode se referir ao pilha de chamadas , a pilha de quadros fornecendo contexto para a execução do método ou para um determinado quadro pilha de operandos , sobre o qual as instruções JVM operam.

Variáveis ​​Locais

Cada frame de pilha mantém uma tabela de variáveis ​​locais . Isso normalmente inclui uma referência a this objeto, quaisquer argumentos que foram passados ​​quando o método foi chamado e quaisquer variáveis ​​locais declaradas dentro do corpo do método. Em execução javap com o -v opção incluirá informações sobre como o quadro de pilha de cada método deve ser configurado, incluindo sua tabela de variável local:

public double getPerimeter(double); // ... Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... // ... LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LRegularPolygon; 0 16 1 sideLength D

Neste exemplo, existem duas variáveis ​​locais. A variável no slot 0 é denominada this, com o tipo RegularPolygon. Esta é a referência à própria classe do método. A variável no slot 1 é denominada sideLength, com o tipo D (indicando um duplo). Este é o argumento que é passado para o nosso getPerimeter() método.

Instruções como iload_1, fstore_2 ou aload [n] transferem diferentes tipos de variáveis ​​locais entre a pilha de operandos e a tabela de variáveis ​​locais. Como o primeiro item da tabela é geralmente a referência a this, a instrução aload_0 é comumente visto em qualquer método que opere em sua própria classe.

Isso conclui nosso passo a passo dos fundamentos da JVM. Clique aqui para retornar ao artigo principal.

para 1, de modo que da próxima vez m4() for chamado, ele ignorará a chamada do método de inicialização e, em vez disso, simplesmente retornará o valor de m4.

Os resultados da primeira chamada para um valor lento Scala.

O bytecode que Scala produz aqui é projetado para ser seguro para thread e eficaz. Para ser seguro para threads, o método de computação lenta usa o método monitorenter / monitorexit par de instruções. O método permanece efetivo, pois a sobrecarga de desempenho dessa sincronização ocorre apenas na primeira leitura do valor lento.

Apenas um bit é necessário para indicar o estado do valor lento. Portanto, se não houver mais do que 32 valores lazy, um único campo int pode rastrear todos eles. Se mais de um valor lento for definido no código-fonte, o bytecode acima será modificado pelo compilador para implementar uma máscara de bits para esse propósito.

Novamente, Scala nos permite tirar vantagem facilmente de um tipo específico de comportamento que deveria ser implementado explicitamente em Java, economizando esforços e reduzindo o risco de erros de digitação.

Função como valor

Agora vamos dar uma olhada no seguinte código-fonte do Scala:

class Printer(val output: String => Unit) { } object Hello { def main(arg: Array[String]) { val printer = new Printer( s => println(s) ); printer.output('Hello'); } }

O Printer classe tem um campo, output, com o tipo String => Unit: uma função que leva um String e retorna um objeto do tipo Unit (semelhante a void em Java). No método principal, criamos um desses objetos e atribuímos a esse campo uma função anônima que imprime uma determinada string.

Compilar este código gera quatro arquivos de classe:

O código-fonte é compilado em quatro arquivos de classe.

Hello.class é uma classe wrapper cujo método principal simplesmente chama Hello$.main():

public final class Hello // ... public static void main(java.lang.String[]); Code: 0: getstatic #16 // Field Hello$.MODULE$:LHello$; 3: aload_0 4: invokevirtual #18 // Method Hello$.main:([Ljava/lang/String;)V 7: return

O oculto Hello$.class contém a implementação real do método principal. Para dar uma olhada em seu bytecode, certifique-se de escapar corretamente de $ de acordo com as regras do seu shell de comando, para evitar sua interpretação como caractere especial:

public final class Hello$ // ... public void main(java.lang.String[]); Code: // initialize Printer and anonymous function 0: new #16 // class Printer 3: dup 4: new #18 // class Hello$$anonfun 7: dup 8: invokespecial #19 // Method Hello$$anonfun.'':()V 11: invokespecial #22 // Method Printer.'':(Lscala/Function1;)V 14: astore_2 // load the anonymous function onto the stack 15: aload_2 16: invokevirtual #26 // Method Printer.output:()Lscala/Function1; // execute the anonymous function, passing the string 'Hello' 19: ldc #28 // String Hello 21: invokeinterface #34, 2 // InterfaceMethod scala/Function1.apply:(Ljava/lang/Object;)Ljava/lang/Object; // return 26: pop 27: return

O método cria um Printer. Em seguida, ele cria um Hello$$anonfun, que contém nossa função anônima s => println(s). O Printer é inicializado com este objeto como output campo. Este campo é então carregado na pilha e executado com o operando 'Hello'.

Vamos dar uma olhada na classe de função anônima, Hello$$anonfun.class, abaixo. Podemos ver que ele estende Function1 de Scala | (como AbstractFunction1) implementando apply() método. Na verdade, ele cria dois apply() métodos, um envolvendo o outro, que juntos executam a verificação de tipo (neste caso, se a entrada é um String), e executam a função anônima (imprimindo a entrada com println()).

public final class Hello$$anonfun extends scala.runtime.AbstractFunction1 implements scala.Serializable // ... // Takes an argument of type String. Invoked second. public final void apply(java.lang.String); Code: // execute Scala's built-in method println(), passing the input argument 0: getstatic #25 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: aload_1 4: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 7: return // Takes an argument of type Object. Invoked first. public final java.lang.Object apply(java.lang.Object); Code: 0: aload_0 // check that the input argument is a String (throws exception if not) 1: aload_1 2: checkcast #36 // class java/lang/String // invoke the method apply( String ), passing the input argument 5: invokevirtual #38 // Method apply:(Ljava/lang/String;)V // return the void type 8: getstatic #44 // Field scala/runtime/BoxedUnit.UNIT:Lscala/runtime/BoxedUnit; 11: areturn

Olhando para trás, para Hello$.main() acima, podemos ver que, no deslocamento 21, a execução da função anônima é disparada por uma chamada a seu apply( Object ) método.

Finalmente, para completar, vamos dar uma olhada no bytecode para Printer.class:

public class Printer // ... // field private final scala.Function1 output; // field getter public scala.Function1 output(); Code: 0: aload_0 1: getfield #14 // Field output:Lscala/Function1; 4: areturn // constructor public Printer(scala.Function1); Code: 0: aload_0 1: aload_1 2: putfield #14 // Field output:Lscala/Function1; 5: aload_0 6: invokespecial #21 // Method java/lang/Object.'':()V 9: return

Podemos ver que a função anônima aqui é tratada como qualquer val variável. Ele é armazenado no campo de classe output e no getter output() é criado. A única diferença é que essa variável deve agora implementar a interface Scala scala.Function1 (o que AbstractFunction1 faz).

Portanto, o custo desse recurso elegante do Scala são as classes de utilitário subjacentes, criadas para representar e executar uma única função anônima que pode ser usada como um valor. Você deve levar em consideração o número de tais funções, bem como os detalhes de sua implementação de VM, para descobrir o que isso significa para seu aplicativo específico.

Explorando os bastidores com Scala: Explore como essa linguagem poderosa é implementada no bytecode JVM. Tweet

Traços de Escala

As características do Scala são semelhantes às interfaces em Java. A característica a seguir define duas assinaturas de método e fornece uma implementação padrão da segunda. Vamos ver como isso é implementado:

trait Similarity { def isSimilar(x: Any): Boolean def isNotSimilar(x: Any): Boolean = !isSimilar(x) }

O código-fonte é compilado em dois arquivos de classe.

Duas entidades são produzidas: Similarity.class, a interface declarando ambos os métodos, e a classe sintética, Similarity$class.class, fornecendo a implementação padrão:

public interface Similarity { public abstract boolean isSimilar(java.lang.Object); public abstract boolean isNotSimilar(java.lang.Object); } public abstract class Similarity$class public static boolean isNotSimilar(Similarity, java.lang.Object); Code: 0: aload_0 // execute the instance method isSimilar() 1: aload_1 2: invokeinterface #13, 2 // InterfaceMethod Similarity.isSimilar:(Ljava/lang/Object;)Z // if the returned value is 0, skip to position 14 (return with value 1) 7: ifeq 14 // otherwise, return with value 0 10: iconst_0 11: goto 15 // return the value 1 14: iconst_1 15: ireturn public static void $init$(Similarity); Code: 0: return

Quando uma classe implementa essa característica e chama o método isNotSimilar, o compilador Scala gera a instrução bytecode invokestatic para chamar o método estático fornecido pela classe que o acompanha.

Polimorfismo complexo e estruturas de herança podem ser criados a partir de características. Por exemplo, vários traços, bem como a classe de implementação, podem substituir um método com a mesma assinatura, chamando super.methodName() para passar o controle para a próxima característica. Quando o compilador Scala encontra essas chamadas, ele:

Portanto, podemos ver que o poderoso conceito de características é implementado no nível da JVM de uma forma que não leva a uma sobrecarga significativa, e os programadores de Scala podem aproveitar esse recurso sem se preocupar se ele será muito caro em tempo de execução.

projetos de servidor web raspberry pi

Singletons

Scala fornece a definição explícita de classes singleton usando a palavra-chave object. Vamos considerar a seguinte classe única:

object Config { val home_dir = '/home/user' }

O compilador produz dois arquivos de classe:

O código-fonte é compilado em dois arquivos de classe.

Config.class é muito simples:

public final class Config public static java.lang.String home_dir(); Code: // execute the method Config$.home_dir() 0: getstatic #16 // Field Config$.MODULE$:LConfig$; 3: invokevirtual #18 // Method Config$.home_dir:()Ljava/lang/String; 6: areturn

Este é apenas um decorador para o material sintético Config$ classe que incorpora a funcionalidade do singleton. Examinando essa classe com javap -p -c produz o seguinte bytecode:

public final class Config$ public static final Config$ MODULE$; // a public reference to the singleton object private final java.lang.String home_dir; // static initializer public static {}; Code: 0: new #2 // class Config$ 3: invokespecial #12 // Method '':()V 6: return public java.lang.String home_dir(); Code: // get the value of field home_dir and return it 0: aload_0 1: getfield #17 // Field home_dir:Ljava/lang/String; 4: areturn private Config$(); Code: // initialize the object 0: aload_0 1: invokespecial #19 // Method java/lang/Object.'':()V // expose a public reference to this object in the synthetic variable MODULE$ 4: aload_0 5: putstatic #21 // Field MODULE$:LConfig$; // load the value '/home/user' and write it to the field home_dir 8: aload_0 9: ldc #23 // String /home/user 11: putfield #17 // Field home_dir:Ljava/lang/String; 14: return

Consiste no seguinte:

O singleton é um padrão de design popular e útil. A linguagem Java não fornece uma maneira direta de especificá-la no nível da linguagem; em vez disso, é responsabilidade do desenvolvedor implementá-lo no código-fonte Java. Scala, por outro lado, fornece uma maneira clara e conveniente de declarar um singleton explicitamente usando object palavra-chave. Como podemos ver olhando por baixo do capô, ele é implementado de forma acessível e natural.

Conclusão

Agora vimos como o Scala compila vários recursos de programação implícitos e funcionais em sofisticadas estruturas de bytecode Java. Com este vislumbre do funcionamento interno do Scala, podemos obter uma apreciação mais profunda do poder do Scala, ajudando-nos a obter o máximo desta linguagem poderosa.

Agora também temos as ferramentas para explorar a linguagem por conta própria. Existem muitos recursos úteis da sintaxe Scala que não são abordados neste artigo, como classes de caso, currying e compreensões de lista. Eu o encorajo a investigar a implementação dessas estruturas em Scala, para que você possa aprender como ser um ninja Scala de próximo nível!


A máquina virtual Java: um curso intensivo

Assim como o compilador Java, o compilador Scala converte o código-fonte em .class arquivos, contendo bytecode Java a ser executado pela Java Virtual Machine. Para entender como os dois idiomas diferem nos bastidores, é necessário entender o sistema que ambos visam. Aqui, apresentamos uma breve visão geral de alguns dos principais elementos da arquitetura da Java Virtual Machine, da estrutura do arquivo de classe e dos fundamentos do assembler.

Observe que este guia cobrirá apenas o mínimo para permitir o acompanhamento do artigo acima. Embora muitos dos principais componentes da JVM não sejam discutidos aqui, detalhes completos podem ser encontrados nos documentos oficiais, Aqui .

Decompilando arquivos de classe com javap
Pool Constante
Tabelas de campo e método
Bytecode JVM
Chamadas de método e pilha de chamadas
Execução na pilha de operando
Variáveis ​​Locais
Voltar ao topo

Decompilando arquivos de classe com javap

Java é fornecido com javap utilitário de linha de comando, que descompila .class arquivos em um formato legível por humanos. Como os arquivos de classe Scala e Java têm como destino a mesma JVM, javap pode ser usado para examinar arquivos de classe compilados pelo Scala.

Vamos compilar o seguinte código-fonte:

// RegularPolygon.scala class RegularPolygon( val numSides: Int ) { def getPerimeter( sideLength: Double ): Double = { println( 'Calculating perimeter...' ) return sideLength * this.numSides } }

Compilando isso com scalac RegularPolygon.scala irá produzir RegularPolygon.class. Se executarmos javap RegularPolygon.class veremos o seguinte:

$ javap RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Esta é uma divisão muito simples do arquivo de classe que simplesmente mostra os nomes e tipos dos membros públicos da classe. Adicionando o -p opção incluirá membros privados:

$ javap -p RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); public double getPerimeter(double); public RegularPolygon(int); }

Isso ainda não é muita informação. Para ver como os métodos são implementados no bytecode Java, vamos adicionar o -c opção:

$ javap -p -c RegularPolygon.class Compiled from 'RegularPolygon.scala' public class RegularPolygon { private final int numSides; public int numSides(); Code: 0: aload_0 1: getfield #13 // Field numSides:I 4: ireturn public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn public RegularPolygon(int); Code: 0: aload_0 1: iload_1 2: putfield #13 // Field numSides:I 5: aload_0 6: invokespecial #38 // Method java/lang/Object.'':()V 9: return }

Isso é um pouco mais interessante. No entanto, para realmente obter a história toda, devemos usar o -v ou -verbose opção, como em javap -p -v RegularPolygon.class:

O conteúdo completo de um arquivo de classe Java.

Aqui finalmente vemos o que realmente está no arquivo da classe. O que tudo isso significa? Vamos dar uma olhada em algumas das partes mais importantes.

Pool Constante

O ciclo de desenvolvimento para aplicativos C ++ inclui estágios de compilação e vinculação. O ciclo de desenvolvimento para Java ignora um estágio de ligação explícita porque a ligação acontece no tempo de execução. O arquivo de classe deve oferecer suporte a esse link de tempo de execução. Isso significa que quando o código-fonte se refere a qualquer campo ou método, o bytecode resultante deve manter as referências relevantes em forma simbólica, prontas para serem desreferenciadas assim que o aplicativo for carregado na memória e os endereços reais puderem ser resolvidos pelo vinculador em tempo de execução. Esta forma simbólica deve conter:

A especificação do formato do arquivo de classe inclui uma seção do arquivo chamada de piscina constante , uma tabela de todas as referências necessárias para o vinculador. Ele contém entradas de diferentes tipos.

// ... Constant pool: #1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #3 = Utf8 java/lang/Object #4 = Class #3 // java/lang/Object // ...

O primeiro byte de cada entrada é uma etiqueta numérica que indica o tipo de entrada. Os bytes restantes fornecem informações sobre o valor da entrada. O número de bytes e as regras para sua interpretação dependem do tipo indicado pelo primeiro byte.

Por exemplo, uma classe Java que usa um número inteiro constante 365 pode ter uma entrada de pool constante com o seguinte código de bytes:

x03 00 00 01 6D

O primeiro byte, x03, identifica o tipo de entrada, CONSTANT_Integer. Isso informa ao vinculador que os próximos quatro bytes contêm o valor do inteiro. (Observe que 365 em hexadecimal é x16D). Se esta for a 14ª entrada no pool constante, javap -v irá renderizá-lo assim:

#14 = Integer 365

Muitos tipos de constantes são compostos de referências a tipos de constantes mais “primitivos” em outras partes do pool de constantes. Por exemplo, nosso código de exemplo contém a instrução:

println( 'Calculating perimeter...' )

O uso de uma constante de string produzirá duas entradas no pool de constantes: uma entrada com tipo CONSTANT_String e outra entrada do tipo CONSTANT_Utf8. A entrada do tipo Constant_UTF8 contém a representação UTF8 real do valor da string. A entrada do tipo CONSTANT_String contém uma referência ao CONSTANT_Utf8 entrada:

o que o node js faz
#24 = Utf8 Calculating perimeter... #25 = String #24 // Calculating perimeter...

Essa complicação é necessária porque existem outros tipos de entradas de pool constantes que se referem a entradas do tipo Utf8 e que não são entradas do tipo String. Por exemplo, qualquer referência a um atributo de classe produzirá um CONSTANT_Fieldref tipo, que contém uma série de referências ao nome da classe, nome do atributo e tipo de atributo:

#1 = Utf8 RegularPolygon #2 = Class #1 // RegularPolygon #9 = Utf8 numSides #10 = Utf8 I #12 = NameAndType #9:#10 // numSides:I #13 = Fieldref #2.#12 // RegularPolygon.numSides:I

Para obter mais detalhes sobre o pool constante, consulte a documentação JVM .

Tabelas de campo e método

Um arquivo de classe contém um tabela de campo que contém informações sobre cada campo (ou seja, atributo) definido na classe. Estas são referências a entradas de pool constantes que descrevem o nome e tipo do campo, bem como sinalizadores de controle de acesso e outros dados relevantes.

Um similar tabela de métodos está presente no arquivo de classe. No entanto, além das informações de nome e tipo, para cada método não abstrato, ele contém as instruções de bytecode reais a serem executadas pela JVM, bem como estruturas de dados usadas pelo frame de pilha do método, descrito abaixo.

Bytecode JVM

A JVM usa seu próprio conjunto de instruções internas para executar o código compilado. Em execução javap com o -c opção inclui as implementações do método compilado na saída. Se examinarmos nosso RegularPolygon.class dessa forma, veremos a seguinte saída para o nosso getPerimeter() método:

public double getPerimeter(double); Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... 5: invokevirtual #29 // Method scala/Predef$.println:(Ljava/lang/Object;)V 8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul 15: dreturn

O bytecode real pode ser parecido com isto:

xB2 00 17 x12 19 xB6 00 1D x27 ...

Cada instrução começa com um byte Código de operação identificação da instrução JVM, seguida de zero ou mais operandos de instrução a serem operados, dependendo do formato da instrução específica. Normalmente, são valores constantes ou referências ao conjunto de constantes. javap traduz o bytecode em uma forma legível exibindo:

Operandos que são exibidos com um sinal de libra, como #23, são referências a entradas no conjunto de constantes. Como podemos ver, javap também produz comentários úteis na saída, identificando o que exatamente está sendo referenciado no pool.

Discutiremos algumas das instruções comuns abaixo. Para obter informações detalhadas sobre o conjunto completo de instruções JVM, consulte o documentação .

Chamadas de método e pilha de chamadas

Cada chamada de método deve ser capaz de rodar com seu próprio contexto, que inclui coisas como variáveis ​​declaradas localmente ou argumentos que foram passados ​​para o método. Juntos, eles formam um empilhar quadro . Após a invocação de um método, um novo quadro é criado e colocado no topo do pilha de chamadas . Quando o método retorna, o quadro atual é removido da pilha de chamadas e descartado, e o quadro que estava em vigor antes da chamada do método é restaurado.

Um quadro de pilha inclui algumas estruturas distintas. Dois importantes são os pilha de operandos e a tabela de variável local , discutido a seguir.

A pilha de chamadas JVM.

Execução na pilha de operando

Muitas instruções JVM operam em seus quadros pilha de operandos . Em vez de especificar um operando constante explicitamente no bytecode, essas instruções tomam os valores no topo da pilha de operandos como entrada. Normalmente, esses valores são removidos da pilha no processo. Algumas instruções também colocam novos valores no topo da pilha. Desta forma, as instruções JVM podem ser combinadas para executar operações complexas. Por exemplo, a expressão:

sideLength * this.numSides

é compilado da seguinte forma em nosso getPerimeter() método:

8: dload_1 9: aload_0 10: invokevirtual #31 // Method numSides:()I 13: i2d 14: dmul

As instruções JVM podem operar na pilha de operandos para realizar funções complexas.

Quando um método é chamado, uma nova pilha de operandos é criada como parte de sua estrutura de pilha, onde as operações serão realizadas. Devemos ter cuidado com a terminologia aqui: a palavra 'pilha' pode se referir ao pilha de chamadas , a pilha de quadros fornecendo contexto para a execução do método ou para um determinado quadro pilha de operandos , sobre o qual as instruções JVM operam.

Variáveis ​​Locais

Cada frame de pilha mantém uma tabela de variáveis ​​locais . Isso normalmente inclui uma referência a this objeto, quaisquer argumentos que foram passados ​​quando o método foi chamado e quaisquer variáveis ​​locais declaradas dentro do corpo do método. Em execução javap com o -v opção incluirá informações sobre como o quadro de pilha de cada método deve ser configurado, incluindo sua tabela de variável local:

public double getPerimeter(double); // ... Code: 0: getstatic #23 // Field scala/Predef$.MODULE$:Lscala/Predef$; 3: ldc #25 // String Calculating perimeter... // ... LocalVariableTable: Start Length Slot Name Signature 0 16 0 this LRegularPolygon; 0 16 1 sideLength D

Neste exemplo, existem duas variáveis ​​locais. A variável no slot 0 é denominada this, com o tipo RegularPolygon. Esta é a referência à própria classe do método. A variável no slot 1 é denominada sideLength, com o tipo D (indicando um duplo). Este é o argumento que é passado para o nosso getPerimeter() método.

Instruções como iload_1, fstore_2 ou aload [n] transferem diferentes tipos de variáveis ​​locais entre a pilha de operandos e a tabela de variáveis ​​locais. Como o primeiro item da tabela é geralmente a referência a this, a instrução aload_0 é comumente visto em qualquer método que opere em sua própria classe.

Isso conclui nosso passo a passo dos fundamentos da JVM. Clique aqui para retornar ao artigo principal.