Coleta de lixo Java

Coleta de lixo Java

Entenda como funciona o Java Garbage Collection e otimize o uso de memória em suas aplicações. Saiba mais sobre as complexidades do gerenciamento de memória em Java.

Imagem em destaque

A coleta de lixo é um processo crucial que pode ajudar qualquer empresa de desenvolvimento Java. Esse poderoso recurso em linguagens de programação gerencia habilmente a alocação e desalocação de memória, evitando vazamentos de memória e otimizando a utilização de recursos. Agindo como um zelador constante, ele limpa diligentemente objetos não utilizados, evitando que sejamos inundados por desordem digital desnecessária. Como uma empresa de desenvolvimento Java, frequentemente encontramos esse desafio em nossos esforços de codificação, e a coleta de lixo fornece uma solução elegante para isso. Vamos nos aprofundar neste mecanismo sofisticado.

GC, o herói anônimo da programação Java, não apenas nos limpa, mas também torna a memória Java eficiente. É crucial porque nós, como programadores, precisamos gerenciar bem a memória, liberando recursos, especialmente objetos não referenciados, e muitas vezes nos esquecemos disso em nossa busca por novas ideias (e às vezes por preguiça).

Como funciona a coleta de lixo Java

Vamos examinar os detalhes de como esse limpador silencioso funciona em Java.

A coleta de lixo em Java é um processo que gerencia automaticamente a memória, recuperando objetos não utilizados que não estão mais em uso.

Estrutura de memória

Em Java, a memória é dividida em memória de pilha, memória heap e metaespaço. Vamos olhar mais de perto.

Memória de pilha

A memória de pilha é uma região da memória que armazena variáveis ​​locais, referências de objeto, parâmetros de método e outros dados específicos do método durante a execução de um método. O tamanho da memória da pilha é fixo.

Memória de pilha

A memória heap é uma região da memória usada para armazenar os objetos reais. Seu tamanho é fixo e pode aumentar ou diminuir dinamicamente conforme necessário.

Aqui está um exemplo.

Integer num = new Integer(12);

Isso cria uma variável “num” na memória da pilha e um novo objeto Inteiro na memória heap. A variável “num” na memória da pilha armazena uma referência ao objeto original.

Metaespaço

Metaspace é uma parte da memória nativa usada pela Java Virtual Machine (JVM) para armazenar metadados sobre classes e métodos estáticos. Ela substituiu a memória heap de Geração Permanente, que fazia parte da memória heap.

Nas versões anteriores do Java, o PermGen era usado para armazenar metadados sobre classes e métodos estáticos. Mas tinha algumas limitações. Um deles era o fato de terem tamanho fixo. Outro problema era que o espaço PermGen era coletado como lixo junto com o restante do heap, o que gerava problemas de desempenho.

O Metaspace é redimensionável dinamicamente e é coletado como lixo separadamente. Ele permite o compartilhamento de metadados de classe entre múltiplas instâncias JVM, o que pode reduzir o uso de memória e melhorar o desempenho.

Elegibilidade para coleta de lixo

Objetos acessíveis ao vivo são aqueles que estão sendo referenciados por alguma parte do programa, enquanto objetos mortos ou inacessíveis são aqueles que não são referenciados por nenhuma parte do programa. Por exemplo:

Integer num = new Integer(12);
num = null;

Conforme discutido, a primeira linha cria um novo objeto Inteiro na memória do heap e uma variável na memória da pilha, que armazena uma referência ao objeto original.

Então, na próxima linha, estamos alterando a referência de “num”, o que significa que “num” não se refere ao objeto Inteiro que criamos anteriormente. Na verdade, esse objeto Integer não está sendo referenciado por nenhuma parte do nosso programa. Portanto, é um objeto inacessível ou morto. Objetos mortos podem ser coletados como lixo.

Os objetos tornam-se inacessíveis quando:

  1. Todas as variáveis ​​que fazem referência a esse objeto não o fazem mais referência (elas são definidas como nulas ou definidas com um valor diferente).
  2. Objetos criados dentro de um método ficarão inacessíveis quando esse método for liberado da memória da pilha.

Ilhas de Isolamento

Uma ilha de isolamento refere-se a um grupo de objetos que fazem referência entre si, mas não são mais referenciados por nenhum objeto no programa. No exemplo abaixo, “a” e “b” fazem referência um ao outro, mas não são referenciados por nenhum outro objeto.

class Node {

Node next;

Node prev;

}

Node a = new Node ;

Node b = new Node ;

a.next = b;

b.prev = a;

Para quebrar a ilha de isolamento, precisamos alterar as referências dos objetos. Aqui, eles podem ser coletados como lixo somente após alterar as referências de “a” e “b” (por exemplo, definir aeb como nulos).

Partes da memória heap

Conforme discutido anteriormente, a memória heap é a parte da memória responsável por armazenar os objetos. Está dividido em Espaço da Geração Jovem e Espaço da Velha Geração.

Geração jovem

Em Java, a memória heap da geração mais jovem é onde novos objetos são criados. Este segmento de memória é dividido em duas seções: o espaço do Éden e os espaços do Sobrevivente.

Espaço Éden

O espaço Éden é a parte do espaço geracional jovem onde novos objetos são alocados.

Espaços Sobreviventes

Objetos no espaço Éden que sobrevivem a uma rodada de coleta de lixo são promovidos para os espaços sobreviventes.

O número de Survivor Spaces no Java Garbage Collector depende do coletor específico que está sendo usado. O número de Espaços Sobreviventes depende do coletor específico que está sendo usado. No Coletor Paralelo e no Coletor CMS, existem vários espaços de sobreviventes. O Coletor Paralelo divide o espaço sobrevivente em múltiplas regiões, enquanto o Coletor CMS usa vários espaços sobreviventes. Examinaremos mais de perto os diferentes coletores de lixo Java abaixo).

Velha Geração

Objetos que sobrevivem a um certo número de coletas de lixo são promovidos para a geração antiga. Os objetos da geração antiga têm vida mais longa. Eles não são elegíveis para GC menores e só são removidos durante uma grande coleta de lixo.

A velha geração também é chamada de geração estável.

Etapas envolvidas na coleta de lixo

A coleta de lixo Java funciona monitorando continuamente a memória heap da Java Virtual Machine para identificar objetos que não estão mais em uso.

A coleta de lixo Java é executada nas seguintes etapas:

  • Marcação: O GC primeiro identifica todos os objetos ativos na pilha e os marca.
  • Varredura: Depois que todos os objetos ativos forem identificados e marcados, o GC varre o heap e libera a memória que não está mais em uso. A memória fica então disponível para ser alocada para novos objetos.
  • Compactação: Em alguns algoritmos de coleta de lixo Java, após a varredura, os objetos restantes são compactados, o que significa que eles são movidos para uma extremidade do heap, facilitando a alocação de novos objetos pela JVM.

Coletor de lixo menor

A coleta de lixo menor é o processo de identificação e coleta de lixo da geração mais jovemmantendo-o livre de lixo e reduzindo a frequência dos principais ciclos de coleta de lixo Java.

A coleta de lixo Java secundária funciona em um tamanho de heap menor e, portanto, é consideravelmente mais rápida que a coleta de lixo principal.

Veja como funciona a coleta de lixo menor:

Preenchimento do Espaço Éden: À medida que novos objetos são alocados no espaço do Éden, ele eventualmente fica cheio. Quando o espaço Eden fica cheio, o coletor de lixo inicia um ciclo menor de GC.

Marcação Inicial: O coletor de lixo inicia o ciclo secundário de coleta de lixo realizando uma fase inicial de marcação. Durante esta fase, o coletor de lixo identifica todos os objetos vivos no espaço Éden e nos espaços sobreviventes. O coletor de lixo marca esses objetos vivos para indicar que eles não devem ser coletados.

Copiando coleção: Após a conclusão da fase inicial de marcação, o coletor de lixo executa uma fase de coleta de cópias. Durante esta fase, o coletor de lixo copia todos os objetos vivos do espaço Éden e de um dos espaços sobreviventes para o outro espaço sobrevivente.

Limpando espaço não utilizado: Depois de copiar os objetos ativos para o espaço sobrevivente, o coletor de lixo limpa o espaço Eden e o espaço sobrevivente que não foi usado durante o ciclo de coleta de lixo atual. Quaisquer objetos que não foram copiados para o espaço sobrevivente são considerados lixo e são recuperados pelo coletor de lixo.

Promoção de Objetos: Os objetos que sobreviveram a um certo número de ciclos de coleta de lixo serão eventualmente promovidos para a geração antiga (também conhecida como geração estável), onde serão gerenciados por um algoritmo de coleta de lixo diferente, otimizado para objetos de vida mais longa.

Ciclos Múltiplos: Se o espaço sobrevivente que foi usado durante o ciclo atual de coleta de lixo Java ficar cheio, o coletor executará ciclos secundários adicionais de coleta de lixo até que objetos suficientes tenham sido promovidos para a geração antiga ou o espaço sobrevivente fique vazio novamente.

Grande coletor de lixo

Quando o antigo espaço geracional é preenchido, o grande coletor de lixo atinge. É chamado de “principal” porque funciona em todo o heap e é invocado com menos frequência que o coletor de lixo secundário. Geralmente é mais demorado e consome muitos recursos.

As etapas envolvidas na coleta de lixo principal são muito semelhantes às descritas acima.

Tipos de coletores de lixo em Java

Java oferece alguns tipos diferentes de coletores de lixo. Aqui está uma lista de todos os coletores de lixo, seu funcionamento e suas vantagens.

Coletor serial ou parar e copiar

O Serial Garbage Collector é um tipo de GCr em Java que usa um thread único para executar o processo de coleta de lixo Java. É usado principalmente em aplicativos básicos que possuem padrões de uso de memória relativamente simples.

Como você deve ter adivinhado, o Serial Garbage Collector funciona sequencialmente, o que significa que interrompe todos os threads no aplicativo durante a execução do processo de coleta de lixo Java. Essa pausa na execução do aplicativo às vezes é chamada de evento “pare o mundo”.

Para usar o Serial Collector, passe -XX:UseSerialCollector como argumento.

Exemplo, java -XX:UseSerialCollector SeuPrograma

Vantagens do coletor serial:

  1. Simplicidade: Este é o coletor de lixo mais simples e direto em Java. Ele ocupa pouco espaço e requer ajuste mínimo.
  2. Previsibilidade: Por usar um único thread, seu comportamento é previsível e fácil de entender. Isso o torna útil para aplicativos que exigem um padrão de uso de memória previsível.
  3. Despesas gerais muito baixas: Isso o torna útil para pequenas aplicações onde o desempenho é crítico.

Desvantagens do coletor serial:

  1. Não projetado para escalar: Ele não foi projetado para ser dimensionado de acordo com o tamanho do heap ou o número de processadores disponíveis no sistema.
  2. Mau uso de memória: Fornece má utilização da memória disponível.
  3. Longos tempos de pausa: Devido ao seu design, longos tempos de pausa são incorporados ao processo.

Coletor Paralelo

O Coletor de lixo paralelo, servindo como coletor de lixo padrão em Java, é um método que utiliza vários threads para aumentar o desempenho da coleta de lixo. É particularmente eficaz para aplicativos maiores com padrões complexos de uso de memória.

Ao subdividir o heap em segmentos menores, o Parallel Garbage Collector utiliza vários threads para executar o processo de coleta de lixo simultaneamente. Semelhante ao coletor serial, o Coletor Paralelo também provoca uma pausa temporária na execução da aplicação durante a coleta de lixo.

Para usar o Parallel Collector, passe -XX:+UseParallelGC como argumento.

Vantagens do coletor paralelo:

  1. Mais rápido: Oferece melhor desempenho em comparação aos coletores seriais devido à utilização de vários threads, permitindo operações de coleta de lixo mais rápidas.
  2. Melhor escalabilidade: Projetado para ser dimensionado de forma eficaz com o tamanho do heap, tornando-o adequado para aplicativos com maiores requisitos de memória.

Desvantagens do coletor paralelo:

  1. Tempos de pausa mais longos: O Parallel Garbage Collector interrompe o aplicativo durante o processo de coleta de lixo, o que pode resultar em tempos de pausa mais longos em comparação com outros coletores de lixo.
  2. Maior sobrecarga de CPU: O Parallel Garbage Collector usa vários threads, o que pode resultar em maior sobrecarga e maior uso de memória.

Coletor de varredura de marca simultânea

O Coletor CMS (marcação e varredura simultâneas) é outro tipo de coletor de lixo. Funciona simultaneamente com o aplicativo para realizar o processo de coleta de lixo ou, dito de outra forma, ele usa vários threads do coletor de lixo. Ele foi projetado para minimizar os tempos de pausa no aplicativo e reduzir o impacto no desempenho da coleta de lixo Java.

Para usar CMSCollector, passe -XX:+UseConcMarkSweepGC como argumento.

Vantagens do CMS:

  1. Tempos de pausa baixos: O CMS minimiza os tempos de pausa durante a coleta de lixo, proporcionando uma experiência mais tranquila para aplicativos sensíveis à latência.
  2. Previsível: O CMS oferece pausas de coleta de lixo mais previsíveis, o que pode ser crucial para sistemas ou aplicativos em tempo real com requisitos de desempenho rigorosos.
  3. Adequado para pilhas maiores: O CMS mantém seu desempenho mesmo quando o tamanho do heap aumenta, tornando-o uma opção viável para aplicativos com demandas substanciais de memória.

Desvantagens do CMS:

  1. Maior sobrecarga de CPU: O CMS consome mais recursos de CPU devido à sua natureza simultânea, o que pode afetar o desempenho geral do aplicativo.
  2. Risco de fragmentação: O CMS não é a escolha ideal para aplicativos de longa execução, pois o heap pode ficar fragmentado com o tempo. Essa fragmentação pode levar ao aumento do uso de memória e à diminuição do desempenho.

Colecionador G1

O Coletor de lixo primeiro (G1) é um algoritmo de coleta de lixo introduzido no Java 7, projetado para resolver as limitações dos coletores de lixo tradicionais, como o Parallel Collector e o CMS Collector. O G1 foi projetado para ser um coletor orientado para a taxa de transferência e com poucas pausas, que pode lidar com heaps muito grandes.

Para usar o Coletor G1, passe o argumento:

-XX:+UseG1GC

Vantagens do Coletor G1:

  1. Tempos de pausa baixos: O G1 foi projetado para minimizar os tempos de pausa, o que pode torná-lo adequado para aplicações em tempo real.
  2. Escalabilidade: G1 é escalável, o que o torna adequado para grandes aplicações com tamanhos de heap variados.

Desvantagens do Coletor G1:

  1. A sobrecarga: G1 consome mais recursos de CPU em comparação com outros coletores de lixo, levando ao aumento da sobrecarga da CPU.
  2. Tempo de marcação inicial alto: A fase inicial de marcação pode demorar mais no G1, especialmente para tamanhos de heap grandes, o que pode impactar negativamente o desempenho do aplicativo.
  3. Não é adequado para pequenas pilhas: O G1 Collector não é ideal para aplicativos com tamanhos de heap pequenos porque seus benefícios são melhor obtidos em ambientes de memória maiores.

ZGC

O Coletor de lixo Z (ZGC) é um coletor de lixo Java projetado especificamente para gerenciar heaps extremamente grandes (até 16 TB), mantendo tempos de pausa mínimos. Seu objetivo principal é minimizar a duração dos processos de coleta de lixo, maximizando assim o rendimento da aplicação.

Vantagens do ZGC:

  1. Tempos de pausa baixos: Ele foi projetado para minimizar os tempos de pausa, tornando-o adequado para aplicativos em tempo real ou sensíveis à latência.
  2. Escalabilidade: Ele foi projetado para ser dimensionado de acordo com o tamanho do heap e o número de processadores disponíveis.
  3. Alta performance: Ele é otimizado para alto desempenho, alcançando rendimento substancial e minimizando o impacto da coleta de lixo Java no desempenho do aplicativo.

Desvantagens do ZGC:

  1. Alta sobrecarga de memória: O ZGC requer uma quantidade significativa de memória para funcionar de forma eficiente.
  2. Compatibilidade Limitada: O ZGC está disponível apenas em determinadas plataformas, incluindo Linux/x64, e requer no mínimo JDK 11.
  3. Maior utilização da CPU: Devido aos seus recursos avançados, o ZGC pode consumir mais recursos da CPU em comparação com outros coletores de lixo, afetando potencialmente o desempenho geral do aplicativo.

Colecionador Shenandoah

Shenandoah é um coletor de lixo Java projetado para fornecer tempos de pausa ultrabaixos enquanto mantém alto rendimento. Como coletor de lixo simultâneo, ele opera em paralelo com a aplicação, tornando-o adequado para aplicações sensíveis à latência.

Para usar o Shenandoah Collector, passe o argumento:

-XX:+UnlockExperimentalVMOptions -XX:+UseShenandoahGC

Vantagens do Shenandoah:

  1. Tempos de pausa ultrabaixos: Fornece tempos de pausa mínimos, tornando-o ideal para aplicativos em tempo real ou sensíveis à latência.
  2. Baixa sobrecarga de memória: Gerencia a memória com eficiência, reduzindo a sobrecarga de memória.
  3. Alto rendimento: Apesar de seu foco em tempos de pausa baixos, o Shenandoah ainda mantém alto rendimento, garantindo desempenho ideal do aplicativo.

Desvantagens do Shenandoah:

  1. Compatibilidade Limitada: Shenandoah está disponível apenas em determinadas plataformas, incluindo Linux/x64, e requer no mínimo JDK 8u40.
  2. Maior utilização da CPU: Isso pode consumir mais recursos da CPU em comparação com outros coletores de lixo, afetando potencialmente o desempenho geral do aplicativo.

Sistema.gc

Runtime.getRuntime .gc ou System.gc são métodos que solicitam à JVM que execute a coleta de lixo para liberar memória – ênfase na palavra “sugere”, pois é exatamente isso que ela faz.

Você não pode forçar a coleta de lixo. Se você está recebendo “java.lang.OutOfMemoryError”, chamar System,gc não resolverá o problema porque a JVM geralmente tenta executar um coletor de lixo antes de lançar “java.lang.OutOfMemoryError”. Possíveis correções podem ser usar um GC diferente ou aumentar o tamanho do heap.

Em geral, é recomendado que os desenvolvedores evitem chamar System.gc diretamente e, em vez disso, confiem na coleta automática de lixo fornecida pela JVM.

Problemas comuns com coleta de lixo e como resolvê-los

Aqui estão alguns problemas que você pode enfrentar:

Erros de falta de memória:

Este erro ocorre quando a JVM fica sem memória. Para resolver esse problema, os desenvolvedores podem aumentar o tamanho do heap ou otimizar o aplicativo para usar menos memória.

  • -Xmx: define o tamanho máximo de heap para seu aplicativo.
  • -Xms: define o tamanho inicial do heap para seu aplicativo.

Por exemplo, para definir o tamanho máximo de heap para 2 gigabytes e o tamanho inicial de heap para 512 megabytes, você pode usar o seguinte comando:

java -Xmx2g -Xms512m YourProgram

Longos tempos de pausa:

Longos tempos de pausa durante a coleta de lixo podem fazer com que o aplicativo pare de responder. Para resolver esse problema, você pode escolher um coletor de lixo projetado para tempos de pausa baixos ou ajustar os parâmetros do coletor de lixo.

Perda de memória:

Vazamentos de memória ocorrem quando os objetos não são liberados adequadamente da memória, levando ao aumento do uso da memória ao longo do tempo. Para resolver esse problema, os desenvolvedores podem usar ferramentas como criadores de perfil para identificar vazamentos de memória e corrigi-los.

Melhores práticas para gerenciar memória em Java

Para evitar problemas comuns com a coleta de lixo e gerenciar a memória com eficiência, aqui estão algumas práticas recomendadas:

  1. Defina a referência como nula: Sempre defina a referência como nula quando não precisar mais de um objeto.
  2. Evite criar objetos desnecessários: A criação de objetos desnecessários pode aumentar o uso de memória e causar coleta de lixo mais frequente. Você deve evitar criar objetos desnecessários e reutilizar objetos existentes quando possível.
  3. Usando objeto anônimo: É quando você não armazena a referência ao objeto.

Exemplo, createUser(novo usuário ).

  1. Liberar recursos quando eles não forem mais necessários: Objetos que usam recursos externos, como identificadores de arquivos ou conexões de banco de dados, devem ser liberados quando não forem mais necessários para evitar vazamentos de memória.

Conclusão

Compreender a mecânica da coleta de lixo Java é essencial, quer você esteja desenvolvendo internamente ou decidindo terceirizar o desenvolvimento Java, principalmente se quiser melhorar o desempenho de seu aplicativo Java. Demos uma olhada detalhada nesta parte importante da programação Java, desde os conceitos básicos de como funciona a coleta de lixo e os diferentes tipos de coletores de lixo até os pontos mais delicados do gerenciamento de memória. Lembre-se, mesmo quando você terceiriza o desenvolvimento Java, escolher o tipo certo de coleta de lixo Java e gerenciar a memória com eficiência fará uma grande diferença na velocidade do seu aplicativo. Continue explorando, continue codificando e lembre-se de que toda eficiência pode contribuir para uma aplicação mais suave e rápida. Boa codificação!

Se você gostou disso, não deixe de conferir um de nossos outros artigos sobre Java:

  • Ajuste de desempenho Java: 10 técnicas comprovadas para maximizar a velocidade Java
  • As 7 melhores ferramentas Java Profiler para 2023
  • Listadas as 9 melhores ferramentas de análise de código estático Java
  • Teste de unidade Java com JUnit 5: melhores práticas e técnicas explicadas
  • Java x Kotlin: as principais diferenças explicadas

Perguntas frequentes

Como os desenvolvedores podem identificar e diagnosticar vazamentos de memória ou ineficiências em seus aplicativos Java?

Os desenvolvedores podem identificar e diagnosticar vazamentos de memória ou ineficiências em seus aplicativos Java:

  1. Monitorando o uso de memória do aplicativo e os logs de GC usando ferramentas como JConsole, VisualVM ou Java Flight Recorder.
  2. Analisando dumps de heap para identificar objetos que estão ocupando uma quantidade excessiva de memória.
  3. Usando ferramentas de criação de perfil como YourKit, JProfiler ou Java Mission Control para identificar pontos de acesso e padrões de uso de memória.
  4. Escrever e executar testes de desempenho que simulam cenários de uso reais.
  5. Inspecionar o código e revisar as práticas recomendadas para gerenciamento de memória, como evitar a criação desnecessária de objetos, minimizar a retenção de objetos e usar estruturas de dados apropriadas.

Quais são as compensações entre rendimento, latência e sobrecarga de memória ao selecionar e configurar um coletor de lixo para um aplicativo Java?

Ao escolher e configurar um coletor de lixo que Java usa para um aplicativo, os desenvolvedores devem avaliar as vantagens e desvantagens entre taxa de transferência, latência e sobrecarga de memória:

  • Taxa de transferência: Isso se refere à capacidade do aplicativo de processar cargas de trabalho com eficiência. Os coletores de lixo que enfatizam o alto rendimento podem sofrer pausas mais longas durante a coleta de lixo, afetando a latência do aplicativo.
  • Latência: Este é o tempo necessário para que um aplicativo responda a uma solicitação. Os coletores de lixo que se concentram na baixa latência podem consumir mais memória para minimizar a frequência das pausas na coleta de lixo, resultando em maior sobrecarga de memória.
  • Sobrecarga de memória: Isto representa a quantidade de memória que o GC usa para gerenciar a memória do aplicativo. Os coletores de lixo que exigem menos memória podem precisar de pausas mais frequentes na coleta de lixo, o que pode afetar tanto o rendimento quanto a latência.

Os desenvolvedores devem avaliar cuidadosamente essas compensações ao selecionar e configurar um coletor de lixo para seu aplicativo Java, levando em consideração os requisitos e restrições exclusivos de seu caso de uso específico.

Fonte: BairesDev

Conteúdo Relacionado

O Rails 8 sempre foi um divisor de águas...
Os genéricos são uma característica poderosa em Java que...
A GenAI está transformando a força de trabalho com...
Entenda o papel fundamental dos testes unitários na validação...
Aprenda como os testes de carga garantem que seu...
Aprofunde-se nas funções complementares dos testes positivos e negativos...
Vídeos deep fake ao vivo cada vez mais sofisticados...
Entenda a metodologia por trás dos testes de estresse...
Descubra a imprevisibilidade dos testes ad hoc e seu...
A nomeação de Nacho De Marco para o Fast...
Aprenda como os processos baseados em IA aprimoram o...
Java e JavaScript são duas das linguagens de programação...
Introdução Quando se trata de desenvolvimento de software, a...
Os desenvolvedores Java enfrentam uma variedade de erros relacionados...
Neste artigo, discutiremos os benefícios de gerar imagens em...
O Java 23 finalmente foi lançado, e podemos começar...
Milhares de sites são criados todos os dias. Não...
Os recursos de linguagem que serão incluídos na próxima...
Вернуться к блогу

Комментировать

Обратите внимание, что комментарии проходят одобрение перед публикацией.