Exploitation et conformité

Heure légale française et JVM : Europe/Paris, outre-mer et changement d'heure sans bug

Exploitation et conformité · · Publié le 2026-11-10

Deux fois par an, des tâches planifiées s'exécutent deux fois, ou pas du tout, et des journaux affichent des heures impossibles. Presque toujours, la cause n'est pas un bug de la JVM mais une confusion entre trois notions : l'instant, l'heure légale française et le décalage par rapport à UTC. Ce guide explique d'où la JVM tient l'heure légale, comment elle traite le changement d'heure, et les réglages qui évitent les incidents de fin mars et de fin octobre.

D'où vient l'heure légale française

L'heure légale en France est fixée par le décret n° 2017-292 du 6 mars 2017. Il charge l'Observatoire de Paris « d'établir la valeur locale de l'UTC, dénommée "temps légal de base", et de la fournir aux utilisateurs ». L'heure de la métropole est définie par rapport à cette référence, et l'heure d'été suit la règle européenne : la directive 2000/84/CE du 19 janvier 2001 fixe le passage à l'heure d'été au dernier dimanche de mars et le retour à l'heure d'hiver au dernier dimanche d'octobre dans tous les États membres. En 2026, comme le rappelle Service-Public.fr, l'heure d'été a commencé le dimanche 29 mars à 2 heures du matin, où il est devenu 3 heures.

La JVM, elle, ne lit aucun texte officiel. Elle embarque une copie de la base de données des fuseaux horaires de l'IANA, dite tzdata, mise à jour par les éditeurs de JDK à chaque nouvelle version de la base. C'est cette copie qui décide de l'heure affichée pour Europe/Paris. Pour connaître la version embarquée :

jshell> java.time.zone.ZoneRulesProvider.getVersions("Europe/Paris").lastKey()
$1 ==> "2026b"

(Résultat obtenu sur un OpenJDK 21.0.12 de novembre 2026 ; le vôtre peut différer.)

Europe/Paris n'est pas +01:00

La règle la plus utile tient en une ligne : stockez des instants, affichez des heures locales. Un Instant ou un OffsetDateTime en UTC désigne un moment sans ambiguïté. Une heure locale française n'a de sens qu'avec une règle de fuseau, et cette règle s'appelle ZoneId.of("Europe/Paris"), pas ZoneOffset.of("+01:00"). Un décalage fixe est faux la moitié de l'année : de fin mars à fin octobre, la métropole est à +02:00.

ZoneId paris = ZoneId.of("Europe/Paris");
Instant maintenant = Instant.now();                    // à stocker, à journaliser
ZonedDateTime affichage = maintenant.atZone(paris);    // à afficher à l'usager

En base de données, une colonne TIMESTAMP WITH TIME ZONE ou une valeur en UTC évite de dépendre du fuseau du serveur au moment de l'écriture.

L'heure qui n'existe pas, l'heure qui existe deux fois

Le dernier dimanche de mars, l'horloge passe de 2 h 00 à 3 h 00 : toute heure locale entre les deux n'existe pas. Le dernier dimanche d'octobre, elle repasse de 3 h 00 à 2 h 00 : entre 2 h 00 et 3 h 00, chaque heure locale existe deux fois. java.time le sait et le dit, si on lui pose la question :

ZoneRules r = ZoneId.of("Europe/Paris").getRules();
r.getValidOffsets(LocalDateTime.parse("2027-03-28T02:30"));  // []  : heure inexistante
r.getValidOffsets(LocalDateTime.parse("2026-10-25T02:30"));  // [+02:00, +01:00] : heure doublée

LocalDateTime.parse("2027-03-28T02:30").atZone(ZoneId.of("Europe/Paris"));
// 2027-03-28T03:30+02:00[Europe/Paris]  : décalée d'une heure, sans erreur

Ces résultats viennent de la tzdata 2026b : elle situe le prochain passage à l'heure d'été au 28 mars 2027 à 2 h 00. Notez le dernier exemple : atZone ne lève pas d'exception pour une heure inexistante, il la décale. Pour l'heure doublée, il choisit par défaut le décalage d'été (le premier) ; withLaterOffsetAtOverlap() donne le second. Si une heure saisie par un usager tombe dans ces fenêtres, testez getValidOffsets et décidez explicitement.

Les tâches planifiées

Une tâche programmée « tous les jours à 2 h 30, heure de Paris » est le cas d'école. Le jour du passage à l'heure d'été, 2 h 30 n'existe pas ; le jour du retour à l'heure d'hiver, 2 h 30 se produit deux fois. Selon l'ordonnanceur, la tâche sera sautée, décalée ou lancée deux fois. Trois parades :

La plupart des ordonnanceurs Java acceptent un fuseau explicite (attribut zone d'une annotation de planification, paramètre de déclencheur cron). Ne laissez pas le fuseau par défaut du serveur décider.

Le fuseau par défaut de la JVM

Sans indication, la JVM prend le fuseau du système d'exploitation au démarrage. Dans une image de conteneur minimale, il n'y en a souvent pas, et la JVM se retrouve en UTC. On peut l'imposer avec -Duser.timezone=Europe/Paris ou la variable d'environnement TZ, mais la bonne pratique est inverse : laisser les serveurs en UTC, journaliser en UTC, et ne convertir en heure de Paris qu'à l'affichage, avec un ZoneId explicite dans le code. Le fuseau par défaut ne devrait jamais être une hypothèse du programme.

L'outre-mer

Une application nationale sert aussi les départements, régions et collectivités d'outre-mer, et chaque territoire a son identifiant tzdata. Voici ce que renvoie la JVM (tzdata 2026b) pour quelques identifiants courants, au 10 novembre 2026 :

IdentifiantDécalage renvoyéChangement d'heure dans la tzdata
America/Guadeloupe-04:00non
America/Martinique-04:00non
America/Cayenne-03:00non
America/Miquelon-03:00oui
Indian/Reunion+04:00non
Indian/Mayotte+03:00non
Pacific/Noumea+11:00non
Pacific/Tahiti-10:00non

Ce tableau décrit la base tzdata, pas le droit : il sert à vérifier que votre application traite correctement un usager de La Réunion ou de Nouméa. Ne déduisez jamais le fuseau d'un usager du fuseau du serveur ; stockez-le avec son profil, ou utilisez celui que transmet le navigateur.

Mettre à jour la tzdata

Les règles changent régulièrement quelque part dans le monde. Une JVM qui n'est pas mise à jour garde les anciennes règles et affiche des heures fausses pour les pays concernés. Deux moyens : installer les mises à jour trimestrielles de votre JDK, qui intègrent la dernière tzdata, ou, pour une version que vous ne pouvez pas changer, utiliser l'outil TZUpdater fourni par certains éditeurs. Pour la France métropolitaine, la règle applicable reste celle de la directive de 2001, mais une application qui sert des usagers à l'étranger dépend de cette mise à jour.

À retenir. Instants en UTC dans le stockage et les journaux, ZoneId.of("Europe/Paris") à l'affichage, aucun décalage fixe, pas de tâche planifiée entre 2 h et 3 h, des traitements idempotents et une tzdata à jour. Côté conformité, voir aussi notre article sur les heap dumps et le RGPD ; pour lire les horodatages des journaux du ramasse-miettes, le guide Reading GC logs.

Sources