[Geral] Perguntas básicas da entrevista clássica sobre computador, dia 1
1. A composição do JVM
- Carregador de classes (Class Loader): Responsável por carregar classes Java compiladas na JVM e carregar dinamicamente as classes necessárias em tempo de execução.
- Área de dados de tempo de execução: É a área de gerenciamento de memória da JVM, incluindo principalmente a área de método, heap, pilha, contador de programa, etc.
- Mecanismo de Execução: Responsável por executar instruções de bytecode, que podem ser implementadas por intérpretes, compiladores just-in-time (JIT), etc.
- Coletor de Lixo: Responsável por gerenciar automaticamente a alocação e liberação de memória heap e reciclar a memória de objetos inúteis.
- Interface nativa: permite que o código Java chame código escrito em C, C++ local e outras linguagens.
- JNI (Java Native Interface): permite que o código Java interaja com o código local e chame métodos escritos em C, C++ local e outras linguagens.
2. Compreensão de heap e pilha em jvm
- Área de Método: A área de método faz parte da JVM e é usada para armazenar informações estruturais de uma classe, como campos de classe, métodos, pools de constantes, variáveis estáticas, etc. É uma área compartilhada por todos os threads. Na especificação da JVM, não há uma disposição clara para a implementação da área de método. Diferentes implementações de JVM podem ter diferentes implementações da área de método.
- Heap: O heap é a área da JVM onde as instâncias dos objetos são armazenadas.É a área de memória alocada para objetos criados no programa Java. O heap é uma área compartilhada por todos os threads. Um heap é criado quando a JVM é iniciada e o tamanho do heap pode ser ajustado por meio de parâmetros de inicialização. A pilha é dividida em geração jovem e geração velha, e a geração jovem é dividida em espaço Éden e espaço Sobrevivente (De e Para).
- Espaço Éden: A alocação inicial de objetos é realizada no espaço Éden. Quando o espaço Eden está cheio, o Minor GC é acionado e os objetos sobreviventes são copiados para o espaço Survivor.
- Espaço Sobrevivente: armazena objetos que sobreviveram ao espaço Éden.Quando o espaço Sobrevivente está cheio, o GC Menor é acionado e os objetos sobreviventes são copiados para outro espaço Sobrevivente, e o espaço Sobrevivente atual é limpo ao mesmo tempo.
- Geração antiga: armazena objetos de longa duração.Quando a geração antiga está cheia, o Full GC é acionado.
- Pilha: A pilha é uma área privada de thread na JVM, usada para armazenar variáveis locais de métodos, pilhas de operandos, valores de retorno de métodos, informações de tratamento de exceções, etc. Cada thread cria um quadro de pilha ao executar um método. O quadro de pilha armazena as variáveis locais do método, a pilha de operandos e outras informações. À medida que os métodos são chamados e retornados, os frames da pilha são enviados e exibidos.
- Contador de programa: O contador de programa é uma área privada de thread na JVM que é usada para registrar o endereço das instruções de bytecode executadas pelo thread atual. Cada thread possui um contador de programa independente, que aponta para a instrução em execução no momento. O valor do contador de programa do thread atual é salvo e restaurado quando o thread é trocado. O contador do programa é privado do thread, o estouro de memória não pode ocorrer e a coleta de lixo não é executada.
3. Três maneiras de carregar classes Java
O carregamento da classe Java refere-se ao processo de carregamento do arquivo de bytecode de uma classe na JVM e inicialização e conexão da classe. Existem três maneiras de carregar classes Java:
- Carregamento implícito (carregamento implícito de classe): Quando o programa usa a palavra-chave new para criar um objeto ou chama um método estático ou variável estática durante a execução, a JVM carrega automaticamente a classe correspondente. Este é o método de carregamento de classe mais comum e também o método de carregamento de classe padrão.
- Carregamento explícito (carregamento explícito de classe): Use o método Class.forName() para carregar explicitamente a classe. O método Class.forName() carregará a classe com base no nome totalmente qualificado da classe fornecida (incluindo o nome do pacote) e retornará o objeto Class correspondente. Este método pode carregar classes dinamicamente e decidir qual classe carregar com base nas condições de tempo de execução.
- Carregamento passivo (carregamento passivo de classe): Quando uma classe é referenciada, mas não é realmente usada, a classe não será carregada. A classe só será carregada quando for realmente usada. Cenários comuns de carregamento passivo incluem referência a campos estáticos da classe pai por meio de subclasses, referência a classes por meio de definições de array, constantes armazenadas no pool de constantes da classe chamadora durante a fase de compilação, etc.
4. Conte-me sobre sua compreensão do mecanismo de delegação pai em Java.
O mecanismo de delegação pai é um dos mecanismos de carregamento de classe Java. Sua ideia central é que quando um carregador de classes precisar carregar uma classe, ele primeiro a delegará ao carregador de classes pai para tentar carregá-la. Somente quando o carregador de classes pai não puder carregar a classe, ela será carregada pela classe atual carregador. Isso pode garantir a exclusividade e a segurança da classe e evitar carregamentos e carregamentos repetidos de código malicioso. O mecanismo de delegação pai desempenha um papel importante em Java, permitindo o compartilhamento e a reutilização de classes.
5. Por favor, diga-me o que você entende de gc.
GC (Garbage Collection) é um mecanismo automático de gerenciamento de memória usado para recuperar automaticamente o espaço de memória ocupado por objetos que não são mais usados. Ele completa o processo de coleta de lixo por meio de etapas como marcação, limpeza e compactação. Os programadores não precisam acioná-lo manualmente, ele é executado automaticamente pelo coletor de lixo da JVM. A codificação razoável e o gerenciamento de memória podem maximizar a função do GC e melhorar o desempenho e a estabilidade do programa.
6. Como expandir a capacidade do hashmap
Quando o HashMap insere elementos, ele julgará se precisa expandir de acordo com o fator de carga. O fator de carga refere-se à relação entre o número de elementos armazenados na tabela hash e a capacidade real.
Quando o fator de carga do HashMap exceder o limite definido (o padrão é 0,75), a operação de expansão será acionada. A expansão criará uma nova tabela hash maior e redistribuirá os elementos originais para a nova tabela hash para reduzir conflitos de hash e melhorar a eficiência da consulta.
O processo de expansão do HashMap inclui aproximadamente as seguintes etapas:
- Crie uma nova tabela hash com o dobro da capacidade da tabela hash original. A capacidade da nova tabela hash é geralmente escolhida como a potência de 2 que é mais próxima e maior que a capacidade original.
- Percorra cada intervalo na tabela hash original, recalcule o valor hash dos elementos no intervalo e atribua-os ao intervalo correspondente na nova tabela hash. Esta etapa recalcula o valor hash e a posição do índice do elemento para garantir que a posição do elemento na nova tabela hash seja alterada.
- Defina a nova tabela hash como a tabela hash atual e a tabela hash original se tornará um objeto de lixo aguardando a coleta de lixo.
A operação de expansão pode ter certo impacto no desempenho porque os hashes precisam ser recalculados e os elementos realocados. Para reduzir a frequência de expansão da capacidade, você pode controlar o equilíbrio entre a capacidade e o desempenho do HashMap ajustando o fator de carga. Um fator de carga menor fará com que a tabela de hash se expanda mais rapidamente, mas ocupará mais espaço de memória. Um fator de carga maior reduzirá o número de expansões, mas poderá causar mais conflitos de hash.
7. Fale sobre sua compreensão de hashtable e currenthashmap
Hashtable e ConcurrentHashMap são implementações de tabela hash seguras para threads em Java. Eles têm algumas semelhanças em funcionalidade e uso, mas existem algumas diferenças na implementação interna e no desempenho.
- Hashtable: Hashtable é a primeira implementação de tabela hash introduzida. É thread-safe e todas as operações são sincronizadas (implementadas por meio da palavra-chave sincronizada). Devido à sincronização, o desempenho do HashTable em um ambiente multithread é relativamente baixo e a segurança do thread só pode ser garantida permitindo que apenas um thread o acesse ao mesmo tempo.
- ConcurrentHashMap: ConcurrentHashMap é uma implementação de tabela hash segura para threads de alto desempenho introduzida no Java 5. Atinge a eficiência do acesso simultâneo usando bloqueios de segmento (Segmento). Cada segmento é equivalente a uma pequena HashTable, que pode ser operada de forma independente, e diferentes segmentos podem realizar operações simultâneas de leitura e gravação. Desta forma, na maioria dos casos, diferentes threads podem operar diferentes Segmentos ao mesmo tempo, melhorando a eficiência do acesso simultâneo. ConcurrentHashMap possui alto desempenho e escalabilidade em ambientes simultâneos.
- a diferença:
- Segurança de thread: Hashtable alcança segurança de thread por meio de sincronização, enquanto ConcurrentHashMap alcança acesso simultâneo eficiente por meio de bloqueios de segmento (Segment).
- Desempenho: ConcurrentHashMap tem melhor desempenho que Hashtable em um ambiente multithread e pode suportar maior simultaneidade.
- Consistência fraca do iterador: o iterador do Hashtable é fortemente consistente, ou seja, não será modificado durante o processo de iteração. O iterador de ConcurrentHashMap é fracamente consistente e pode ser modificado durante o processo de iteração, mas não lançará uma exceção ConcurrentModificationException.
Se você precisar usar uma tabela hash em um ambiente multithread e tiver requisitos de alto desempenho, é recomendado usar ConcurrentHashMap.
Se você estiver em um ambiente de thread único ou quando os requisitos de desempenho não forem altos, você poderá usar HashTable.
8. Vamos falar sobre o handshake de três vias e a onda de quatro vias do TCP.
TCP (Transmission Control Protocol) é um protocolo de transmissão de rede confiável e orientado a conexão. Ao estabelecer e fechar uma conexão TCP, são necessários um handshake de três vias e quatro ondas.
O processo de handshake triplo é o seguinte:
- O primeiro handshake: o cliente envia um segmento TCP com o flag SYN (sincronização) ao servidor, solicitando o estabelecimento de uma conexão. Neste ponto o cliente entra no estado SYN_SENT.
- Segundo handshake: Após receber a solicitação do cliente, o servidor responde ao cliente com um segmento de mensagem com flags SYN e ACK (confirmação). Neste ponto, o servidor entra no estado SYN_RECEIVED.
- Terceiro handshake: Após receber a resposta do servidor, o cliente envia novamente ao servidor um segmento de mensagem com a flag ACK, indicando que a conexão foi estabelecida. Neste ponto, a conexão é estabelecida, tanto o cliente quanto o servidor entram no estado ESTABLISHED e a transmissão de dados pode começar.
O processo de handshake de quatro vias é o seguinte:
- A primeira onda: o cliente envia um segmento com flag FIN (end) para o servidor, indicando que o cliente não enviará mais dados. O cliente entra no estado FIN_WAIT_1.
- A segunda onda: Após receber a solicitação final do cliente, o servidor envia ao cliente um segmento de mensagem com um sinalizador ACK, indicando que o servidor recebeu a solicitação final. Neste ponto o servidor entra no estado CLOSE_WAIT.
- A terceira onda: o servidor envia ao cliente um segmento com a flag FIN, indicando que o servidor não enviará mais dados. O servidor entra no estado LAST_ACK.
- A quarta onda: Após receber a solicitação de término do servidor, o cliente envia ao servidor um segmento de mensagem com um sinalizador ACK, indicando que o cliente recebeu a solicitação de término. O cliente entra no estado TIME_WAIT e aguarda um período de tempo antes de fechar a conexão. Após o servidor receber o ACK, ele fecha a conexão e entra no estado CLOSED.
Através do handshake de três vias, o cliente e o servidor estabelecem uma conexão confiável; através da onda de quatro vias, ambas as partes completam a transmissão de dados e fecham a conexão com segurança. Isso garante uma transmissão confiável de dados e uma liberação normal de conexões.
9. Fale sobre a diferença entre tcp e udp
TCP (Protocolo de Controle de Transmissão) e UDP (Protocolo de Datagrama de Usuário) são dois protocolos da camada de transporte comumente usados. Eles têm as seguintes diferenças em características e cenários aplicáveis.
- Conectividade:
- TCP é um protocolo orientado a conexão que estabelece uma conexão por meio de um handshake de três vias e fornece transmissão de dados confiável, ordenada e orientada a fluxo de bytes. O TCP garante integridade e confiabilidade dos dados e é adequado para cenários que exigem alta precisão dos dados.
- UDP é um protocolo sem conexão que não requer o estabelecimento de uma conexão e envia pacotes de dados diretamente. O UDP fornece um método de transmissão de dados simples e sem congestionamentos, adequado para cenários que exigem alto desempenho em tempo real, mas requisitos de confiabilidade relativamente baixos.
- Características de transmissão de dados:
- O TCP fornece transmissão de dados confiável, garantindo que os dados cheguem em ordem e não sejam perdidos por meio do número de sequência e do mecanismo de confirmação. O TCP também fornece mecanismos de controle de fluxo e controle de congestionamento para evitar congestionamento na rede e perda de dados.
- O UDP fornece transmissão de dados não confiável e os pacotes de dados podem ser perdidos, duplicados ou fora de serviço. O UDP não possui mecanismo de controle de fluxo e controle de congestionamento, e a velocidade de transmissão de dados é mais rápida, mas a confiabilidade dos dados não é garantida.
- Tamanho do datagrama:
- O TCP não tem limite máximo fixo de tamanho de datagrama e pode transmitir grandes quantidades de dados, tornando-o adequado para grandes transferências de arquivos.
- O UDP tem um tamanho de datagrama limitado (64 KB) e é adequado para transmitir pacotes de dados menores.
- eficiência:
- Embora o TCP garanta confiabilidade, ele introduzirá um grande atraso e a velocidade de transmissão de dados será relativamente lenta.
- O UDP não possui mecanismo de controle de congestionamento e retransmissão do TCP e sua eficiência de transmissão é maior, porém, devido à sua baixa confiabilidade, não é adequado para cenários que exigem alta precisão dos dados.
10. A diferença entre arrayList e LinkedList
ArrayList e LinkedList são classes de coleção comumente usadas em Java. Eles têm as seguintes diferenças:
- Estrutura de implementação interna:
- A camada inferior de ArrayList é implementada usando um array, e os elementos podem ser acessados e modificados rapidamente por meio de indexação.
- A camada subjacente do LinkedList é implementada usando uma lista duplamente vinculada. Cada nó contém o valor do elemento atual e uma referência aos nós anteriores e seguintes.
- Operações de inserção e exclusão:
- ArrayList é menos eficiente para operações de inserção e exclusão porque outros elementos precisam ser movidos para preencher as posições excluídas ou inseridas.
- LinkedList é mais eficiente para operações de inserção e exclusão e só precisa modificar o ponteiro do nó.
- Acesso aleatório:
- ArrayList suporta acesso aleatório, ou seja, acesso direto aos elementos por meio de índices, e a complexidade de tempo é O(1).
- LinkedList não suporta acesso aleatório e precisa ser percorrido do nó principal até o local de destino, com uma complexidade de tempo de O(n).
- Uso de memória:
- ArrayList requer espaço de armazenamento contínuo na memória, portanto, ao inserir e excluir elementos, o array pode precisar ser expandido e copiado, o que ocupa uma grande quantidade de espaço de memória.
- LinkedList usa uma estrutura de lista vinculada na memória. Cada nó só precisa armazenar referências ao elemento atual e aos nós anteriores e seguintes, e ocupa um espaço de memória relativamente pequeno.
Se você precisar de acesso aleatório frequente e não tiver requisitos de alto desempenho para operações de inserção e exclusão, poderá escolher ArrayList. Se forem necessárias operações frequentes de inserção e exclusão e os requisitos de desempenho para acesso aleatório não forem altos, você poderá escolher LinkedList. Ao escolher qual classe de coleção usar, você precisa avaliá-la com base em cenários e necessidades específicas de aplicação.