Java Language Pitfall - Interner des chaînes pour que vous puissiez utiliser == est une mauvaise idée


Exemple

Quand certains programmeurs voient ce conseil:

"Tester des chaînes avec == est incorrect (à moins que les chaînes ne soient internées)"

leur première réaction consiste à utiliser des chaînes internes pour pouvoir utiliser == . (Après tout, == est plus rapide que d'appeler String.equals(...) , n'est-ce pas?)

C'est la mauvaise approche, sous plusieurs angles:

Fragilité

Tout d'abord, vous ne pouvez utiliser qu'en toute sécurité == si vous savez que tous les objets String vous testez ont été internés. Le JLS garantit que les littéraux String de votre code source auront été internés. Cependant, aucune des API Java SE standard ne garantit de renvoyer des chaînes internes, à l'exception de String.intern(String) elle-même. Si vous ne manquez qu'une seule source d'objets String qui n'ont pas été internés, votre application ne sera pas fiable. Ce manque de fiabilité se traduira par de faux négatifs plutôt que des exceptions susceptibles de rendre la détection plus difficile.

Coûts d'utilisation de 'intern ()'

Sous le capot, l'internement fonctionne en maintenant une table de hachage qui contient des objets String précédemment internés. Une sorte de mécanisme de référence faible est utilisé pour que la table de hachage interne ne devienne pas une fuite de stockage. Alors que la table de hachage est implémentée en code natif (contrairement à HashMap , HashTable , etc.), les appels intern sont encore relativement coûteux en termes de CPU et de mémoire.

Ce coût doit être comparé à celui que nous allons obtenir en utilisant == au lieu d’ equals . En fait, nous n'allons pas à la rupture à moins que chaque chaîne interne soit comparée à d'autres chaînes "quelques fois".

(Mis à part: les quelques situations où l’internat est utile ont tendance à réduire l’empreinte mémoire d’une application où les mêmes chaînes se répètent plusieurs fois, et ces chaînes ont une longue durée de vie.)

L'impact sur la collecte des ordures

Outre les coûts directs de processeur et de mémoire décrits ci-dessus, les chaînes internes affectent les performances du ramasse-miettes.

Pour les versions de Java antérieures à Java 7, les chaînes internes sont conservées dans l'espace "PermGen", qui est rarement collecté. Si PermGen doit être collecté, cela déclenche généralement une récupération de place complète. Si l'espace PermGen se remplit complètement, la machine virtuelle Java se bloque, même s'il y avait de l'espace libre dans les espaces de pile standard.

Dans Java 7, le pool de chaînes a été déplacé de "PermGen" dans le tas normal. Cependant, la table de hachage sera toujours une structure de données à long terme, ce qui entraînera une longue durée de vie des chaînes internes. (Même si les objets de chaîne internes étaient alloués dans l'espace Eden, ils seraient très probablement promus avant d'être collectés.)

Ainsi, dans tous les cas, l’installation d’une ficelle va prolonger sa durée de vie par rapport à une ficelle ordinaire. Cela augmentera les frais généraux de la récupération de place pendant la durée de vie de la machine virtuelle Java.

Le deuxième problème est que la table de hachage doit utiliser un mécanisme de référence faible afin d'empêcher que la chaîne ne contienne de la mémoire. Mais un tel mécanisme est plus utile pour le ramasse-miettes.

Il est difficile de quantifier ces frais généraux de récupération de place, mais il ne fait aucun doute qu'ils existent. Si vous utilisez beaucoup de intern , ils pourraient être importants.

La taille de la table de hachage

Selon cette source , à partir de Java 6, le pool de chaînes de caractères est implémenté sous la forme d'une table de hachage de taille fixe avec des chaînes pour gérer les chaînes qui hachent le même compartiment. Dans les premières versions de Java 6, la table de hachage avait une taille constante (câblée). Un paramètre de réglage ( -XX:StringTableSize ) a été ajouté en tant que mise à jour à mi-vie à Java 6. Dans une mise à jour à mi-vie de Java 7, la taille par défaut du pool est passée de 1009 à 60013 .

L'essentiel est que si vous avez l'intention d'utiliser intensivement intern dans votre code, il est conseillé de choisir une version de Java où la taille hashtable est réglable et assurez-vous de régler la taille de manière appropriée. Sinon, les performances du intern risquent de se dégrader à mesure que le pool augmente.

Interning en tant que vecteur potentiel de déni de service

L'algorithme de hachage pour les chaînes est bien connu. Si vous stockez des chaînes fournies par des utilisateurs ou des applications malveillants, cela peut être utilisé dans le cadre d'une attaque par déni de service (DoS). Si l'agent malveillant organise le même code de hachage pour toutes les chaînes qu'il fournit, cela peut entraîner une table de hachage non équilibrée et des performances O(N) pour intern ... où N est le nombre de chaînes en collision.

(Il existe des moyens plus simples et plus efficaces pour lancer une attaque DoS contre un service. Toutefois, ce vecteur pourrait être utilisé si l’objectif de l’attaque DoS était de briser la sécurité ou d’éviter les défenses DoS de première ligne.)