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