1 /*
2 * Copyright 2008-2019 by Emeric Vernat
3 *
4 * This file is part of Java Melody.
5 *
6 * Licensed under the Apache License, Version 2.0 (the "License");
7 * you may not use this file except in compliance with the License.
8 * You may obtain a copy of the License at
9 *
10 * http://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing, software
13 * distributed under the License is distributed on an "AS IS" BASIS,
14 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 * See the License for the specific language governing permissions and
16 * limitations under the License.
17 */
18 package net.bull.javamelody.internal.model;
19
20 import java.io.ByteArrayInputStream;
21 import java.io.File;
22 import java.io.InputStream;
23 import java.lang.reflect.InvocationTargetException;
24 import java.lang.reflect.Method;
25 import java.net.URL;
26 import java.net.URLClassLoader;
27 import java.nio.charset.StandardCharsets;
28
29 import javax.management.JMException;
30 import javax.management.ObjectName;
31
32 import net.bull.javamelody.internal.common.I18N;
33
34 /**
35 * Classe d'attachement dynamique utilisée ici pour obtenir l'histogramme de la mémoire.
36 * <br/>Cette classe nécessite tools.jar du jdk pour être exécutée (ok dans tomcat),
37 * mais pas pour être compilée.
38 * <br/>@see <a href="http://java.sun.com/javase/6/docs/jdk/api/attach/spec/com/sun/tools/attach/VirtualMachine.html#attach(java.lang.String)">VirtualMachine</a>
39 * @author Emeric Vernat
40 */
41 public final class VirtualMachine {
42 private static boolean enabled = isSupported();
43 // singleton initialisé à la demande
44 private static Object jvmVirtualMachine;
45
46 private VirtualMachine() {
47 super();
48 }
49
50 /**
51 * @return true si heapHisto supporté.
52 */
53 public static boolean isSupported() {
54 // pour nodes Jenkins, on réévalue sans utiliser de constante
55 final String javaVendor = System.getProperty("java.vendor");
56 final String javaVmName = System.getProperty("java.vm.name");
57 return javaVendor.contains("Sun") || javaVendor.contains("Oracle")
58 || javaVendor.contains("Apple") || isJRockit()
59 // #936 OpenJDK was not supported
60 || javaVmName.contains("OpenJDK");
61 }
62
63 /**
64 * @return true si JVM JRockit
65 */
66 public static boolean isJRockit() {
67 // pour nodes Jenkins, on réévalue sans utiliser de constante
68 return System.getProperty("java.vendor").contains("BEA");
69 }
70
71 /**
72 * @return false si non supporté ou si un attachement ou un histogramme a échoué,
73 * true si supporté et pas essayé ou si réussi
74 */
75 public static synchronized boolean isEnabled() { // NOPMD
76 return enabled;
77 }
78
79 /**
80 * @return Singleton initialisé à la demande de l'instance de com.sun.tools.attach.VirtualMachine,
81 * null si enabled est false
82 * @throws Exception e
83 */
84 public static synchronized Object getJvmVirtualMachine() throws Exception { // NOPMD
85 // si hotspot retourne une instance de sun.tools.attach.HotSpotVirtualMachine
86 // cf http://www.java2s.com/Open-Source/Java-Document/6.0-JDK-Modules-sun/tools/sun/tools/attach/HotSpotVirtualMachine.java.htm
87 // et sous windows : sun.tools.attach.WindowsVirtualMachine
88 if (jvmVirtualMachine == null) {
89 // on utilise la réflexion pour éviter de dépendre de tools.jar du jdk à la compilation
90 final Class<?> virtualMachineClass = findVirtualMachineClass();
91 final Method attachMethod = virtualMachineClass.getMethod("attach", String.class);
92 final String pid = PID.getPID();
93 try {
94 jvmVirtualMachine = invoke(attachMethod, null, pid);
95 } finally {
96 enabled = jvmVirtualMachine != null;
97 }
98 }
99 return jvmVirtualMachine;
100 }
101
102 private static Class<?> findVirtualMachineClass() throws Exception { // NOPMD
103 // méthode inspirée de javax.tools.ToolProvider.Lazy.findClass
104 // http://grepcode.com/file/repository.grepcode.com/java/root/jdk/openjdk/6-b27/javax/tools/ToolProvider.java#ToolProvider.Lazy.findClass%28%29
105 final String virtualMachineClassName = "com.sun.tools.attach.VirtualMachine";
106 try {
107 // try loading class directly, in case tools.jar is in the classpath
108 return Class.forName(virtualMachineClassName);
109 } catch (final ClassNotFoundException e) {
110 // exception ignored, try looking in the default tools location (lib/tools.jar)
111 File file = new File(System.getProperty("java.home"));
112 if ("jre".equalsIgnoreCase(file.getName())) {
113 file = file.getParentFile();
114 }
115 final String[] defaultToolsLocation = { "lib", "tools.jar" };
116 for (final String name : defaultToolsLocation) {
117 file = new File(file, name);
118 }
119 // if tools.jar not found, no point in trying a URLClassLoader
120 // so rethrow the original exception.
121 if (!file.exists()) {
122 throw e;
123 }
124
125 final URL url = file.toURI().toURL();
126 final ClassLoader cl;
127 // if (ClassLoader.getSystemClassLoader() instanceof URLClassLoader) {
128 // // The attachment API relies on JNI, so if we have other code in the JVM that tries to use the attach API
129 // // (like the monitoring of another webapp), it'll cause a failure (issue 398):
130 // // "UnsatisfiedLinkError: Native Library C:\Program Files\Java\jdk1.6.0_35\jre\bin\attach.dll already loaded in another classloader
131 // // [...] com.sun.tools.attach.AttachNotSupportedException: no providers installed"
132 // // So we try to load tools.jar into the system classloader, so that later attempts to load tools.jar will see it.
133 // cl = ClassLoader.getSystemClassLoader();
134 // // The URLClassLoader.addURL method is protected
135 // final Method addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
136 // addURL.setAccessible(true);
137 // addURL.invoke(cl, url);
138 // } else {
139 final URL[] urls = { url };
140 cl = URLClassLoader.newInstance(urls);
141 // }
142 return Class.forName(virtualMachineClassName, true, cl);
143 }
144 }
145
146 /**
147 * Détachement du singleton.
148 * @throws Exception e
149 */
150 public static synchronized void detach() throws Exception { // NOPMD
151 if (jvmVirtualMachine != null) {
152 final Class<?> virtualMachineClass = jvmVirtualMachine.getClass();
153 final Method detachMethod = virtualMachineClass.getMethod("detach");
154 jvmVirtualMachine = invoke(detachMethod, jvmVirtualMachine);
155 jvmVirtualMachine = null;
156 }
157 }
158
159 /**
160 * @return flux contenant l'histogramme mémoire comme retourné par jmap -histo
161 * @throws Exception e
162 */
163 public static InputStream heapHisto() throws Exception { // NOPMD
164 if (!isSupported()) {
165 throw new IllegalStateException(I18N.getString("heap_histo_non_supporte"));
166 }
167 if (!isEnabled()) {
168 throw new IllegalStateException(I18N.getString("heap_histo_non_actif"));
169 }
170
171 try {
172 final ObjectName objectName = new ObjectName(
173 "com.sun.management:type=DiagnosticCommand");
174 final String gcClassHistogram = (String) MBeansAccessor.invoke(objectName,
175 "gcClassHistogram", new Object[] { null }, new Class[] { String[].class });
176 return new ByteArrayInputStream(gcClassHistogram.getBytes(StandardCharsets.UTF_8));
177 } catch (final JMException e1) {
178 // MBean "DiagnosticCommand" not found (with JDK 7 for example),
179 // continue with VM attach method
180 try {
181 final Class<?> virtualMachineClass = getJvmVirtualMachine().getClass();
182 final Method heapHistoMethod = virtualMachineClass.getMethod("heapHisto",
183 Object[].class);
184 return (InputStream) invoke(heapHistoMethod, getJvmVirtualMachine(),
185 new Object[] { new Object[] { "-all" } });
186 } catch (final ClassNotFoundException e) {
187 // si on obtient ClassNotFoundException alors que heap histo est "supporté"
188 // alors c'est que la jvm est un JRE et non un JDK (certainement avec tomcat) :
189 // on le signale à l'administrateur car il peut simplement installer un JDK et changer JAVA_HOME,
190 throw new IllegalStateException(I18N.getString("heap_histo_jre"), e);
191 } catch (final Exception e) {
192 if ("Can not attach to current VM".equals(e.getMessage())) {
193 throw new IllegalStateException(I18N.getString("allowAttachSelf"), e);
194 }
195 // si on obtient com.sun.tools.attach.AttachNotSupportedException: no providers installed
196 // alors c'est idem (javaws dans Jenkins nodes par exemple)
197 if ("com.sun.tools.attach.AttachNotSupportedException"
198 .equals(e.getClass().getName())) {
199 throw new IllegalStateException(I18N.getString("heap_histo_jre"), e);
200 }
201 throw e;
202 }
203 }
204 }
205
206 /**
207 * @return l'histogramme mémoire
208 * @throws Exception e
209 */
210 public static HeapHistogram createHeapHistogram() throws Exception { // NOPMD
211 try (InputStream input = heapHisto()) {
212 return new HeapHistogram(input, isJRockit());
213 }
214 }
215
216 private static Object invoke(Method method, Object object, Object... args) throws Exception { // NOPMD
217 try {
218 return method.invoke(object, args);
219 } catch (final InvocationTargetException e) {
220 if (e.getCause() instanceof Exception) {
221 throw (Exception) e.getCause();
222 } else if (e.getCause() instanceof Error) {
223 throw (Error) e.getCause();
224 } else {
225 throw new Exception(e.getCause()); // NOPMD
226 }
227 }
228 }
229
230 // Note : on pourrait aussi charger dynamiquement un agent avec jvmVirtualMachine.loadAgent
231 // (en générant un jar temporaire avec ZipFile à partir de
232 // getClass().getResourceAsStream(getClass().getName() + ".class"), d'un manifest contenant Agent-Class)
233 // pour obtenir la taille sans ses références d'un objet (Instrumentation.getObjectSize)
234 // ou pour ajouter de la programmation par aspect à la volée (datasource jdbc, façades...)
235 // (addClassTransformer(new org.aspectj.weaver.loadtime.ClassPreProcessorAgentAdapter())
236 // ou loadAgent("aspectjweaver.jar") par exemple).
237 // Voir http://java.sun.com/javase/6/docs/api/java/lang/instrument/package-summary.html?is-external=true
238
239 // private static final String MONITORING_INSTRUMENTATION_KEY = "javamelody.instrumentation";
240 // private static Instrumentation instrumentation;
241 //
242 // public void loadAgent(String jarFile) throws AgentLoadException, AgentInitializationException,
243 // IOException {
244 // try {
245 // jvmVirtualMachine.loadAgent(jarFile);
246 // } finally {
247 // instrumentation = (Instrumentation) System.getProperties().get(
248 // MONITORING_INSTRUMENTATION_KEY);
249 // System.getProperties().remove(MONITORING_INSTRUMENTATION_KEY);
250 // }
251 // }
252 //
253 // public static void agentmain(@SuppressWarnings("unused") String agentArgs, Instrumentation inst) {
254 // System.getProperties().put(MONITORING_INSTRUMENTATION_KEY, inst);
255 // }
256 //
257 // public long getObjectSize(Object objectToSize) {
258 // return instrumentation.getObjectSize(objectToSize);
259 // }
260 //
261 // public void addClassTransformer(ClassFileTransformer transformer, boolean canRetransform) {
262 // instrumentation.addTransformer(transformer, canRetransform);
263 // }
264 }
265