Java Language Piège: fuites de mémoire


Exemple

Java gère la mémoire automatiquement. Vous n'êtes pas obligé de libérer de la mémoire manuellement. La mémoire d'un objet sur le tas peut être libérée par un garbage collector lorsque l'objet n'est plus accessible par un thread en direct.

Cependant, vous pouvez empêcher la libération de la mémoire, en permettant aux objets d'être accessibles qui ne sont plus nécessaires. Que vous appeliez cela une fuite de mémoire ou un empaquetage de mémoire, le résultat est le même: une augmentation inutile de la mémoire allouée.

Les fuites de mémoire dans Java peuvent se produire de différentes manières, mais la raison la plus courante est la référence permanente aux objets, car le ramasse-miettes ne peut pas supprimer les objets du tas alors qu'il y a encore des références.

Champs statiques

On peut créer une telle référence en définissant la classe avec un champ static contenant une collection d'objets et en oubliant de définir ce champ static sur null après que la collection ne soit plus nécessaire. static champs static sont considérés comme des racines GC et ne sont jamais collectés. Un autre problème concerne les fuites dans la mémoire non-tas lorsque JNI est utilisé.

Fuite de classloader

De loin, le type de fuite de mémoire le plus insidieux est la fuite du chargeur de classe . Un classloader contient une référence à chaque classe qu'il a chargée, et chaque classe contient une référence à son classloader. Chaque objet contient également une référence à sa classe. Par conséquent, même si un seul objet d'une classe chargée par un chargeur de classe n'est pas un déchet, aucune classe chargée par ce chargeur de classes ne peut être collectée. Comme chaque classe fait également référence à ses champs statiques, ils ne peuvent pas non plus être collectés.

Fuite d' accumulation L'exemple de fuite d'accumulation pourrait ressembler à ceci:

final ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
final Deque<BigDecimal> numbers = new LinkedBlockingDeque<>();
final BigDecimal divisor = new BigDecimal(51);

scheduledExecutorService.scheduleAtFixedRate(() -> {
    BigDecimal number = numbers.peekLast();
    if (number != null && number.remainder(divisor).byteValue() == 0) {
        System.out.println("Number: " + number);
        System.out.println("Deque size: " + numbers.size());
    }
}, 10, 10, TimeUnit.MILLISECONDS);

scheduledExecutorService.scheduleAtFixedRate(() -> {
    numbers.add(new BigDecimal(System.currentTimeMillis()));
}, 10, 10, TimeUnit.MILLISECONDS);

try {
    scheduledExecutorService.awaitTermination(1, TimeUnit.DAYS);
} catch (InterruptedException e) {
    e.printStackTrace();
}

Cet exemple crée deux tâches planifiées. La première tâche prend le dernier numéro d'un numbers appelé deque et, si le nombre est divisible par 51, elle imprime le nombre et la taille du deque. La deuxième tâche met des chiffres dans le deque. Les deux tâches sont planifiées à un taux fixe et elles s'exécutent toutes les 10 ms.

Si le code est exécuté, vous verrez que la taille de la police augmente en permanence. Cela entraînera éventuellement le remplissage de deque avec des objets consommant toute la mémoire de tas disponible.

Pour éviter cela tout en préservant la sémantique de ce programme, nous pouvons utiliser une méthode différente pour prendre des nombres à partir de deque: pollLast . Contrairement à la méthode peekLast , pollLast renvoie l'élément et le supprime de deque tandis que peekLast ne renvoie que le dernier élément.