| /* |
| * 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.Constants; |
| 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, "(" + Constants.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().replace("'", "\\'")).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) |
| { |
| try { |
| Method m = logService.getClass() |
| .getDeclaredMethod("log", int.class, String.class, Throwable.class); |
| m.setAccessible(true); |
| m.invoke(logService, level, message, t); |
| } catch (Exception e) { |
| logSTD(LogService.LOG_WARNING, e, "Unable to log with the given log service"); |
| logSTD(level, t, message); |
| } |
| } |
| else |
| { |
| logSTD(level, t, message); |
| } |
| } |
| |
| private void logSTD(int level, Throwable t, String message) { |
| 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 + ")"; |
| } |
| } |
| } |