Tutoriel pour connaître les dangers d'utiliser System.gc()

Antonio Gomes Rodrigues

En Java, la gestion de la mémoire est réalisée par la JVM, en particulier par le Garbage Collector (ramasse miettes). Or, si l'on regarde d'un peu plus près, on remarque que l'on peut appeler le GC par l'instruction System.gc().

On se dit que cela pourrait être utile afin de libérer un maximum de mémoire avant certains traitements. Cette démarche est fortement déconseillée, car c'est une « fausse bonne idée » comme nous allons le démontrer par la suite.

Pour réagir à ce tutoriel, un espace de dialogue vous est proposé sur le forum Commentez .

- Titre : Tutoriel pour connaître les dangers d'utiliser System.gc()

- Auteur : Antonio Gomes Rodrigues

- Parution : 11 novembre 2015

- Licence : Copyright ® 2015 Antonio Gomes Rodrigues. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Un Peu de théorie

- II. Passons à la pratique

- II-A. Exécutons notre scénario avec un seul utilisateur

- II-B. Augmentons la charge en simulant 100 utilisateurs

- II-C. Trouvons la source du problème

- II-D. Relançons nos tests de charge avec 100 utilisateurs

- III. Conclusion et remerciements

I - Un Peu de théorie

Regardons la Javadoc.

1. public static void gc()

3. Runs the garbage collector.

5. Calling the gc method suggests that the Java Virtual Machine expend effort toward recycling unused

6. objects in order to make the memory they currently occupy available for quick reuse.

7. When control returns from the method call, the Java Virtual Machine has made a best effort to reclaim space from all discarded objects.

9. The call System.gc() is effectively equivalent to the call:

10. Runtime.getRuntime().gc()

12. See Also:

13. Runtime.gc()

Première mauvaise nouvelle, System.gc() ne réalise pas forcément un GC (il peut ne rien faire) et surtout on ne sait pas quand.

On a des chances de se retrouver dans ce cas.

Supposons que la JVM réagisse comme on le souhaite (le GC est bien exécuté), le GC ne s'exécutera pas forcément au meilleur moment par rapport à l'état de la JVM. Le tout en gardant le coût d'un GC (qui en fonction d'un certain nombre de paramètres peut aller jusqu'à plusieurs secondes).

II - Passons à la pratique

Afin de tester notre hypothèse (ne pas utiliser de System.gc()) modifions l'application Spring Pet Clinic en lui ajoutant un appel au GC.

Créons un scénario JMeter afin de simuler de l'activité.

II-A - Exécutons notre scénario avec un seul utilisateur

On remarque que l'activité GC est énorme (plus de 10 %) pour la charge.

La Heap est stable (bien trop stable par rapport au test exécuté).

En regardant d'un peu plus près l'activité du GC dans la partie de la mémoire appelée Old Generation, on remarque qu'il y a beaucoup trop d'activité.

De même, les temps de réponse ne sont pas extraordinaires.

À ce stade on peut faire un focus sur l'activité du GC qui ne semble pas normale.

II-B - Augmentons la charge en simulant 100 utilisateurs

Le taux d'activité GC passe de 10 % à 40 %.

De même le nombre de GC dans la Old Generation augmente fortement.

Les temps de réponse se dégradent fortement.

Sans parler de leur stabilité qui nous conforte dans l'idée qu'il y a un gros problème.

L'utilité d'un test en charge a été clairement démontrée et il devient donc illusoire de passer l'application en production.

II-C - Trouvons la source du problème

Afin de détecter si System.gc() est appelé, nous allons instrumenter le bytecode (pratique si l'on n'a pas le code source) de l'application afin qu'il nous l'indique avec le message « System.gc() est appelé » lors de chaque appel.

Bingo, on obtient rapidement la confirmation.

1. System.gc() est appelé

2. System.gc() est appelé

3. System.gc() est appelé

4. System.gc() est appelé

5. System.gc() est appelé

6. System.gc() est appelé

7. System.gc() est appelé

8. System.gc() est appelé

9. System.gc() est appelé

10. System.gc() est appelé

11. System.gc() est appelé

Modifions un peu notre script d'instrumentation afin d'avoir la stacktrace lors d'un appel de System.gc(). On obtient :

1. System.gc() est appelé

2. StackTrace :

3. --------------------------------

4. java.lang.System.gc(System.java)

5. org.springframework.samples.petclinic.web.FindOwnersForm.processSubmit(FindOwnersForm.java:50)

6. sun.reflect.GeneratedMethodAccessor82.invoke(Unknown Source)

7. sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)

8. java.lang.reflect.Method.invoke(Method.java:597)

9. org.springframework.web.bind.annotation.support.HandlerMethodInvoker.doInvokeMethod(HandlerMethodInvoker.java:710)

10. org.springframework.web.bind.annotation.support.HandlerMethodInvoker.invokeHandlerMethod(HandlerMethodInvoker.java:167)

11. org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.invokeHandlerMethod(AnnotationMethodHandlerAdapter.java:414)

12. org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter.handle(AnnotationMethodHandlerAdapter.java:402)

13. org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:771)

14. org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:716)

15. org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:647)

16. org.springframework.web.servlet.FrameworkServlet.doGet(FrameworkServlet.java:552)

17. javax.servlet.http.HttpServlet.service(HttpServlet.java:621)

18. javax.servlet.http.HttpServlet.service(HttpServlet.java:722)

19. org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:304)

20. org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)

21. org.springframework.web.filter.HiddenHttpMethodFilter.doFilterInternal(HiddenHttpMethodFilter.java:71)

22. org.springframework.web.filter.OncePerRequestFilter.doFilter(OncePerRequestFilter.java:76)

23. org.apache.catalina.core.ApplicationFilterChain.internalDoFilter(ApplicationFilterChain.java:243)

24. org.apache.catalina.core.ApplicationFilterChain.doFilter(ApplicationFilterChain.java:210)

25. org.apache.catalina.core.StandardWrapperValve.invoke(StandardWrapperValve.java:240)

26. org.apache.catalina.core.StandardContextValve.invoke(StandardContextValve.java:164)

27. org.apache.catalina.authenticator.AuthenticatorBase.invoke(AuthenticatorBase.java:462)

28. org.apache.catalina.core.StandardHostValve.invoke(StandardHostValve.java:164)

29. org.apache.catalina.valves.ErrorReportValve.invoke(ErrorReportValve.java:100)

30. org.apache.catalina.valves.AccessLogValve.invoke(AccessLogValve.java:563)

31. org.apache.catalina.core.StandardEngineValve.invoke(StandardEngineValve.java:118)

32. org.apache.catalina.connector.CoyoteAdapter.service(CoyoteAdapter.java:399)

33. org.apache.coyote.http11.Http11Processor.process(Http11Processor.java:317)

34. org.apache.coyote.http11.Http11Protocol$Http11ConnectionHandler.process(Http11Protocol.java:204)

35. org.apache.tomcat.util.net.JIoEndpoint$SocketProcessor.run(JIoEndpoint.java:311)

36. java.util.concurrent.ThreadPoolExecutor$Worker.runTask(ThreadPoolExecutor.java:886)

37. java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:908)

38. java.lang.Thread.run(Thread.java:662)

La source du problème est détectée dans la fonction : org.springframework.samples.petclinic.web.FindOwnersForm.processSubmit(FindOwnersForm.java:50)

Il ne reste plus qu'à corriger le problème. Pour cela plusieurs solutions sont possibles :

- modifier directement la fonction org.springframework.samples.petclinic.web.FindOwnersForm.processSubmit

- utiliser Findbugs pour détecter les appels à System.gc()

- utiliser l'option -XX:DisableExplicitGC de la JVM

Dans notre cas, nous allons directement corriger le code source, car nous savons exactement où se trouve le problème.

II-D - Relançons nos tests de charge avec 100 utilisateurs

Nous pouvons constater la différence et confirmer notre hypothèse.

Le temps de réponse de la transaction fautive est même divisé par dix.

III - Conclusion et remerciements

Nous avons vu dans cet article les méfaits de l'utilisation explicite de la méthode System.gc(). Mon conseil en guise de conclusion est de ne pas utiliser de System.gc().

Nous tenons à remercier Pierruel pour sa relecture attentive de cet article et Mickael Baron pour la mise au gabarit.