I. Les outils du JDK▲
Le JDK propose deux utilitaires avec une interface graphique : JConsole et JVisualVM. Le premier permettant principalement d'accéder facilement aux MBeans JMX exposés par la JVM. Le deuxième étant beaucoup plus complet, en particulier si vous utilisez les plugins en téléchargement optionnel.
Il propose aussi des outils en ligne de commande :
- jps (liste des process Java) ;
- jstat (statistique sur le JVM en particulier sur l'état de la mémoire) ;
- jstack (pour demander un thread dump), jmap (pour demander un heap dump).
Tous ces outils sont présents dans le répertoire bin du JDK.
Ces outils sont soumis à quelques restrictions :
- certaines sont liées à des questions de sécurité. En particulier, ils ne sont souvent utilisables que par l'utilisateur qui exécute les JVM à monitorer ;
- d'autres restrictions sont moins évidentes ; elles sont en partie décrites ici : http://visualvm.java.net/troubleshooting.html.
Sous Windows, je rencontre actuellement des problèmes lorsque je me connecte à un serveur à distance (via Remote Desktop) : généralement, il faut toujours au moins que je me connecte en mode admin (mstsc.exe /admin (ou /console dans les anciennes versions)) pour que les outils de monitoring puissent fonctionner. Mais cela ne suffit pas toujours…
II. Accès programmatiques▲
Ces outils sont très intéressants, mais il est parfois nécessaire d'accéder à des métriques particulières d'une JVM ou de l'application que la JVM héberge. Ou ils peuvent tout simplement ne pas fonctionner dans certains cas particuliers.
Dans la suite de ce billet, je vais me focaliser sur les différentes façons d'accéder au serveur jmx des JVM en local sans avoir à configurer l'accès distant : qui peut se révéler complexe si l'on veut que cet accès reste sécurisé (tout est décrit ici : http://docs.oracle.com/javase/7/docs/technotes/guides/management/agent.html).
II-A. L'API Attach▲
L'utilisation de l'API Attach pour se connecter à un serveur jmx en local est décrite sur la même page web que l'accès distant : http://docs.oracle.com/javase/7/docs/technotes/guides/management/agent.html#gdhkz
Cette API permet essentiellement :
- de découvrir les JVM disponibles ;
- de récupérer quelques informations sur ces JVM (propriétés système et propriétés des agents chargés) ;
- de charger un agent à l'intérieur de la JVM cible.
Cette dernière fonctionnalité permet en particulier de charger l'agent JMX s'il n'est pas déjà lancé. On peut lancer l'agent jmx au démarrage de la JVM via l'option -Dcom.sun.management.jmxremote. À partir du JDK 6, c'est grâce à l'API Attach, que cette option n'est souvent plus nécessaire : l'agent peut être chargé à la demande comme montré dans l'exemple ci-dessous.
Ce programme nécessite que le jar tools.jar présent dans le répertoire lib du JDK soit dans le classpath.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
78.
79.
80.
81.
package
jmx;
import
java.io.BufferedReader;
import
java.io.InputStreamReader;
import
java.io.File;
import
java.lang.management.ManagementFactory;
import
java.lang.management.RuntimeMXBean;
import
java.util.List;
import
javax.management.MBeanServerConnection;
import
javax.management.remote.JMXConnector;
import
javax.management.remote.JMXConnectorFactory;
import
javax.management.remote.JMXServiceURL;
import
com.sun.tools.attach.VirtualMachine;
import
com.sun.tools.attach.VirtualMachineDescriptor;
public
class
JmxAttachAPI {
public
static
void
main
(
String[] args) throws
Exception {
new
JmxAttachAPI
(
).dotIt
(
);
}
static
final
String CONNECTOR_ADDRESS =
"com.sun.management.jmxremote.localConnectorAddress"
;
private
void
dotIt
(
) throws
Exception {
System.out.println
(
"Available local vms : "
);
List vmdList =
VirtualMachine.list
(
);
for
(
VirtualMachineDescriptor vmd : vmdList) {
System.out.printf
(
"%s : %s%n"
, vmd.id
(
) , vmd.displayName
(
));
}
// Choose one vm :
System.out.println
(
);
BufferedReader stdin =
new
BufferedReader
(
new
InputStreamReader
(
System.in));
String pid =
""
;
while
(
pid.length
(
) ==
0
) {
System.out.println
(
"vm id ? "
);
pid =
stdin.readLine
(
);
}
System.out.println
(
);
System.out.printf
(
"Connecting to JVM : %s%n"
,pid);
VirtualMachine vm =
VirtualMachine.attach
(
pid);
String connectorAddress =
null
;
try
{
// get the connector address
connectorAddress =
vm.getAgentProperties
(
).getProperty
(
CONNECTOR_ADDRESS);
// no connector address, so we start the JMX agent
if
(
connectorAddress ==
null
) {
System.out.println
(
"Agent not Started, loading it ..."
);
String agent =
vm.getSystemProperties
(
).getProperty
(
"java.home"
) +
File.separator +
"lib"
+
File.separator +
"management-agent.jar"
;
vm.loadAgent
(
agent);
// agent is started, get the connector address
connectorAddress =
vm.getAgentProperties
(
).getProperty
(
CONNECTOR_ADDRESS);
}
else
{
System.out.println
(
"JMX Agent already started !"
);
}
}
finally
{
vm.detach
(
);
}
System.out.println
(
);
System.out.printf
(
"Connecting to jmx server with connectorAddress : %s%n"
,connectorAddress);
// establish connection to connector server
JMXServiceURL url =
new
JMXServiceURL
(
connectorAddress);
JMXConnector connector =
JMXConnectorFactory.connect
(
url);
MBeanServerConnection con =
connector.getMBeanServerConnection
(
);
RuntimeMXBean runtime =
ManagementFactory.newPlatformMXBeanProxy
(
con, ManagementFactory.RUNTIME_MXBEAN_NAME, RuntimeMXBean.class
);
System.out.printf
(
"Extracted classpath : %s%n"
,runtime.getClassPath
(
));
}
}
Et voici un exemple de son exécution :
Available local vms :
8372
: org.apache.catalina.startup.Bootstrap start
2108
: jmx.JmxAttachAPI
vm id ?
8372
Connecting to JVM : 8372
Agent not Started, loading it ...
Connecting to jmx server with
connectorAddress : service:jmx:rmi://127.0.0.1/stub/rO0ABXNyAC5qYXZheC5tYW5hZ2VtZW50LnJlbW90ZS5ybWkuUk1JU2VydmVySW1wbF9TdHViAAAAAAAAAAICAAB4cgAaamF2YS5ybWkuc2VydmVyLlJlbW90ZVN0dWLp/tzJi+FlGgIAAHhyABxqYXZhLnJtaS5zZXJ2ZXIuUmVtb3RlT2JqZWN002G0kQxhMx4DAAB4cHc6AAtVbmljYXN0UmVmMgAADzE2OS4yNTQuMTQxLjIwNAAA1Qkf/eH4sQVkBOeL7SsAAAE1TnnPy4ABAHg=
Extracted classpath : C:\products\apache-
tomcat-
6.0.32
\bin\bootstrap.jar
Après avoir listé les JVM disponibles, ce programme s'attache à la JVM sélectionnée et regarde si l'agent JMX est déjà chargé (en cherchant la propriété com.sun.management.jmxremote.localConnectorAddress).
S'il ne la trouve pas, il demande à la JVM de charger cet agent (l'agent est dans le jar management-agent.jar présent dans le répertoire jre/lib/ de la JVM cible).
Notez que la JConsole fait exactement la même chose : elle l'indique d'ailleurs dans la fenêtre de connexion en affichant « Note : The management agent will be enabled on this process » sous la liste des process disponibles lorsque le process sélectionné n'a pas encore l'agent JMX chargé.
Une fois l'adresse JMX locale récupérée, l'API Attach a terminé son travail. L'API JMX prend ensuite le relai.
Ici on se contente d'extraire le classpath de la JVM cible. Mais l'ensemble des MBeans JMX de la JVM sont maintenant accessibles : que ce soit les MBeans de la JVM ou les MBeans que l'application aurait exposé dans le serveur JMX de la JVM.
III. L'API MonitoredHost▲
Dans certains cas, l'API Attach ne fonctionne pas… (lorsque la JVM est exécutée par un service Windows en particulier ; du moins dans l'environnement où je travaille actuellement).
JConsole utilise une deuxième API pour lister les JVM et extraire des infos : sun.JVMstat.monitor.MonitoredHost. Il utilise aussi une autre API bas niveau sun.management.ConnectorAddressLink : pour récupérer l'adresse JMX locale.
La limitation principale de cette API est qu'elle n'est pas capable d'activer l'agent JMX s'il ne l'est pas déjà. Il faut donc l'avoir activé explicitement via l'option -Dcom.sun.management.jmxremote (même avec un jdk 6 ou plus).
Sous réserve que l'agent jmx soit activé, on peut donc accéder au serveur JMX de cette façon :
Ce programme, très fortement inspiré du code source de la JConsole, nécessite aussi que le jar tools.jar soit dans le classpath.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
63.
64.
65.
66.
67.
68.
69.
70.
71.
72.
73.
74.
75.
76.
77.
package
jmx;
import
java.io.BufferedReader;
import
java.io.InputStreamReader;
import
java.lang.management.ManagementFactory;
import
java.lang.management.RuntimeMXBean;
import
java.util.HashMap;
import
java.util.Map;
import
java.util.Set;
import
javax.management.MBeanServerConnection;
import
javax.management.remote.JMXConnector;
import
javax.management.remote.JMXConnectorFactory;
import
javax.management.remote.JMXServiceURL;
import
sun.JVMstat.monitor.HostIdentifier;
import
sun.JVMstat.monitor.MonitoredHost;
import
sun.JVMstat.monitor.MonitoredVm;
import
sun.JVMstat.monitor.MonitoredVmUtil;
import
sun.JVMstat.monitor.VmIdentifier;
import
sun.management.ConnectorAddressLink;
public
class
JmxMonitoredHost {
public
static
void
main
(
String[] args) throws
Exception {
new
JmxMonitoredHost
(
).doIt
(
);
}
private
void
doIt
(
) throws
Exception {
MonitoredHost localMonitoredHost =
MonitoredHost.getMonitoredHost
(
new
HostIdentifier
((
String)null
));
Set activeVms =
localMonitoredHost.activeVms
(
);
Map pidToAddress =
new
HashMap
(
);
System.out.println
(
"Available local vms : "
);
for
(
Integer vmId : activeVms) {
String pid =
vmId.toString
(
);
try
{
MonitoredVm localMonitoredVm =
localMonitoredHost.getMonitoredVm
(
new
VmIdentifier
(
pid));
String commandLine =
MonitoredVmUtil.commandLine
(
localMonitoredVm);
boolean
attachable =
MonitoredVmUtil.isAttachable
(
localMonitoredVm);
String connectorAddress =
ConnectorAddressLink.importFrom
(
vmId.intValue
(
));
System.out.printf
(
"%5s %30s %4s%n"
,pid,commandLine,attachable);
pidToAddress.put
(
pid,connectorAddress);
localMonitoredVm.detach
(
);
}
catch
(
Exception e) {
e.printStackTrace
(
);
}
}
// Choose one vm :
System.out.println
(
);
BufferedReader stdin =
new
BufferedReader
(
new
InputStreamReader
(
System.in));
String pid =
""
;
while
(
pid.length
(
) ==
0
) {
System.out.println
(
"vm id ? "
);
pid =
stdin.readLine
(
);
}
// get the connector address
String connectorAddress =
pidToAddress.get
(
pid);
System.out.println
(
);
System.out.printf
(
"Connecting to jmx server with connectorAddress : %s%n"
,connectorAddress);
// establish connection to connector server
JMXServiceURL url =
new
JMXServiceURL
(
connectorAddress);
JMXConnector connector =
JMXConnectorFactory.connect
(
url);
MBeanServerConnection con =
connector.getMBeanServerConnection
(
);
RuntimeMXBean runtime =
ManagementFactory.newPlatformMXBeanProxy
(
con, ManagementFactory.RUNTIME_MXBEAN_NAME, RuntimeMXBean.class
);
System.out.printf
(
"Extracted classpath : %s%n"
,runtime.getClassPath
(
));
}
}
IV. L'API LocalVirtualMachine de la JConsole▲
La JConsole a implémenté sa propre API de découverte au-dessus des API décrites ci-dessus : sun.tools.jconsole.LocalVirtualMachine (on pourra trouver son code source ici par exemple : http://javasourcecode.org/html/open-source/jdk/jdk-6u23/sun/tools/jconsole/LocalVirtualMachine.java.html).
Plutôt que d'utiliser ces deux API directement, il est donc plus simple d'utiliser LocalVirtualMachine.
Cela nous donne ceci :
Ce programme nécessite tools.jar et jconsole.jar dans la classpath pour fonctionner.
2.
3.
4.
5.
6.
7.
8.
9.
10.
11.
12.
13.
14.
15.
16.
17.
18.
19.
20.
21.
22.
23.
24.
25.
26.
27.
28.
29.
30.
31.
32.
33.
34.
35.
36.
37.
38.
39.
40.
41.
42.
43.
44.
45.
46.
47.
48.
49.
50.
51.
52.
53.
54.
55.
56.
57.
58.
59.
60.
61.
62.
package
jmx;
import
java.io.BufferedReader;
import
java.io.IOException;
import
java.io.InputStreamReader;
import
java.lang.management.ManagementFactory;
import
java.lang.management.RuntimeMXBean;
import
java.util.Map;
import
javax.management.MBeanServerConnection;
import
javax.management.remote.JMXConnector;
import
javax.management.remote.JMXConnectorFactory;
import
javax.management.remote.JMXServiceURL;
import
sun.tools.jconsole.LocalVirtualMachine;
public
class
JmxJConsole {
public
static
void
main
(
String[] args) throws
Exception {
new
JmxJConsole
(
).doIt
(
);
}
private
void
doIt
(
) throws
Exception {
Map allVirtualMachines =
LocalVirtualMachine.getAllVirtualMachines
(
);
System.out.println
(
"Available local vms : "
);
for
(
Integer vmId : allVirtualMachines.keySet
(
)) {
LocalVirtualMachine vm =
allVirtualMachines.get
(
vmId);
System.out.printf
(
"%10s %50s %s%n"
,vmId,vm.displayName
(
),vm.isAttachable
(
));
}
// Choose one vm :
System.out.println
(
);
BufferedReader stdin =
new
BufferedReader
(
new
InputStreamReader
(
System.in));
String pid =
""
;
while
(
pid.length
(
) ==
0
) {
System.out.println
(
"vm id ? "
);
pid =
stdin.readLine
(
);
}
// get the connector address
LocalVirtualMachine vm =
allVirtualMachines.get
(
new
Integer
(
pid));
// load jmx agent if necessary/possible :
vm.startManagementAgent
(
);
String connectorAddress =
vm.connectorAddress
(
);
System.out.println
(
);
System.out.printf
(
"Connecting to jmx server with connectorAddress : %s%n"
,connectorAddress);
// establish connection to connector server
JMXServiceURL url =
new
JMXServiceURL
(
connectorAddress);
JMXConnector connector =
JMXConnectorFactory.connect
(
url);
MBeanServerConnection con =
connector.getMBeanServerConnection
(
);
RuntimeMXBean runtime =
ManagementFactory.newPlatformMXBeanProxy
(
con, ManagementFactory.RUNTIME_MXBEAN_NAME, RuntimeMXBean.class
);
System.out.printf
(
"Extracted classpath : %s%n"
,runtime.getClassPath
(
));
}
}
V. Conclusion▲
Avec ce dernier code, vous devriez être paré pour vous connecter en local aussi bien que JConsole et extraire vos propres métriques ou lancer des opérations de diagnostic (au hasard : lancer un heap dump) via les API JMX de la JVM.
Toutefois même la JConsole n'arrive pas tout le temps à se connecter ou à voir une JVM locale.
En cas de soucis, voici au moins trois prérequis qui m'ont été nécessaires récemment :
- exécutez JConsole (ou votre code custom) avec le même utilisateur que la JVM cible ;
- si vous vous connectez en remote desktop sur un serveur Windows : utilisez le mode admin : mstsc.exe /admin ;
- activez explicitement l'agent jmx sur votre JVM cible (via -Dcom.sun.management.jmxremote).
VI. Remerciements▲
Cet article a été publié avec l'aimable autorisation de Fabien Arrault. L'article original peut être vu sur le blog/site de Ippon.
Nous tenons à remercier Claude Leloup pour sa relecture orthographique attentive de cet article et Mickael Baron et mlny84 pour la mise au gabarit.