/* * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.apache.felix.webconsole.plugins.memoryusage.internal; import java.io.File; import java.io.PrintStream; import java.lang.management.ManagementFactory; import java.lang.management.MemoryMXBean; import java.lang.management.MemoryNotificationInfo; import java.lang.management.MemoryPoolMXBean; import java.lang.management.MemoryUsage; import java.lang.reflect.Method; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.Arrays; import java.util.Date; import java.util.List; import java.util.NoSuchElementException; import java.util.TreeSet; import javax.management.ListenerNotFoundException; import javax.management.MBeanServer; import javax.management.Notification; import javax.management.NotificationEmitter; import javax.management.NotificationListener; import javax.management.ObjectName; import org.osgi.framework.BundleContext; import org.osgi.framework.InvalidSyntaxException; import org.osgi.framework.ServiceEvent; import org.osgi.framework.ServiceListener; import org.osgi.framework.ServiceReference; import org.osgi.service.log.LogService; final class MemoryUsageSupport implements NotificationListener, ServiceListener { // This is the name of the HotSpot Diagnostic MBean private static final String HOTSPOT_BEAN_NAME = "com.sun.management:type=HotSpotDiagnostic"; // to get the LogService private final BundleContext context; // the default dump location: the dumps folder in the bundle private data // or the current working directory private final File defaultDumpLocation; // the default threshold value private final int defaultThreshold; // the minimum number of milliseconds between two consecutive memory // dumps written. this setting allows limitting the generation of memory // dumps if memory consumption is oscillating around the memory // threshold value private long minDumpInterval; // the configured dump location private File dumpLocation; // the actual threshold (configured or dynamically set in the console UI) private int threshold; // the system time of the last memory snapshot written; initialized so as // at least write one dump private long nextDumpTime = -1; // log service private ServiceReference logServiceReference; private Object logService; MemoryUsageSupport(final BundleContext context) { this.context = context; // register for the log service try { context.addServiceListener(this, "(objectclass=org.osgi.service.log.LogService)"); logServiceReference = context.getServiceReference("org.osgi.service.log.LogService"); if (logServiceReference != null) { logService = context.getService(logServiceReference); } } catch (InvalidSyntaxException ise) { // TODO } // the default dump location String propDumps = context.getProperty(MemoryUsageConstants.PROP_DUMP_LOCATION); if (propDumps == null) { propDumps = "dumps"; } // ensure dump location is an absolute path/location File dumps = new File(propDumps); if (!dumps.isAbsolute()) { File bundleDumps = context.getDataFile(propDumps); if (bundleDumps != null) { dumps = bundleDumps; } } this.defaultDumpLocation = dumps.getAbsoluteFile(); // prepare the dump location setDumpLocation(null); // register for memory threshold notifications NotificationEmitter memEmitter = (NotificationEmitter) getMemory(); memEmitter.addNotificationListener(this, null, null); // set the initial automatic dump threshold int defaultThreshold; String propThreshold = context.getProperty(MemoryUsageConstants.PROP_DUMP_THRESHOLD); if (propThreshold != null) { try { defaultThreshold = Integer.parseInt(propThreshold); setThreshold(defaultThreshold); } catch (Exception e) { // NumberFormatException - if propTreshold cannot be parsed to // int // IllegalArgumentException - if threshold is invalid defaultThreshold = -1; } } else { defaultThreshold = -1; } // default threshold has not been configured (correctly), assume fixed // default if (defaultThreshold < 0) { defaultThreshold = MemoryUsageConstants.DEFAULT_DUMP_THRESHOLD; setThreshold(defaultThreshold); } this.defaultThreshold = defaultThreshold; // set the initial automatic dump threshold int interval; String propInterval = context.getProperty(MemoryUsageConstants.PROP_DUMP_INTERVAL); if (propInterval != null) { try { interval = Integer.parseInt(propInterval); } catch (Exception e) { // NumberFormatException - if propTreshold cannot be parsed to // int // IllegalArgumentException - if threshold is invalid interval = -1; } } else { interval = -1; } setInterval(interval); } void dispose() { NotificationEmitter memEmitter = (NotificationEmitter) getMemory(); try { memEmitter.removeNotificationListener(this); } catch (ListenerNotFoundException e) { // don't care } context.removeServiceListener(this); if (logServiceReference != null) { context.ungetService(logServiceReference); logServiceReference = null; logService = null; } } public BundleContext getBundleContext() { return context; } /** * Sets the threshold percentage. * * @param percentage The threshold as a percentage of memory consumption. * This value may be 0 (zero) to switch off automatic heap dumps * or in the range {@link #MIN_DUMP_THRESHOLD} to * {@link #MAX_DUMP_THRESHOLD}. If set to a negative value, * the default threshold is assumed. * @throws IllegalArgumentException if the percentage value is outside of * the valid range of thresholds. The message is the percentage * value which is not accepted. */ final void setThreshold(int percentage) { if (percentage < 0) { percentage = defaultThreshold; } if (MemoryUsageConstants.isThresholdValid(percentage)) { TreeSet<String> thresholdPools = new TreeSet<String>(); TreeSet<String> noThresholdPools = new TreeSet<String>(); List<MemoryPoolMXBean> pools = getMemoryPools(); for (MemoryPoolMXBean pool : pools) { if (pool.isUsageThresholdSupported()) { long threshold = pool.getUsage().getMax() * percentage / 100; pool.setUsageThreshold(threshold); thresholdPools.add(pool.getName()); } else { noThresholdPools.add(pool.getName()); } } this.threshold = percentage; log(LogService.LOG_INFO, "Setting Automatic Memory Dump Threshold to %d%% for pools %s", threshold, thresholdPools); log(LogService.LOG_INFO, "Automatic Memory Dump cannot be set for pools %s", noThresholdPools); } else { throw new IllegalArgumentException(String.valueOf(percentage)); } } final int getThreshold() { return threshold; } final void setInterval(long interval) { if (interval < 0) { interval = MemoryUsageConstants.DEFAULT_DUMP_INTERVAL; } else { interval = 1000L * interval; } this.minDumpInterval = interval; log(LogService.LOG_INFO, "Setting Automatic Memory Dump Interval to %d seconds", getInterval()); } final long getInterval() { return minDumpInterval / 1000L; } final void printMemory(final PrintHelper pw) { pw.title("Overall Memory Use", 0); pw.keyVal("Heap Dump Threshold", getThreshold() + "%"); pw.keyVal("Heap Dump Interval", getInterval() + " seconds"); printOverallMemory(pw); pw.title("Memory Pools", 0); printMemoryPools(pw); pw.title("Heap Dumps", 0); listDumpFiles(pw); } final void printOverallMemory(final PrintHelper pw) { final MemoryMXBean mem = getMemory(); pw.keyVal("Verbose Memory Output", (mem.isVerbose() ? "yes" : "no")); pw.keyVal("Pending Finalizable Objects", mem.getObjectPendingFinalizationCount()); pw.keyVal("Overall Heap Memory Usage", mem.getHeapMemoryUsage()); pw.keyVal("Overall Non-Heap Memory Usage", mem.getNonHeapMemoryUsage()); } final void printMemoryPools(final PrintHelper pw) { final List<MemoryPoolMXBean> pools = getMemoryPools(); for (MemoryPoolMXBean pool : pools) { final String title = String.format("%s (%s, %s)", pool.getName(), pool.getType(), (pool.isValid() ? "valid" : "invalid")); pw.title(title, 1); pw.keyVal("Memory Managers", Arrays.asList(pool.getMemoryManagerNames())); pw.keyVal("Peak Usage", pool.getPeakUsage()); pw.keyVal("Usage", pool.getUsage()); if (pool.isUsageThresholdSupported()) { pw.keyVal("Usage Threshold", String.format("%d, %s, #exceeded=%d", pool.getUsageThreshold(), pool .isUsageThresholdExceeded() ? "exceeded" : "not exceeded", pool.getUsageThresholdCount())); } else { pw.val("Usage Threshold: not supported"); } pw.keyVal("Collection Usage", pool.getCollectionUsage()); if (pool.isCollectionUsageThresholdSupported()) { pw.keyVal("Collection Usage Threshold", String.format("%d, %s, #exceeded=%d", pool .getCollectionUsageThreshold(), pool.isCollectionUsageThresholdExceeded() ? "exceeded" : "not exceeded", pool.getCollectionUsageThresholdCount())); } else { pw.val("Collection Usage Threshold: not supported"); } } } final String getMemoryPoolsJson() { final StringBuilder buf = new StringBuilder(); buf.append("["); long usedTotal = 0; long initTotal = 0; long committedTotal = 0; long maxTotal = 0; final List<MemoryPoolMXBean> pools = getMemoryPools(); for (MemoryPoolMXBean pool : pools) { buf.append("{"); buf.append("'name':'").append(pool.getName()).append('\''); buf.append(",'type':'").append(pool.getType()).append('\''); MemoryUsage usage = pool.getUsage(); final long used = usage.getUsed(); formatNumber(buf, "used", used); if ( used > -1 ) { usedTotal += used; } final long init = usage.getInit(); formatNumber(buf, "init", init); if ( init > - 1 ) { initTotal += init; } final long committed = usage.getCommitted(); formatNumber(buf, "committed", committed); committedTotal += committed; final long max = usage.getMax(); formatNumber(buf, "max", usage.getMax()); final long score; if ( max == -1 || used == -1 ) { score = 100; } else { maxTotal += max; score = 100L * used / max; } buf.append(",'score':'").append(score).append("%'"); buf.append("},"); } // totalisation buf.append("{"); buf.append("'name':'Total','type':'TOTAL'"); formatNumber(buf, "used", usedTotal); formatNumber(buf, "init", initTotal); formatNumber(buf, "committed", committedTotal); formatNumber(buf, "max", maxTotal); final long score = 100L * usedTotal / maxTotal; buf.append(",'score':'").append(score).append("%'"); buf.append("}"); buf.append("]"); return buf.toString(); } void formatNumber(final StringBuilder buf, final String title, final long value) { final BigDecimal KB = new BigDecimal(1000L); final BigDecimal MB = new BigDecimal(1000L * 1000); final BigDecimal GB = new BigDecimal(1000L * 1000 * 1000); BigDecimal bd = new BigDecimal(value); final String suffix; if (bd.compareTo(GB) > 0) { bd = bd.divide(GB); suffix = "GB"; } else if (bd.compareTo(MB) > 0) { bd = bd.divide(MB); suffix = "MB"; } else if (bd.compareTo(KB) > 0) { bd = bd.divide(KB); suffix = "kB"; } else if (value >= 0 ) { suffix = "B"; } else { suffix = null; } buf.append(",'").append(title).append("':'"); if ( suffix == null ) { buf.append("unknown"); } else { bd = bd.setScale(2, RoundingMode.UP); buf.append(bd).append(suffix); } buf.append('\''); } final String getDefaultDumpLocation() { return defaultDumpLocation.getAbsolutePath(); } final void setDumpLocation(final String dumpLocation) { if (dumpLocation == null || dumpLocation.length() == 0) { this.dumpLocation = defaultDumpLocation; } else { this.dumpLocation = new File(dumpLocation).getAbsoluteFile(); } log(LogService.LOG_INFO, "Storing Memory Dumps in %s", this.dumpLocation); } final File getDumpLocation() { return dumpLocation; } final void listDumpFiles(final PrintHelper pw) { pw.title(dumpLocation.getAbsolutePath(), 1); File[] dumps = getDumpFiles(); if (dumps == null || dumps.length == 0) { pw.keyVal("-- None", null); } else { long totalSize = 0; for (File dump : dumps) { // 32167397 2010-02-25 23:30 thefile pw .val(String.format("%10d %tF %2$tR %s", dump.length(), new Date(dump.lastModified()), dump .getName())); totalSize += dump.length(); } pw.val(String.format("%d files, %d bytes", dumps.length, totalSize)); } } final File getDumpFile(final String name) { // expect a non-empty string without slash if (name == null || name.length() == 0 || name.indexOf('/') >= 0) { return null; } File dumpFile = new File(dumpLocation, name); if (dumpFile.isFile()) { return dumpFile; } return null; } final File[] getDumpFiles() { return dumpLocation.listFiles(); } final boolean rmDumpFile(final String name) { if (name == null || name.length() == 0) { return false; } final File dumpFile = new File(dumpLocation, name); if (!dumpFile.exists()) { return false; } dumpFile.delete(); return true; } /** * Dumps the heap to a temporary file * * @param live <code>true</code> if only live objects are to be returned * @return * @throws NoSuchElementException If no provided mechanism is successfully * used to create a heap dump */ final File dumpHeap(String name, final boolean live) { // ensure dumplocation exists dumpLocation.mkdirs(); File dump = dumpSunMBean(name, live); if (dump == null) { dump = dumpIbmDump(name); } if (dump == null) { throw new NoSuchElementException(); } return dump; } final MemoryMXBean getMemory() { return ManagementFactory.getMemoryMXBean(); } final List<MemoryPoolMXBean> getMemoryPools() { return ManagementFactory.getMemoryPoolMXBeans(); } public void handleNotification(Notification notification, Object handback) { String notifType = notification.getType(); if (notifType.equals(MemoryNotificationInfo.MEMORY_THRESHOLD_EXCEEDED)) { if (System.currentTimeMillis() >= nextDumpTime) { log(LogService.LOG_WARNING, "Received Memory Threshold Exceeded Notification, dumping Heap"); try { File file = dumpHeap(null, true); log(LogService.LOG_WARNING, "Heap dumped to " + file); nextDumpTime = System.currentTimeMillis() + minDumpInterval; } catch (NoSuchElementException e) { log(LogService.LOG_ERROR, "Failed dumping the heap, JVM does not provide known mechanism to create a Heap Dump"); } } else { log(LogService.LOG_WARNING, "Ignoring Memory Threshold Exceeded Notification, minimum dump interval since last dump has not passed yet"); } } } static interface PrintHelper { void title(final String title, final int level); void val(final String value); void keyVal(final String key, final Object value); } // ---------- Various System Specific Heap Dump mechanisms /** * @see http://blogs.sun.com/sundararajan/entry/ * programmatically_dumping_heap_from_java */ private File dumpSunMBean(String name, boolean live) { if (name == null) { name = "heap." + System.currentTimeMillis() + ".hprof"; } File tmpFile = new File(dumpLocation, name); MBeanServer server = ManagementFactory.getPlatformMBeanServer(); try { server.invoke(new ObjectName(HOTSPOT_BEAN_NAME), "dumpHeap", new Object[] { tmpFile.getAbsolutePath(), live }, new String[] { String.class.getName(), boolean.class.getName() }); log(LogService.LOG_DEBUG, "dumpSunMBean: Dumped Heap to %s using Sun HotSpot MBean", tmpFile); return tmpFile; } catch (Throwable t) { log(LogService.LOG_DEBUG, "dumpSunMBean: Dump by Sun HotSpot MBean not working", t); tmpFile.delete(); } return null; } /** * @param name * @return * @see <a href="http://publib.boulder.ibm.com/infocenter/javasdk/v5r0/index.jsp?topic=/com.ibm.java.doc.diagnostics.50/diag/tools/heapdump_enable.html">Getting Heapdumps</a> */ private File dumpIbmDump(String name) { try { // to try to indicate which file will contain the heap dump long minFileTime = System.currentTimeMillis(); // call the com.ibm.jvm.Dump.HeapDump() method Class<?> c = ClassLoader.getSystemClassLoader().loadClass("com.ibm.jvm.Dump"); Method m = c.getDeclaredMethod("HeapDump", (Class<?>[]) null); m.invoke(null, (Object[]) null); // find the file in the current working directory File dir = new File("").getAbsoluteFile(); File[] files = dir.listFiles(); if (files != null) { for (File file : files) { if (file.isFile() && file.lastModified() > minFileTime) { if (name == null) { name = file.getName(); } File target = new File(dumpLocation, name); file.renameTo(target); log(LogService.LOG_DEBUG, "dumpSunMBean: Dumped Heap to %s using IBM Dump.HeapDump()", target); return target; } } log(LogService.LOG_DEBUG, "dumpIbmDump: None of %d files '%s' is younger than %d", files.length, dir, minFileTime); } else { log(LogService.LOG_DEBUG, "dumpIbmDump: Hmm '%s' does not seem to be a directory; isdir=%b ??", dir, dir.isDirectory()); } log(LogService.LOG_WARNING, "dumpIbmDump: Heap Dump has been created but cannot be located"); return dumpLocation; } catch (Throwable t) { log(LogService.LOG_DEBUG, "dumpIbmDump: Dump by IBM Dump class not working", t); } return null; } // ---------- Logging support public void serviceChanged(ServiceEvent event) { if (event.getType() == ServiceEvent.REGISTERED && logServiceReference == null) { logServiceReference = event.getServiceReference(); logService = context.getService(event.getServiceReference()); } else if (event.getType() == ServiceEvent.UNREGISTERING && logServiceReference == event.getServiceReference()) { logServiceReference = null; logService = null; context.ungetService(event.getServiceReference()); } } void log(int level, String format, Object... args) { log(level, null, format, args); } void log(int level, Throwable t, String format, Object... args) { Object logService = this.logService; final String message = String.format(format, args); if (logService != null) { ((LogService) logService).log(level, message, t); } else { PrintStream out = (level <= LogService.LOG_ERROR) ? System.err : System.out; out.printf("%s: %s (%d): %s%n", toLevelString(level), context.getBundle().getSymbolicName(), context .getBundle().getBundleId(), message); if (t != null) { t.printStackTrace(out); } } } private String toLevelString(int level) { switch (level) { case LogService.LOG_DEBUG: return "DEBUG"; case LogService.LOG_INFO: return "INFO "; case LogService.LOG_WARNING: return "WARN "; case LogService.LOG_ERROR: return "ERROR"; default: return "unknown(" + level + ")"; } } }