Charles L. Perkins e Laura Lemay
Hoje o trabalho interior do sistema de Java vai se revelar.
Descobrirá todos sobre visão de Java, máquina virtual de Java, aqueles bytecodes sobre os quais ouviu tanto, que o coletor de lixo misterioso, e porque poderia incomodar-se com a segurança mas não ter a.
Observar |
O título deste capítulo bem descreve o seu conteúdo; a discussão na lição de hoje é bastante técnica e supõe que saiba algo sobre línguas de baixo nível conceitos de design de compilador/intérprete e (reunião). |
Vamos começar, contudo, com o grande quadro.
A equipe de Java é muito ambiciosa. A sua meta última não é nada menos do que revolucionar o modo que o software se escreve e se distribui. Começaram com a Internet, onde acreditam que a maior parte do software interessante do futuro viverá.
Para realizar uma meta tão ambiciosa, uma grande porção da própria comunidade de programação de Internet deve ordenar-se atrás de uma meta semelhante e dar-se os instrumentos para ajudar a realizá-lo. A língua de Java, com o seu quatro S (pequeno, simples, seguro, seguro) e o seu ambiente flexível, orientado à Rede, espera tornar-se o ponto focal da reunião desta nova legião de programadores.
A este fim, os Microsistemas de Sol fizeram algo bastante corajoso. O que foi originalmente um segredo, dezenas de milhões do projeto de pesquisa e desenvolvimento dólares, e 100 por cento proprietários, tornou-se um padrão de tecnologia livre, aberto, e relativamente não onerado sobre o qual cada um pode construir. Entregam-no literalmente e só reservando os direitos têm de manter e cultivar o padrão.
Qualquer padrão realmente aberto deve apoiar-se por pelo menos uma implementação "de manifestação" excelente, livremente disponível. O sol já embarcou a 1,0 versão de Java como parte do JDK e publicou especificações da própria língua e da máquina virtual e compiladores bytecode. Na paralela, várias universidades, as companhias e os indivíduos já exprimiram a sua intenção de duplicar o ambiente de Java baseado no API aberto que o Sol criou.
Além disso, o ambiente de tempo de execução de Java está incorporando-se em uma grande variedade de sistemas operacionais e ambientes em plataformas diferentes. Microsoft e Apple autorizaram Java a incluir o tempo de execução no Windows e MacOS. Um tempo de execução de Java estará disponível em sistemas de IBM (OS/2 e AIX) bem como em quase cada sabor comercial de UNIX. O que isto significa é que as aplicações escritas em Java serão automaticamente executáveis nestes sistemas, sem qualquer outro software que precisa de instalar-se. Estes passos foram significantes na criação de Java ubíquo como não só a língua da Internet mas também a língua do futuro desenvolvimento de software.
Observar |
Em todas as partes deste livro, o tempo de execução de Java e Java máquina virtual entregam-se a de modo trocável. Enquanto há algumas diferenças leves entre os dois, comparar deles destaca o ambiente único que deve criar-se para apoiar Java. |
Várias outras línguas até intentam compilar abaixo a Java bytecodes, ajudar a apoiá-los na formação de um padrão mais robusto e comum para deslocar o conteúdo executável na Rede.
Uma das razões este movimento brilhante da parte de Sol tem uma possibilidade real do êxito é a frustração retida de literalmente uma geração inteira de programadores que desesperadamente querem compartilhar o seu código um com outro. Agora mesmo, o mundo das Ciências da Computação é balkanized em facções em universidades e companhias em todo o mundo, com centenas de línguas, dúzias deles largamente usado, dividindo-se e separando todos nós. É o pior tipo da Torre de Babel. Java espera construir algumas pontes e ajuda demolem aquela torre. Como é tão simples, porque é tão útil para programar pela Internet, e porque a Internet é tão "quente" agora mesmo - esta confluência de forças deve ajudar a propelir Java para a etapa de centro.
Merece estar lá. É a protuberância natural de ideias que, desde o início dos anos 1970 dentro do grupo de Smalltalk na Xerox PARC, estiveram relativamente dormentes na corrente principal. Smalltalk, de fato, inventou o primeiro intérprete bytecode orientado ao objeto e explorou muitas das ideias profundas que Java se baseia hoje. Aqueles esforços não se abraçaram sobre as décadas intervenientes como uma solução para os problemas gerais do software, de qualquer modo. Hoje, com aqueles problemas que ficam muito mais óbvio, e com a Rede que pede uma nova espécie a gritos da programação, o solo é fértil para cultivar algo mais forte daquelas velhas raízes, algo que somente poderia estender-se como fogo grego. (É uma coincidência que os nomes internos prévios de Java foram Green e CARVALHO?)
Esta nova visão do software é aquela na qual a Rede se torna um oceano de objetos, classes e o APIs aberto entre eles. As aplicações tradicionais desapareceram, substituíram por armações esqueléticas como a Torre Eiffel na qual pode ajustar-se qualquer parte deste oceano, contra apresentação, para ajustar qualquer objetivo. As interfaces de usuário vão se misturar e vão se combinar, vão se construir em partes e vão se construir para saborear, sempre que a necessidade surja, pelos seus próprios usuários. Os cardápios de escolhas vão se encher por listas dinâmicas de todas as escolhas disponíveis para aquela função, naquele momento exato, através do oceano inteiro (da Rede).
Em tal mundo, a distribuição de software não é já uma questão. O software estará em todo lugar e vai se pagar por via uma pletora de novos modelos microcontábeis, que cobram frações muito pequenas de cêntimos das partes como se reúnem e se usam. As armações nascerão para apoiar o entretenimento, o negócio e o social (ciber-) espaços do perto do futuro.
Isto é um sonho de que muitos de nós esperaram todas as nossas vidas para ser uma parte. Há desafios tremendos à criação de tudo isso realizar-se, mas os ventos potentes da modificação que todos nós sentimos devem mexer-nos na ação porque, finalmente, há uma base na qual construir aquele Java do sonho.
Para fazer visões como isto possíveis, Java deve ser ubíquo. Deve ser capaz de correr em qualquer computador e qualquer sistema agora operacional e no futuro. Para realizar este nível da portabilidade, Java deve ser muito exato não só sobre a própria língua, mas sobre o ambiente no qual a língua vive. Viu em todas as partes deste livro que o ambiente de Java inclui um jogo geralmente útil de pacotes de classes e uma implementação livremente disponível deles. Isto cuida de uma parte do que é necessário, mas é também crucial especificar exatamente como o ambiente em tempo de execução de Java se comporta.
Esta exigência final é o que bloqueou muitas tentativas na ubiquidade no passado. Se basear o seu sistema em alguma suposição sobre o que é abaixo do sistema em tempo de execução, perde. Se depender de algum modo do sistema de computador ou sistema operacional abaixo, perde. Java resolve este problema inventando um computador abstrato do seu próprio e correndo nisto.
Esta máquina virtual, como se chama, e que usou em todas as partes deste livro como Java bytecode intérprete, dirige um jogo especial de instruções, chamadas bytecodes, que são simplesmente uma corrente de bytes formatados, cada um dos quais tem uma especificação exata de exatamente o que cada bytecode faz a esta máquina virtual. A máquina virtual também é responsável por certas capacidades fundamentais de Java, como criação de objeto e coleção de lixo.
Finalmente, para ser capaz de mover bytecodes seguramente através da Internet, precisa de um modelo a prova de bala da segurança - e como mantê-lo - e um formato exato para como esta corrente de bytecodes pode enviar-se de uma máquina virtual ao outro.
Cada uma destas exigências dirige-se na lição de hoje.
Observar |
A maior parte da seguinte descrição baseia-se estreitamente nos últimos "documentos" de Especificações de Máquina Virtuais (e os 1.0 bytecodes), portanto se pesquisar mais profundamente nos detalhes online, deve cobrir alguma terra familiar. |
Vale a pena cotar a introdução para Java documentação de máquina virtual aqui, porque é tão relevante para a visão delineou antes:
A especificação de máquina virtual de Java tem um objetivo que é tanto como como diferentemente de documentos equivalentes de outras línguas e máquinas abstratas. Destina-se para apresentar um desenho de máquina abstrato, lógico livre da distração de detalhes sem importância de qualquer implementação. Não espera uma tecnologia de implementação ou um anfitrião de implementação. Ao mesmo tempo dá a um leitor a informação suficiente para permitir a implementação do desenho abstrato em uma variedade de tecnologias.
Contudo, a intenção de [...] o projeto de Java é criar uma língua [...] que permitirá a permuta pela Internet "do conteúdo executável", que se personificará pelo código de Java compilado. O projeto especificamente não quer que Java seja uma língua proprietária e não quer ser o único fornecedor de implementações de língua de Java. Melhor esperamos fazer documentos como este e texto fonte da nossa implementação, livremente disponível para pessoas para usar como selecionam.
Esta visão [...] pode realizar-se só se o conteúdo executável puder compartilhar-se confiantemente entre implementações de Java diferentes. Estas intenções proíbem a definição de Java máquina virtual de ser totalmente abstratas. Melhor os elementos lógicos relevantes do desenho têm de fazer-se suficientemente concretos para permitir a permuta do código de Java compilado. Isto não cai Java especificação de máquina virtual a uma descrição de uma implementação de Java; os elementos do desenho que não desempenham um papel na permuta do conteúdo executável permanecem abstratos. Mas realmente força-nos a especificar, além do desenho de máquina abstrato, um formato de permuta concreto do código de Java compilado.
Java especificação de máquina virtual compõe-se do seguinte:
Cada um destes é coberto hoje.
Apesar deste grau da especificidade, ainda há vários elementos do desenho que permanecem (de propósito) abstratos, inclusive o seguinte:
Estes lugares consistem em onde a criatividade de um executor de máquina virtual tem a rédea cheia.
Java máquina virtual pode dividir-se em cinco partes fundamentais:
Alguns destes poderiam implementar-se usando um intérprete, um compilador de código binário nativo, ou até um pedaço de hardware - mas todos estes componentes lógicos, abstratos da máquina virtual devem fornecer-se em alguma forma em cada sistema de Java.
Observar |
'As áreas de memória usadas por Java máquina virtual não devem estar em qualquer determinado lugar na memória, estar em qualquer determinada ordem, ou até usar a memória contígua. Contudo, todos exceto a área de método devem ser capazes de representar valores de 32 bits alinhados (por exemplo, a pilha de Java é 32 bits de largura). |
A máquina virtual e o seu código de apoio, são muitas vezes referidos como o ambiente em tempo de execução, e quando este livro se refere a algo que se faz no tempo de execução, a máquina virtual é o que o faz.
Java conjunto de instruções de máquina virtual otimiza-se para ser pequeno e compacto. Projeta-se para viajar através da Rede, e assim equilibrou a velocidade da interpretação do espaço. (Dado que tanto a largura de banda Líquida como as velocidades de armazenamento de massa aumentam menos rapidamente do que a velocidade de CPU, isto parece uma troca apropriada.)
Como mencionado, o texto fonte de Java "compila-se" em bytecodes e guarda-se em um arquivo de .class. No sistema de Java de Sol, isto executa-se usando o compilador de Java (javac). O compilador de Java não é exatamente um "compilador" tradicional, porque traduz o texto fonte para bytecodes, um formato de nível mais baixo que não pode dirigir-se diretamente mas deve interpretar-se além disso por cada computador. Naturalmente, é exatamente este nível de vias indiretas que o compram o poder, flexibilidade e portabilidade extrema do código de Java.
Observar |
As aspas usam-se em volta da palavra "compilador" falando sobre o compilador de Java porque depois hoje também aprenderá sobre o compilador "just-in-time", que atua mais como o fim traseiro de um compilador tradicional. O uso da mesma palavra "compilador" destas duas partes diferentes da tecnologia de Java é inoportuno, mas um tanto razoável, porque cada um é realmente metade (a frente ou o fim traseiro) de um compilador mais tradicional. |
Uma instrução bytecode compõe-se de um código de operação de um byte que serve para identificar a instrução implicada e zero ou mais operands, cada um dos quais pode ser mais de um byte de longitude, que codificam os parâmetros que o código de operação necessita.
Observar |
Quando operands são mais de um byte de longitude, guardam-se na grande-endian ordem, byte de alta ordem primeiro. Estes operands devem reunir-se da corrente de byte no tempo de execução. Por exemplo, um parâmetro de 16 bits aparece na corrente como dois bytes para que o seu valor seja first_byte * 256 + second_byte. A corrente de instrução bytecode só se alinha com o byte, e o alinhamento de qualquer quantidade maior não se garante (exceto o interior o lookupswitch bytecodes especial e tableswitch, que têm regras de alinhamento especiais do seu próprio). |
Bytecodes interpretam dados nas áreas de memória em tempo de execução como pertencendo a um jogo fixo de tipos: os tipos primitivos viu várias vezes antes, composto de vários tipos de número inteiro assinados (byte de 8 bits, short de 16 bits, int de 32 bits, long de 64 bits), um tipo de número inteiro não assinado (char de 16 bits) e dois tipos de ponto flutuante assinados (float de 32 bits, double de 64 bits), mais o tipo "referência para um objeto" (um tipo parecido a um ponteiro de 32 bits). Alguns bytecodes especiais (por exemplo, as instruções de dup) tratam áreas de memória em tempo de execução como dados brutos, sem respeito ao tipo. Isto é a exceção, contudo - não a regra.
Estes tipos primitivos distinguem-se e dirigem-se pelo compilador de Java, não pelo ambiente de tempo de execução de Java. Estes tipos não se identificam na memória, e por isso não pode distinguir-se no tempo de execução. bytecodes diferentes projetam-se para tratar cada um de vários tipos primitivos unicamente, e o compilador cuidadosamente seleciona esta paleta baseada no seu conhecimento dos tipos reais guardados em várias áreas de memória. Por exemplo, acrescentando dois números inteiros, o compilador gera um iadd bytecode; para acrescentar duas bóias, fadd gera-se.
A especificação sobre Java bytecodes ela mesma contém-se no apêndice D, "Referência Bytecodes".
Os registros de Java máquina virtual são como os registros dentro de um verdadeiro computador.
Novos termos |
Os registros usam-se para guardar temporariamente dados. Em Java os registros de máquina virtuais mantêm o estado da máquina, afetam a sua operação e atualizam-se depois que cada bytecode realiza-se. |
O seguinte é os registros de Java:
A máquina virtual define estes registros para ser 32 bits de largura.
Observar |
Como a máquina virtual é principalmente baseada na pilha, não usa nenhum registro para passar ou receber argumentos. Isto é uma escolha consciente inclinada em direção a simplicidade bytecode e compacidade. Também ajuda a implementação eficiente em sistemas de computador com menos registros. A propósito, o registro de pc também se usa quando o tempo de execução trata exceções; as cláusulas de catch associam-se (enfim) com variedades do pc dentro de bytecodes de um método. |
Java máquina virtual é baseado na pilha. Uma armação de pilha de Java é semelhante à armação de pilha de uma língua de programação convencional - mantém o estado de uma chamada de método única. As armações de chamadas de método aninhadas empilham-se em cima desta armação.
Novo termo |
A pilha usa-se para fornecer a parâmetros a bytecodes e métodos, e receber resultados atrás deles. |
Cada armação de pilha contém três (possivelmente vazio) jogos de dados: as variáveis locais da chamada de método, o seu ambiente de execução e a sua pilha de operand. Os tamanhos dos primeiros dois fixam-se na partida de uma chamada de método, mas a pilha de operand varia no tamanho como os bytecodes se realizam no método.
As variáveis locais guardam-se em uma tabela de fendas de 32 bits, postas no índex pelo registro vars. A maior parte de tipos tomam uma fenda na tabela, mas o long e double datilografam cada um toma duas fendas.
Observar |
Muito tempo e os valores duplos, guardados ou referidos via um índice N, tomam os (de 32 bits) soquetes [N] e [N]+1. Estes valores de 64 bits, por isso, não se garantem para ser 64 bits alinhados. Os executores são livres de decidir o modo apropriado de dividir estes valores entre as duas fendas. |
O ambiente de execução em uma armação de pilha ajuda a manter a própria pilha. Contém um ponteiro para a armação de pilha prévia, um ponteiro para as variáveis locais da chamada de método e ponteiros para "a base" atual da pilha e "topo". A informação sobre depuração adicional também pode colocar-se no ambiente de execução.
A pilha de operand, pilha de primeiro em primeiro fora (FIFO) de 32 bits, usa-se para guardar os parâmetros e os valores de retorno das a maior parte de instruções bytecode. Por exemplo, o iadd bytecode espera que dois números inteiros se guardem no topo da pilha. Põe-nos de repente, acrescenta-os em conjunto e empurra a soma resultante atrás para a pilha.
Cada tipo de dados primitivo tem instruções únicas que sabem como extrair, fazer funcionar e repelem operands daquele tipo. Por exemplo, long e double operands tomam duas posições quanto à pilha, e os bytecodes especiais que tratam estes operands levam em conta isto. É ilegal para os tipos na pilha e a instrução que os produz ser incompatível (as produções de compilador de Java bytecodes que sempre obedecem a esta regra).
Observar |
O topo da pilha de operand e o topo da pilha de Java total quase sempre são o mesmo. Assim, "a pilha" refere-se a ambas as pilhas, coletivamente. |
O montão é que a parte da memória da qual os exemplos recentemente criados (objetos) se alocam.
O montão muitas vezes destina-se um tamanho grande, fixo quando o sistema de tempo de execução de Java se começa, mas em sistemas que apoiam a memória virtual, pode crescer como necessário, de uma maneira quase ilimitada.
Como os objetos se reúnem do lixo automaticamente em Java, os programadores não têm a (e, de fato, não pode) manualmente libertam a memória alocada para um objeto quando se terminam usando-o.
Os objetos de Java referem-se indiretamente no tempo de execução via maçanetas, que são uma espécie de ponteiro no montão.
Como os objetos nunca se referem diretamente, põem coletores de lixo em paralelo pode escrever-se o que funciona independentemente do seu programa, deslocando objetos no montão à vontade. Aprenderá mais sobre a coleção de lixo na seção "O Coletor de lixo", depois nesta lição.
Como as áreas de código compilado de ambientes de linguagem de programação convencionais ou segmento de TEXT em um processo de UNIX, a área de método guarda Java bytecodes que implementam quase cada método no sistema de Java. (Lembre-se de que alguns métodos poderiam declarar-se por native, e assim implementar-se, por exemplo, em C.) a área de método também guarda as tabelas de símbolos necessárias para vinculação dinâmica bem como qualquer outro depurador de informação adicional ou ambientes de desenvolvimento que poderiam querer associar-se com a implementação de cada método.
Como bytecodes se guardam como correntes de byte, a área de método alinha-se em limites de byte. (Outras áreas alinham-se todos em limites de palavra de 32 bits.)
No montão, cada classe tem uma tabela de constantes, chamadas um consórcio constante, disponível para ele. Normalmente criado pelo compilador de Java, estas constantes codificam todos os nomes (de variáveis, métodos, e assim por diante) usado por qualquer método em uma classe. A classe contém uma conta de quantas constantes lá são e uma compensação que especifica a que distância na própria descrição de classe a tabela de constantes começa. Estas constantes datilografam-se via bytes especialmente codificados e têm um formato precisamente definido quando aparecem no arquivo de .class de uma classe. Depois hoje, um pouco deste formato de arquivo é coberto, mas tudo se especifica totalmente pelas especificações de máquina virtuais no seu lançamento de Java.
A máquina virtual, como atualmente definido, coloca algumas restrições em programas Java legais em virtude das escolhas que fez (alguns descreveram-se anteriormente, e mais vai se detalhar depois hoje).
Estas limitações e as suas implicações são
Além disso, a implementação de Sol da máquina virtual usa assim chamado _quick bytecodes, que além disso limitam o sistema. As compensações de 8 bits não assinadas em objetos podem limitar o número de métodos em uma classe a 256 (este limite pode não existir no lançamento final), e as contas de argumento de 8 bits não assinadas limitam o tamanho da lista de parâmetros a 255 palavras de 32 bits. (Embora isto signifique que pode ter até 255 argumentos da maior parte de tipos, pode ter só 127 deles se forem todo o long ou double.)
Um intérprete bytecode examina cada byte de código de operação (bytecode) na corrente bytecode de um método, à sua vez, e realiza uma ação única para isto bytecode. Isto poderia consumir novos bytes do operands do bytecode e poderia afetar que bytecode se examinará depois. Funciona como o CPU de hardware em um computador, que examina a memória sobre instruções de executar em exatamente a mesma maneira. É o software CPU de Java máquina virtual.
A sua tentativa primeira, ingênua de escrever a um intérprete tão bytecode será quase certamente desastrosamente lenta. O laço interior, que despacha um bytecode cada vez pelo laço, é notoriamente difícil de otimizar. De fato, as pessoas inteligentes têm pensado neste problema, em uma forma ou o outro, durante mais de 20 anos. Afortunadamente, adquiriram resultados, todos dos quais podem aplicar-se a Java.
O resultado final consiste em que o intérprete embarcado no lançamento atual de Java tem um laço interior extremamente rápido. De fato, em até um computador relativamente lento, este intérprete pode executar mais de 590.000 bytecodes por segundo! Isto é realmente bastante bom - o CPU naquele computador faz só aproximadamente 30 vezes melhor, e tem a vantagem de usar o hardware para fazê-lo.
Este intérprete é bastante rápido para a maior parte de programas Java (e para os que necessitam mais velocidade, sempre podem usar métodos nativos - veem a discussão de ontem), mas e se um executor inteligente quiser fazer melhor?
Há aproximadamente uma década, um truque realmente inteligente descobriu-se por Peter Deutsch tentando fazer Smalltalk funcionar mais rápido. Chamou-o tradução dinâmica durante a interpretação. O sol chama-o "just-in-time" (ou JIT) compilação, que, efetivamente, significa converter o bytecode interpretado relativamente lento no código de máquina nativo justo antes da gerência dele - e por isso adquirindo muito perto da realização nativa fora da transversal plataforma Java bytecode.
O truque deve notar que o intérprete realmente rápido que acaba de escrever - em C, para o exemplo já tem uma sequência útil do código binário nativo de cada bytecode que interpreta: o código binário que o intérprete ele mesmo realiza. Como o intérprete já se compilou de C no código binário nativo, para cada bytecode que interpreta, passa por uma sequência de instruções de código nativas do CPU de hardware no qual corre. Salvando uma cópia de cada instrução binária como se realiza, o intérprete pode guardar um log corrente do código binário que ele mesmo dirigiu para interpretar um bytecode. Pode guardar tão facilmente um log do jogo de bytecodes que dirigiu para interpretar um método inteiro.
Toma aquele log de instruções e "o orifício de observação - otimiza-o", como um compilador inteligente faz (a otimização de orifício de observação implica a toma de uma sequência curta em instruções e substituição deles com um jogo mais curto ou mais rápido de instruções). Isto elimina instruções redundantes ou desnecessárias do log e fá-lo olhar como o código binário otimizado que um bom compilador poderia ter produzido.
Observar |
Isto é onde o compilador de nome vem de, no compilador "just-in-time", mas realmente só é o fim traseiro de um compilador tradicional - a parte que realmente codifica a geração. A propósito, a parte dianteira aqui é o compilador de Java (javac). |
Aqui está onde o truque entra. A próxima vez que o método dirige-se (de exatamente o mesmo modo), o intérprete simplesmente pode realizar agora diretamente o log guardado do código nativo binário. Como isto otimiza o laço interior em cima de cada bytecode, bem como qualquer outra redundância entre o bytecodes em um método, pode ganhar um fator de 10 ou mais na velocidade. De fato, uma versão experimental desta tecnologia no Sol mostrou que os programas Java usando-o podem correr tão rápido como programas C compilados.
Observar |
O qualificador parentético no parágrafo último é necessário porque se algo for diferente sobre a entrada ao método, toma um caminho diferente pelo intérprete e deve reregistrar-se em log. (Há versões sofisticadas desta tecnologia que resolvem isto, e outro, dificuldades.) O esconderijo do código nativo de um método deve invalidar-se sempre que o método se tenha modificado, e o intérprete deve pagar a um pequeno preço a frente cada vez quando um método dirige-se pela primeira vez. Contudo, estes pequenos preços de escrituração excedem-se em peso longe pelos lucros assombrosos na velocidade possível. |
Os compiladores just-in-time, muitas vezes chamados somente compiladores de JIT, ficam cada vez mais populares, e muitos vendedores principais (inclusive Microsoft e Symantec) competem neste reino. O Internet Explorer de Microsoft 3,0 barcos com um compilador JIT já. Aprenderá mais sobre vários compiladores JIT disponíveis (ou logo ser) no Dia 28, "Emerging Technologies".
Um arquivo de classe de Java é o arquivo gerado pelo compilador de Java com uma extensão de .class. Não lhe darão o formato de arquivo de .class inteiro aqui, só um gosto de com que se parece. (Pode ler todos sobre ele na documentação de lançamento.) menciona-se aqui porque é uma das partes de Java que tem de especificar-se cuidadosamente se todas as implementações de Java deverem ser compatíveis um com outro, e se se esperar que Java bytecodes viaje através de redes arbitrais - a e de computadores arbitrais e sistemas operacionais - e ainda chegue seguramente.
O resto desta seção paráfrases, e extensivamente condensa-se, o último lançamento da documentação de arquivo de classe.
Os arquivos de classe de Java usam-se para manter as versões compiladas tanto de classes de Java como de interfaces de Java. Os intérpretes de Java complacentes devem ser capazes do procedimento com todos os arquivos de classe que se conformam com a seguinte especificação.
Um arquivo de classe de Java compõe-se de uma corrente de bytes de 8 bits. Todas as quantidades de 16 bits e de 32 bits constroem-se lendo em dois ou quatro bytes de 8 bits, respectivamente. Os bytes juntam-se em conjunto na grande-endian ordem. (Use java.io.DataInput e java.io.DataOutput para ler e escrever classificar arquivos.)
O formato de arquivo de classe apresenta-se abaixo como uma série de estruturas C-struct-like. Contudo, diferentemente de um struct C, não há enchimento ou o alinhamento entre partes da estrutura. Cada campo da estrutura pode ser do tamanho variável, e uma tabela pode ser do tamanho variável (neste caso, algum campo antes da tabela dá a dimensão da tabela). Os tipos u1, u2 e u4 representam um 1-não assinado, 2-, ou quantidade de 4 bytes, respectivamente.
Os atributos usam-se em vários lugares diferentes no formato de arquivo de classe. Todos os atributos têm o seguinte formato:
GenericAttribute_info { u2 attribute_name; u4 attribute_length; u1 info[attribute_length]; }
O attribute_name é um índice de 16 bits no consórcio constante da classe; o valor de constant_pool[attribute_name] é uma cadeia que dá o nome do atributo. O attribute_length de campanha dá o comprimento da informação subsequente em bytes. Este comprimento não inclui 6 bytes tinha de guardar attribute_name e attribute_length. Nos exemplos no resto desta seção, sempre que um atributo se necessite, os nomes de todos os atributos que se entendem atualmente enumeram-se. No futuro, mais atributos vão se acrescentar. Espera-se que os leitores de arquivo de classe omitam e ignorem a informação em qualquer atributo que não entendem.
A seguinte pseudoestrutura dá uma descrição superior do formato de um arquivo de classe:
ClassFile { u4 magic; u2 minor_version u2 major_version u2 constant_pool_count; cp_info constant_pool[constant_pool_count - 1]; u2 access_flags; u2 this_class; u2 super_class; u2 interfaces_count; u2 interfaces[interfaces_count]; u2 fields_count; field_info fields[fields_count]; u2 methods_count; method_info methods[methods_count]; u2 attributes_count; attribute_info attributes[attribute_count]; }
Aqui está uma das mais pequenas estruturas usadas:
method_info { u2 access_flags; u2 name_index; u2 signature_index; u2 attributes_count; attribute_info attributes[attribute_count]; }
Finalmente, aqui está uma amostra de uma das estruturas posteriores na descrição de arquivo de classe:
Code_attribute { u2 attribute_name_index; u2 attribute_length; u1 max_stack; u1 max_locals; u2 code_length; u1 code[code_length]; u2 exception_table_length; { u2 start_pc; u2 end_pc; u2 handler_pc; u2 catch_type; } exception_table[exception_table_length]; u2 attributes_count; attribute_info attributes[attribute_count]; }
Nenhum disto está destinado para ser completamente compreensível (embora pudesse ser capaz de adivinhar quantos membros de estrutura são para), mas somente sugestivo do tipo de estruturas que vivem dentro de arquivos de classe. Como o compilador e as fontes em tempo de execução estão disponíveis, sempre pode começar com eles se de fato tiver de ler ou escrever arquivos de classe você mesmo. Por isso, não precisa de ter uma compreensão profunda dos detalhes, até neste caso.
Como as assinaturas de método se usam em arquivos de classe, agora é um tempo apropriado para explorá-los no detalhe prometido em dias mais adiantados - mas são provavelmente os mais úteis para você escrevendo os métodos nativos da lição de ontem.
Novos termos |
A assinatura de método, neste exemplo, é uma cadeia que representa o tipo de método, campo ou tabela. |
Uma assinatura de campanha representa o valor de um argumento a um método ou o valor de uma variável e é uma série de bytes na seguinte gramática:
<field signature> := <field_type> <field type> := <base_type> | <object_type> | <array_type> <base_type> := B | C | D | F | I | J | S | Z <object_type> := L <full.ClassName> ; <array_type> := [ <optional_size> <field_type> <optional_size> := [0-9]*
Aqui estão as significações dos tipos baseados: B (byte), C (char), D (double), F (float), I (int), J (long), S (short) e Z (boolean).
Uma assinatura de tipo do regresso representa o valor de retorno de um método e é uma série de bytes na seguinte gramática:
<return signature> := <field type> | V
O V de caráter (void) indica que o método não devolve nenhum valor. De outra maneira, a assinatura indica o tipo do valor de retorno. Uma assinatura de argumento representa um argumento passado a um método:
<argument signature> := <field type>
Finalmente, uma assinatura de método representa os argumentos que o método espera e o valor que devolve:
<method_signature> := (<arguments signature>) <return signature> <arguments signature> := <argument signature>*
Vamos provar as novas regras: Um método chamado complexMethod() na classe my.package.name.ComplexClass toma três argumentos-a long, um boolean e uma tabela d-dimensional de areia de short devolve this. Então a sua assinatura de método é (JZ[[S)Lmy.package.name.ComplexClass;.
Uma assinatura de método muitas vezes prefixa-se pelo nome do método, ou pelo seu pacote cheio (usando sublinhar no lugar de pontos) e o seu nome de classe seguido de um golpe (/) e o nome do método, para formar uma assinatura de método completa. (Viu vários destes gerados em comentários de toco ontem.) Agora, finalmente, tem a história cheia! Assim, o seguinte:
my_package_name_ComplexClass/complexMethod(JZ[[S)Lmy.package.name.ComplexClass;
é a assinatura de método cheia, completa de complexMethod(). (Uf!)
Há umas décadas, os programadores tanto nas comunidades de Smalltalk como em Lisp realizaram como extremamente valioso deve ser capaz de ignorar a dealocação de memória. Realizaram que, embora a alocação seja fundamental, a dealocação se consegue no programador pela preguiça do sistema - deve ser capaz de compreender o que não é já útil, e livre-se dele. Na obscuridade relativa, estes programadores de exploração desenvolveram uma série inteira de coletores de lixo para executar este emprego, cada obtenção mais sofisticado e eficiente quando os anos foram por. Finalmente, agora que a comunidade de programação de corrente principal começou a reconhecer o valor desta técnica automatizada, Java pode tornar-se a primeira aplicação realmente comum da tecnologia que aqueles pioneiros desenvolveram.
Suponha que é programador em um parecido a uma linguagem C (provavelmente não demasiado difícil para você, porque estas línguas são os dominantes agora mesmo). Cada vez quando cria algo, algo, dinamicamente em tal língua, é completamente responsável por seguir a pista da vida daquele objeto em todas as partes do seu programa e mentalmente decidir quando estará seguro anulá-lo. Isto pode ser bastante um difícil (às vezes impossível) tarefa, porque alguma de outras bibliotecas ou métodos que chamou poderia ter "squirreled longe" um ponteiro para o objeto, sem conhecimento de você. Quando fica impossível saber, simplesmente decide anular nunca o objeto, ou pelo menos esperar até que cada chamada de método e biblioteca implicada tenha concluído, que pode ser quase como muito tempo.
A sensação preocupada vir escrevendo tal código é uma resposta natural, sã ao que é inerentemente um estilo perigoso e inseguro da programação. Se tiver a disciplina tremenda - e também todo o mundo que escreve a cada biblioteca e método que chama - pode sobreviver, em princípio, a esta responsabilidade sem demasiados infortúnios. Mas não são você ser humano? Não são eles? Devem haver alguns pequenos erros nesta disciplina perfeita devido ao erro. O que é pior, tais erros são praticamente não detectáveis, como cada um que se processa para acossar um problema de ponteiro desgarrado em C lhe dirá. Que tal milhares de programadores que não têm aquele tipo da disciplina?
Outro modo de fazer esta pergunta é: Porque deve algum programador forçar-se a ter esta disciplina quando é inteiramente possível para o sistema retirar esta carga pesada dos seus ombros?
As estimativas de engenharia de software mostraram recentemente que para cada 55 linhas da produção código parecido a C no mundo, há um defeito. Isto significa que a sua navalha elétrica tem aproximadamente 80 defeitos e a sua TV, 400. Logo terão até mais, porque o tamanho desta espécie do software de computador introduzido cresce exponencialmente. Quando começa a pensar quanto código parecido a C está no motor do seu carro, deve dar-lhe a pausa.
Muitos destes erros são devido ao abuso de ponteiros, entendendo mal ou por acaso, e à libertação primeira, incorreta de objetos alocados na memória. Java dirige ambos destes - o antigo eliminando ponteiros explícitos da língua de Java completamente e o último pela inclusão, em cada sistema de Java, um coletor de lixo que resolve o problema.
Imagine um sistema em tempo de execução que segue a pista de cada objeto que cria, avisos quando a referência última para ele desapareceu, e liberta o objeto para você. Como tal coisa pode trabalhar de fato?
Uma aproximação de força bruta, tentada cedo durante os dias da reunião de lixo, é anexar um balcão de referência a cada objeto. Quando o objeto se cria, o balcão estabelece-se em 1. Cada vez quando uma nova referência para o objeto faz-se, o balcão incrementa-se, e cada vez quando tal referência desaparece, o balcão é decremented. Como todas tais referências se controlam pelas variáveis de língua como e nomeações, para o exemplo - o compilador pode contar sempre que uma referência a objeto pudesse criar-se ou destruir-se, como faz no manejo de embeber de variáveis locais, e assim pode assistir com esta tarefa. O próprio sistema mantém o grupo de objetos de raiz que se consideram demasiado importantes para libertar-se. A classe Object é um exemplo de tal objeto de V.I.P. (V.I.O.?) Finalmente, tudo isto é necessário deve testar, depois de cada decremento, se o balcão bateu em 0. Se tiver, o objeto liberta-se.
Se pensar cuidadosamente nesta aproximação, vai se convencer logo que é definitivamente correto quando decide libertar algo. É tão simples que pode dizer imediatamente que trabalhará. O pirata de baixo nível em você também poderia sentir que se for tão simples, é provavelmente não bastante rápido para correr ao nível mais baixo do sistema - e teria razão.
Pense em todas as armações de pilha, variáveis locais, argumentos de método, valores de retorno e variáveis locais criadas no decorrer de até algumas centenas de milissegundos da vida de um programa. Para cada um destes muito pequenos, os nano-passos no programa, um extra incremento (no melhor) ou decremento, teste e dealocação (na pior das hipóteses) vão se acrescentar à duração do programa. De fato, os primeiros coletores de lixo foram bastante lentos que muitos predisseram que nunca podem usar-se em absoluto!
Afortunadamente, uma geração inteira de programadores inteligentes inventou uma grande bolsa de truques para resolver estes problemas de cima. Um truque deve introduzir o "objeto passageiro especial" áreas que não precisam de ser referência contada. O melhor destes generational que limpam coletores de lixo hoje pode tomar menos de 3 por cento do tempo total do seu programa-a feito notável se realizar que muitas outras características de língua, como despesas gerais de laço, podem ser tão grandes ou maiores!
Há outros problemas com a coleção de lixo. Se estiver libertando constantemente e reformando o espaço em um programa, não vai o montão de objetos logo ficar fragmentado, com pequenos buracos em todo lugar e nenhum quarto para criar novos, grandes objetos? Como o programador é livre agora das cadeias da dealocação manual, não criará até mais objetos do que o habitual?
O que é pior, há outro modo que este esquema de contagem de referência simples é ineficiente: em espaço em vez de tempo. Se uma cadeia longa de referências a objeto consequentemente vier o círculo cheio, atrás ao objeto inicial, a conta de referência de cada objeto pelo menos permanece 1 para sempre. Nenhum destes objetos se libertará alguma vez!
Em conjunto, estes problemas contêm que um bom coletor de lixo deve retroceder, de vez em quando, para juntar ou limpar a memória desperdiçada.
A compactação de memória ocorre quando um coletor de lixo retrocede e reorganiza a memória, eliminando os buracos criados pela fragmentação. A compactação de memória é simplesmente uma matéria de reposicionar objetos um por um em um agrupamento novo, compacto que coloca todos eles sucessivamente, deixando toda a memória livre no montão em uma grande parte.
Limpando o lixo circular que ainda está em volta depois que a contagem de referência se chama marcando e varrendo. Uma marca-e-varredura da memória implica primeiro a marcação de cada objeto de raiz no sistema e logo depois de todas as referências a objeto dentro daqueles objetos a novos objetos de marcar, e assim por diante, recursivamente. Então, quando não tem mais referências para seguir, "varre" todos os objetos desmarcados e memória compacta como antes.
As boas notícias são que isto resolve os problemas espaciais que tinha. As más notícias são que quando o coletor de lixo retrocede e faz estas operações, um período de tempo não-trivial passa durante o qual o seu programa é incapaz de correr - todos os seus objetos estão marcando-se, varrendo-se, reajustando-se, e assim por diante, no que parece um procedimento uninterruptible. A sua primeira insinuação a uma solução é a palavra "parece".
A reunião de lixo pode fazer-se de fato um pouco de uma vez, entre ou na paralela com a execução de programa normal, assim dividir o grande período de tempo tinha de retroceder no numeroso tão pequeno não os nota pedaços do tempo que acontecem entre as fendas. (Naturalmente, os anos do pensamento inteligente entraram nos algoritmos abstrusos que permitem tudo isso!)
Um problema final que poderia importuná-lo um pouco tem a ver com estas referências a objeto. Estas referências não se espalham em todas as partes do seu programa e não somente enterram-se em objetos? Mesmo se só estiverem em objetos, não têm de modificar-se sempre que o objeto para que apontam se mova por estes procedimentos? A resposta a ambas destas perguntas é um ressonante sim, e a superação deles é a barreira final à criação de um coletor de lixo eficiente.
Há realmente só duas escolhas. O primeiro, força bruta, supõe que toda a memória que contém referências a objeto tenha de procurar-se em uma base regular, e sempre que as referências a objeto encontradas por esta pesquisa combinem com objetos que se moveram, a velha referência modifica-se. Isto supõe que haja ponteiros "sólidos" nos da memória do montão que apontam diretamente para outros objetos. Introduzindo várias espécies de ponteiros "suaves", inclusive ponteiros que se parecem com endereços de expedição, o algoritmo melhora-se muito. Embora estas aproximações de força bruta pareçam lentas, resulta que os computadores modernos podem fazê-los bastante rápido para ser úteis.
Observar |
Poderia admirar-se como as técnicas de força bruta identificam referências a objeto. Em primeiros sistemas, as referências marcaram-se especialmente com um bit de ponteiro assim podem localizar-se inequivocamente. Agora, os assim chamados coletores de lixo conservadores simplesmente supõem isto se parecer a uma referência a objeto, ele ser pelo menos com os objetivos da marca-e-varredura. Depois, tentando de fato atualizá-lo, podem descobrir se realmente é uma referência a objeto. |
A aproximação final do manejo de referências a objeto e aquela que Java atualmente usa, também é um dos muito primeiros tentados. Implica a utilização ponteiros suaves de 100 por cento. Uma referência a objeto é de fato uma maçaneta, às vezes chamada um OOP, ao verdadeiro ponteiro, e uma grande mesa de objeto existe para fazer o mapa destas maçanetas na referência a objeto real. Embora isto realmente introduza extra em cima em quase cada referência a objeto (alguns dos quais podem eliminar-se por truques inteligentes, como poderia adivinhar), não é um preço demasiado alto para pagar por este nível inacreditavelmente valioso de vias indiretas.
Estas vias indiretas permitem ao coletor de lixo, por exemplo, marcar, varrer, mover ou examinar um objeto de uma vez. Cada objeto pode sair-se independentemente de baixo de um programa Java corrente modificando só as entradas na tabela de objeto. Isto não só permite à fase retroceder acontecer nos passos mais muito pequenos, mas faz um coletor de lixo que corre literalmente na paralela com o seu programa muito mais fácil escrever. Isto é o que o coletor de lixo de Java faz.
Aviso |
Tem de ter muito cuidado sobre a coleção de lixo quando faz programas críticos, em tempo real (como os mencionados ontem que legitimamente necessitam métodos nativos) - mas com que frequência o seu Java codificará estar voando um avião comercial em tempo real, de qualquer maneira? |
Java aplica quase todas estas técnicas promovidas para dar-lhe um coletor de lixo rápido, eficiente, paralelo. Correndo em um fio separado, limpa o ambiente de Java de quase todo o lixo (é conservador), silenciosamente e em background; é eficiente em ambo o espaço e tempo; e nunca retrocede por mais que uma pouca quantidade do tempo. Nunca precisaria de saber que está lá.
A propósito, se quer forçar uma coleção de lixo de marca-e-varredura cheia a acontecer logo, pode fazer então simplesmente chamando o método de System.gc(). Poderia querer fazer isto se somente libertou uma maioria da memória do montão no lixo circular, e querem tudo isso levado rapidamente. Também poderia chamar isto sempre que seja ocioso, como uma insinuação ao sistema sobre quando seria o melhor para vir e reunir o lixo.
De maneira ideal, nunca notará que o coletor de lixo, e todas aquelas décadas de programadores que malham os seus cérebros da sua parte simplesmente o deixará dormir melhor na noite - e o que está enganado com isto?
A fala do sono bem à noite, se ainda não retrocedeu e disse, "A minha bondade! Subentende que os programas Java estarão correndo exuberante na Internet!?!" melhor faz assim agora, já que é um assunto legítimo. De fato, é um dos blocos de tropeção técnicos principais (os outros que são pela maior parte sociais e econômicos) à realização do sonho de ubiquidade e código que compartilha para Java mencionado antes na lição de hoje.
Qualquer tecnologia potente, flexível pode abusar-se. Como a Rede torna-se a corrente principal e comum, também, se abusará. Já, houve muitos pontos luminosos nas telas de radar de segurança daqueles de nós que se incomodamos com tais coisas, avisando que (pelo menos até hoje) não bastante atenção se prestou pela indústria de computador (ou os meios de comunicação) à solução de alguns problemas que este novo mundo traz com ele. Um dos benefícios de construtivamente resolver a segurança definitivamente será uma florescência não vista antes nas comunidades virtuais da Rede; as novas economias inteiras baseadas em atenção de pessoas e criatividade vão a primavera à vida, rapidamente transformando o nosso mundo de modos novos e positivos.
O aspecto negativo a toda esta nova tecnologia é que nós (ou alguém!) deve incomodar-se muito tempo e muito sobre como fazer os playgrounds do futuro cofre das nossas crianças, e de nós. Afortunadamente, Java é uma grande parte da resposta.
O que me dá confiança que a língua de Java e o ambiente estarão seguros, que resolverá os problemas tecnicamente intimidantes e extremamente espinhosos inerentes a alguma boa forma da segurança, especialmente para redes?
Uma razão simples é a história das pessoas e a companhia, que criou Java. Muitos deles são os programadores muito inteligentes mencionados em todas as partes do livro, quem ajudou o pioneiro muitas das ideias que fazem Java grande e quem trabalharam muito sobre as décadas para fazer técnicas como coleção de lixo uma realidade dominante. São tecnicamente capazes de montagem e solução dos problemas difíceis que têm de resolver-se.
Os Microsistemas de sol, companhia, têm empurrado redes como o tema central de todo o seu software durante mais de uma década. O sol tem os engenheiros e o compromisso tinha de resolver estes problemas difíceis, porque estes mesmos problemas estão no mesmo centro tanto do seu futuro negócio como da sua visão do futuro, no qual a ligação em rede é o centro de tudo - e as redes globais são quase inúteis sem boa segurança. Somente neste ano, o Sol promoveu o estado da arte na segurança de Internet fácil a trabalhar com os seus novos produtos de SunScreen, e ordenou Whitfield Diffie a vigiá-los, quem é o homem que descobriu as ideias subjacentes nas quais essencialmente todas as formas interessantes da encriptação moderna se baseiam.
Bastante em contexto profundo. O que o ambiente de Java fornece agora mesmo que me ajuda a sentir-se seguro?
Java protege-o contra o código de Java "sórdido" potencial via uma série da defesa entrelaçada que, em conjunto, forma uma barreira imponente a algum e todos tais ataques.
Aviso |
Naturalmente ninguém pode protegê-lo da sua própria ignorância ou descuido. Se for a espécie da pessoa que cegamente carrega do binário executables do seu browser de Internet e os dirige precisa de ler não mais longe! Já está em mais perigo do que Java posará alguma vez. Como um usuário deste novo meio potente, a Internet, deve educar-se às ameaças possíveis que este mundo novo e excitante implica. Especialmente, carregar "de macros de gerência de auto" ou leitura de e-mail com "anexos executáveis" é quase tanto uma ameaça como carregar de binários da Rede e dirigi-los. Java não introduz nenhum novo perigo aqui, mas sendo o primeiro uso dominante do código executável e móvel da Rede, é responsável por fazer pessoas que repentinamente sabem os perigos que sempre estavam lá. Java já é, como verá logo, muito menos perigoso do que alguma destas atividades comuns sobre a Rede, e pode fazer-se mais seguro ainda dentro de algum tempo. A maioria destas outras (perigosas) atividades nunca podem fazer-se seguras. Assim, por favor, não os faça! Uma boa regra do polegar na Rede é: não carregue de nada que planeja realizar (ou isto vai se realizar automaticamente para você) exceto de alguém (ou alguma companhia) sabe bem e com quem teve experiência pessoal, positiva. Se não se preocupar com a perda de todos os dados sobre a sua unidade de disco rígido, ou sobre o seu isolamento, pode fazer algo do qual você gosta, mas para a maioria de nós, esta regra deve ser lei. Afortunadamente, Java permite-lhe relaxar aquela lei. Pode dirigir Java applets de cada um, em todo o caso, na segurança relativa. |
Os mecanismos de segurança potentes de Java atuam a quatro níveis diferentes da arquitetura de sistema. Em primeiro lugar, a própria língua de Java projetou-se para estar segura, e o compilador de Java assegura que o texto fonte não viola estas regras de segurança. Em segundo lugar, todos os bytecodes realizados pelo tempo de execução protegem-se para estar seguros que também obedecem a estas regras. (Esta camada os guardas contra ter um compilador alterado produzem o código que viola as regras de segurança.) Terço, o carregador de classe assegura que as classes não violam namespace ou restrições de acesso quando se carregam no sistema. Finalmente, a segurança API-específica impede applets de fazer coisas destrutivas. Esta camada final depende das garantias de integridade e segurança de outras três camadas.
Vamos examinar agora cada uma destas camadas à sua vez.
A língua de Java e o seu compilador são a primeira linha da defesa. Java projetou-se para ser uma língua segura.
A maior parte outro parecido às linguagem C tem facilidades para controlar o acesso a estruturas parecidas a um objeto, mas também ter modos de ganhar o acesso heterodoxo a objetos (ou a partes de objetos), normalmente por (MIS-) utilização de ponteiros. Isto introduz duas falhas de segurança fatais a qualquer sistema baseou-se nestas línguas. Cada um não é aquele nenhum objeto pode proteger-se do exterior de modificação, duplicação ou spoofing (outros objetos que pretendem ser aquele objeto). O outro é isto uma língua com ponteiros potentes com maior probabilidade terá defeitos sérios aquela segurança de compromisso. Estes defeitos de ponteiro, onde um ponteiro começa a modificar a memória de algum outro objeto, foram responsáveis pela maioria do público (e "não assim público") problemas de segurança na Internet isto na década passada.
Java elimina estas ameaças em um golpe eliminando ponteiros da língua completamente. Ainda há os ponteiros de umas referências de objeto gentil - mas estes se controlam cuidadosamente para estar seguros: são unforgeables, e todas as formas verificam-se para a legalidade antes de permitir-se. Além disso, as novas facilidades de tabela potentes em Java não só ajudam a compensar a perda de ponteiros, mas acrescentar a segurança adicional forçando estritamente limites de tabela, pegando mais defeitos do programador (defeitos que, em outras línguas, poderiam levar inesperado e, por isso, mau tipo problemas exploráveis).
A definição de língua e os compiladores que o forçam, criam uma barreira potente a qualquer programador de Java com más intenções.
Como uma maioria esmagadora do software de compreensão Líquida na Internet pode ser logo Java, a sua definição de língua segura e os compiladores ajudam a garantir que a maioria deste software tem um corpo sólido, base segura. Com menos defeitos, o software Net será propriedade mais predizível-a que frustra ataques.
E se aquele programador com más intenções se torna um pouco mais decidido, e reescreve o compilador de Java para ajustar os seus objetivos nefandos? O tempo de execução de Java, adquirindo a ação do leão do seu bytecodes da Rede, nunca pode contar se aqueles bytecodes se geraram por um compilador digno de confiança. Por isso, deve verificar que encontram todas as exigências de segurança.
Antes de dirigir qualquer bytecodes, o tempo de execução submete-os a uma série rigorosa de testes que variam na complexidade de cheques de formato simples durante todo o tempo à gerência de um provador de teorema para assegurar-se que jogam segundo as regras. Estes testes verificam que os bytecodes não forjam ponteiros, violam restrições de acesso, objetos de acesso tão outros do que o que são (os objetos de InputStream sempre se usam como InputStream objeta, e nunca como algo mais), chame métodos com valores de argumento impróprios ou tipos, nem inunde a pilha.
Considere que seguinte Java codifica a amostra:
public class VectorTest { public int array[]; public int sum() { int[] localArray = array; int sum = 0; for (int i = localArray.length; -i >= 0; ) sum += localArray[i]; return sum; } }
O bytecodes gerou quando este código se compila olhada algo como o seguinte:
aload_0 Load this getfield #10 Load this.array astore_1 Store in localArray iconst_0 Load 0 istore_2 Store in sum aload_1 Load localArray arraylength Gets its length istore_3 Store in i A: iinc 3 -1 Subtract 1 from i iload_3 Load i iflt B Exit loop if < 0 iload_2 Load sum aload_1 Load localArray iload_3 Load i iaload Load localArray[i] iadd Add sum istore_2 Store in sum goto A Do it again B: iload_2 Load sum ireturn Return it
Observar |
Os exemplos excelentes e as descrições nesta seção do livro parafraseiam-se da "Segurança de nível baixo tremendamente informativa em Java" papel de Frank Yellin. Pode ler este documento em http://java.sun.com:80/sfaq/verifier.html. |
Java bytecodes codifica mais informação sobre tipo do que é estritamente necessário para o intérprete. Embora, por exemplo, o aload e os códigos de operação de iload façam exatamente a mesma coisa, aload sempre se usa para carregar uma referência a objeto e iload costumou carregar um número inteiro. Alguns bytecodes (como getfield) incluem uma referência a tabela de símbolos - e aquela tabela de símbolos tem até mais informação sobre tipo. Esta extra informação sobre tipo permite ao sistema em tempo de execução garantir que os objetos de Java e os dados não se manipulam ilegalmente.
Conceptualmente, antes e depois que cada bytecode realiza-se, cada fenda na pilha e cada variável local têm algum tipo. Esta coleção da informação sobre tipo - todas fendas e variáveis locais - chama-se o estado de tipo do ambiente de execução. Uma exigência importante do estado de tipo de Java consiste em que deve ser determinável estaticamente pela indução - isto é, antes que qualquer código de programa se realize. Por conseguinte, como o sistema em tempo de execução lê bytecodes, cada um deve ter a seguinte propriedade indutiva: considerando só o estado de tipo antes da execução do bytecode, o estado de tipo posteriormente deve determinar-se totalmente.
Considerando bytecodes de linha reta (nenhum ramo) e começando com um estado de pilha conhecido, o estado de cada fenda na pilha, por isso, sempre se conhece. Por exemplo, começando com uma pilha vazia:
iload_1 Load integer variable. Stack type state is I. iconst 5 Load integer constant. Stack type state is II. iadd Add two integers, producing an integer. Stack type state is I.
Observar |
Smalltalk e PostScript bytecodes não têm esta restrição. O seu comportamento de tipo mais dinâmico realmente cria a flexibilidade adicional naqueles sistemas, mas Java tem de fornecer um ambiente de execução seguro. Por isso, deve saber todos os tipos sempre para garantir certo nível da segurança. |
Outra exigência feita pelo tempo de execução de Java consiste em que quando o grupo bytecodes pode tomar mais de um caminho para chegar ao mesmo ponto, todos tais caminhos devem chegar lá com exatamente o mesmo estado de tipo. Isto é uma exigência estrita e contém, por exemplo, que os compiladores não podem gerar bytecodes que carregam todos os elementos de uma tabela para a pilha. (Como cada vez por tal laço as modificações de estado de tipo da pilha, a partida do laço - "o mesmo ponto" em múltiplos caminhos - teria mais de um estado de tipo, que não se permite.)
Bytecodes verificam-se para a complacência com todas estas exigências, usando a extra informação sobre tipo no arquivo de classe, por uma parte do tempo de execução chamado o verificador. Examina cada bytecode à sua vez, construindo o estado de tipo cheio como vai e verifica que todos os tipos de parâmetros, argumentos e resultados são corretos. Assim, o verificador atua como um porteiro ao seu ambiente em tempo de execução, deixando entrar só aqueles bytecodes aquela revista de tropas de passo.
Aviso |
O verificador é a parte crucial da segurança de Java, e depende do que tem um corretamente implementado (nenhum defeito, intencional ou de outra maneira) sistema em tempo de execução. Desde esta escrita, só o Sol produz tempos de execução de Java (e os autorizando a companhias como Netscape e Microsoft do uso nos seus browseres), e são seguros. No futuro, contudo, deve ter cuidado carregando ou comprando outra companhia (ou indivíduo) a versão do ambiente de tempo de execução de Java. Consequentemente, o Sol implementará suites de validação de tempos de execução, compiladores, e assim por diante para estar seguro que estão seguros e corretos. Entrementes, cuidado, comprador! O seu tempo de execução é a base na qual todo o resto da segurança de Java se constrói, então assegure-se que é uma base boa, sólida, segura. |
Quando bytecodes passaram o verificador, garantem-se para não fazer algum do seguinte: cause qualquer pilha de operand abaixo de - ou excessos; use parâmetro, argumento ou tipos de regresso incorretamente; ilegalmente converta dados de um tipo ao outro (de um número inteiro a um ponteiro, por exemplo); ou acessam os campos de qualquer objeto ilegalmente (isto é, o verificador verifica que obedecem às regras de public, private, package e protected).
Como um bônus acrescentado, porque o intérprete pode contar agora com todos estes fatos que são verdadeiros, pode correr muito mais rápido do que antes. Todos os cheques necessários da segurança arrumaram-se frente, portanto pode correr no regulador cheio. Além disso, as referências a objeto podem tratar-se agora como capacidades, porque são unforgeable-capacidades permitem, por exemplo, a modelos de segurança promovidos de entrada-saída de arquivo e autenticação construir-se seguramente em cima de Java.
Observar |
Como pode acreditar agora que uma variável de private realmente seja privada, e que nenhum bytecode pode executar alguma magia com formas para extrair a informação dela (como o seu número de cartão de crédito), muitos dos problemas de segurança que poderiam surgir em outro, os ambientes menos seguros simplesmente desaparecem! Estas garantias também fazem barreiras erigem contra o destrutivo applets possível, e mais fácil. Como o sistema de Java não tem de incomodar-se com bytecodes perigoso, pode continuar a criação de outros níveis da segurança que quer fornecer-lhe. |
O carregador de classe é outra espécie do porteiro, embora um de nível mais alto. O verificador é a segurança do recurso último. O carregador de classe é a segurança do primeiro recurso.
Quando uma nova classe se carrega no sistema, coloca-se em (vidas em) um de vários reinos diferentes. Comumente, há três reinos possíveis: o seu computador local, rede local guardada pelo firewall na qual o seu computador se localiza, e a Internet (a Rede global). Cada um destes reinos trata-se diferentemente pelo carregador de classe.
Observar |
De fato, podem haver tantos reinos como o seu nível desejado da segurança (ou paranóia) necessita. Isto é porque o carregador de classe é sob o seu controle. Como um programador, pode fazer o seu próprio carregador de classe que implementa a sua própria marca peculiar da segurança. (Isto é um passo radical; deveria dar aos usuários do seu programa um ramo inteiro de classes - e dão-lhe um lote inteiro da confiança - para realizar isto.) |
Especialmente, o carregador de classe nunca permite a uma classe de um reino menos protegido substituir uma classe de um reino mais protegido. Os primitivos de entrada-saída do sistema de arquivos, com os quais deve muito preocupar-se (e justamente assim), definem-se todos em uma classe de Java local, que significa que todos eles vivem no reino de computador local. Assim, nenhuma classe do exterior do seu computador (da rede local supostamente digna de confiança ou da Internet) pode tomar o lugar destas classes e imitação código de Java na utilização das versões sórdidas destes primitivos. Além disso, as classes em um reino não podem invocar aos métodos de classes em outros reinos, a menos que aquelas classes tenham declarado explicitamente aqueles métodos public. Isto contém que as classes do outro do que o seu computador local não podem até ver os métodos de entrada-saída de sistema de arquivos, muito menos chame-os, a menos que você ou o sistema os queiram a.
Além disso, cada novo applet carregado da rede coloca-se em um namespace parecido a um pacote separado. Isto significa que applets se protegem até um de outro! Nenhum applet pode acessar métodos de alguém outro (ou variáveis) sem a sua cooperação. Applets do interior do firewall pode até tratar-se diferentemente daqueles do lado de fora do firewall, se você gostar.
Observar |
De fato, é tudo um pouco mais complexo do que isto. No lançamento atual, um applet está em um pacote namespace junto com qualquer outro applets daquela fonte. Esta fonte ou origem, é muitas vezes anfitrião (nome de domínio) na Internet. Este "subreino" especial usa-se extensivamente na seguinte seção. Dependendo de onde a fonte se localiza, do lado de fora do firewall ou no interior, as novas restrições podem aplicar-se (ou retirar-se inteiramente). Este modelo provavelmente vai se estender em futuros lançamentos de Java, fornecendo um grau até mais perfeito do controle sobre o qual as classes vêm para fazer que. |
O carregador de classe essencialmente partilha o mundo de classes de Java no pequeno, protegeu pequenos grupos, sobre os quais pode fazer seguramente suposições que sempre serão verdade. Este tipo da predição é a chave a programas bem-comportados e seguros.
Viu agora a vida cheia de um método. Começa como texto fonte em algum computador, compila-se em bytecodes em alguns (possivelmente diferente) computador e então pode viajar (como um arquivo de .class) em qualquer sistema de arquivos ou rede em qualquer parte do mundo. Quando dirige um applet em um browser permitido por Java (ou carrega de uma classe e o dirige pela mão usando java), os bytecodes do método extraem-se do seu arquivo de .class e cuidadosamente folheiam-se pelo verificador. Depois que se declaram seguros, o intérprete pode realizá-los para você (ou um gerador de códigos pode gerar o código binário nativo para eles usando o compilador just-in-time ou java2c, e logo dirigir aquele código nativo diretamente).
Em cada etapa, cada vez mais a segurança acrescenta-se. O nível final daquela segurança é a própria biblioteca de classe de Java, que tem várias classes cuidadosamente projetadas e APIs que acrescentam os toques finais à segurança do sistema.
SecurityManager é uma classe de abstract que se reúne, em um lugar, todas as decisões de política de segurança que o sistema tem de fazer como bytecodes corrida. Aprendeu antes que pode criar o seu próprio carregador de classe. De fato, não pode ter a, porque pode subclassificar SecurityManager para executar a maioria dos mesmos fazimentos por encomenda.
Um exemplo de alguma subclasse de SecurityManager sempre se instala como o gerente de segurança atual. Tem o controle completo sobre o qual de um jogo bem definido de métodos potencialmente perigosos se permitem chamar-se por qualquer classe dada. Toma os reinos da seção última em conta, a fonte (a origem) da classe e o tipo da classe (autônomo ou carregado por um applet). Cada um destes pode configurar-se separadamente para ter o efeito você (o programador) como no seu sistema de Java. Observe que os ambientes como Navegadores da Web tipicamente já têm um gerente de segurança no lugar de tratar a segurança applet básica (todas as restrições sobre as quais aprendeu por enquanto neste livro), e aquele gerente de segurança não pode substituir-se ou modificar-se.
Qual é este "jogo bem definido" de métodos que se protegem?
A entrada-saída de arquivo é uma parte do jogo, por razões óbvias. Applets, à revelia, pode abrir-se, ler ou escrever arquivos no sistema local.
Também neste jogo protegido são os métodos que criam e usam conexões de rede, tanto de entrada como que partem.
Os membros finais do jogo são aqueles métodos que permitem um fio a acesso, controle, e manipulam outros fios. (Naturalmente, os métodos adicionais podem proteger-se também, criando uma nova subclasse de SecurityManager que os trata.)
Há uma terra meia entre as restrições "de caixa de areia" que applets sempre têm e uma aplicação que pode enlouquecer livremente no seu sistema. Esta terra meia implica o estabelecimento onde um applet vem de, portanto podem permitir a fontes applet diferentes tipos diferentes do acesso.
Por exemplo, poderia especificar grupos diferentes de domínios confiados (companhias), cada uma das quais se permite por privilégios acrescentados quando applets daquele grupo se carregam. Applets de sistemas em uma rede interna, por exemplo, são mais dignos de confiança do que applets da Internet em liberdade. Podem mais confiar a alguns grupos do que outros, e poderia até permitir a grupos crescer automaticamente permitindo a membros existentes recomendar a novos membros da admissão. (O selo de Java de aprovação?)
Em todo o caso, as possibilidades são infinitas, enquanto há um modo seguro de reconhecer o criador original de um applet.
Poderia pensar que este problema já se resolveu, porque as classes se marcam com a sua origem. De fato, o tempo de execução de Java vai longe fora do seu modo de estar seguro que aquela informação sobre origem nunca se perde - qualquer método de execução pode restringir-se dinamicamente por esta informação em qualquer lugar na cadeia de chamada. Então, por que não é isto bastante?
Como o que você realmente gostaria de ser capaz de fazer é permanentemente "marcar" um applet com o seu criador original (a sua origem verdadeira), e não importa onde viajou, um browser pode verificar a integridade e autenticar o criador disto applet.
Se de qualquer maneira aqueles applets se marcassem irrevogavelmente com uma assinatura digital pelo seu criador, e aquela assinatura também pode garantir que o applet não se tinha mexido, seria de ouro.
Afortunadamente, o Sol planeja fazer exatamente que para Java, para ter um mecanismo para usar a criptografia de chave pública para "assinar" um fragmento do código (um applet, uma aplicação, uma classe única) para que possa contar confiantemente e com segurança quem criou ou verificou que a parte do código de Java foi digna de confiança. Com a assinatura no lugar, aquele código então pode libertar-se da caixa de areia e usar o sistema local mais livremente. Espere esta capacidade de ficar mais popular e disponível no futuro.
Prometido em Java 1.1 é o grupo de extensão APIs de segurança gerente e encriptação em Java. Estas novas classes incluem o suporte das capacidades de assinatura digitais mencionadas na seção prévia, bem como criptografia de baixo nível e de alto nível de uso geral, gestão-chave, listas de controle de acesso, bagunças de sumário de mensagem (MD5), e outros instrumentos e utilidade. No Dia 27, "a Extensão Padrão APIs", aprenderá mais sobre as novas classes de segurança em Java.
Hoje aprendeu sobre a visão grande que alguns de nós têm para Java, e sobre o futuro excitante promete.
Abaixo do capuz, o trabalho interior da máquina virtual, o intérprete bytecode (e todo o seu bytecodes), o coletor de lixo, o carregador de classe, o verificador, o gerente de segurança e as características de segurança potentes de Java revelou-se tudo.
Agora sabe quase bastante para escrever um ambiente de tempo de execução de Java do seu próprio - mas afortunadamente, não tem a. Simplesmente pode carregar do último lançamento de Java - ou usar um browser permitido por Java para gostar da maioria dos benefícios de Java imediatamente.
Ainda sou um pouco pouco nítido sobre porque a língua de Java e o compilador fazem a Rede mais segura. Somente não podem "evitar-se" por bytecodes sórdido? | |
Sim, podem - mas não se esquecer de que o ponto inteiro de usar uma língua segura e compilador deveu fazer a Rede no conjunto mais segura como se escreve mais código de Java. Uma maioria esmagadora deste código de Java vai se escrever por programadores de Java honestos, que produzirão bytecodes seguro. Isto faz a Rede mais predizível dentro de algum tempo, e assim mais segura. | |
Sei que disse que a coleção de lixo é algo que não tenho de importunar com, mas e se quiser (ou necessidade) a? | |
Deste modo, planeja voar um avião com Java. Esfrie! Para somente tais casos, há um modo de perguntar o tempo de execução de Java, durante o lançamento (java -noasyncgc), não dirigir a coleção de lixo a menos que não conseguido para, por uma chamada explícita (System.gc()) ou ficando sem memória. (Isto pode ser bastante útil se tiver múltiplos fios que estragam um a outro e querem parar o fio de gc de chegar o caminho testando-os.) Não se esquecem de que virar coleção de lixo de meios que qualquer objeto cria viverá um longo tempo, longo. Se for em tempo real, nunca quer retroceder para um gc cheio - assim estão seguros a objetos de reutilização muitas vezes e não criem demasiados deles! | |
Lá há algo mais que posso fazer ao coletor de lixo? | |
Também pode forçar os métodos de finalize() de qualquer objeto recentemente libertado a chamar-se imediatamente via System.runFinalization(). Poderia querer fazer isto se está a ponto pedem alguns recursos que suspeita ainda poderia amarrar-se por objetos que se vão mas não se esquecem (esperando por finalize()). Isto é até mais raro do que o começo de um gc à mão, mas se menciona aqui para a perfeição. | |
Ouvi de um instrumento chamado java2c, que converteria o código de Java no código de C. Isto existe? Onde posso adquiri-lo? | |
Um tradutor de java2c experimental constou-se existir dentro do Sol, mas nunca se pôs em liberdade. Pode lançar-se mais tarde. | |
Qual é a palavra última em Java? | |
Java acrescenta muito mais do que pode levar alguma vez. Sempre fazia assim para mim, e agora, espero que vá para você também. O futuro da Rede enche-se de horizontes as-yet-undreamt, e o caminho é longo e difícil, mas Java é um grande companheiro de viagem. |