Viele wichtige Systemdaten und Einstellungen von JVMs und modernen Java EE Application Servern können über MBeans (Managed Beans) per JMX (Java Management Extensions) abgefragt und geändert werden.
Es wird ein Hilfstool vorgestellt, welches die prozentualen Garbage Collection, die CPU-Auslastung und weitere Kennwerte per JMX-Remotezugriff erfasst.
Grundlagen zu JMX finden Sie in jmx.htm.
Die folgende Grafik von Sun zeigt eindrucksvoll, wie viel Performance nach dem Amdahlschen Gesetz bei zu langer nicht parallelisierbarer Garbage Collection verloren geht.
Genaueres hierzu finden Sie unter http://java.sun.com/docs/performance und http://java.sun.com/javase/technologies/hotspot/gc/gc_tuning_6.html.
Grundlagen zu JMX finden Sie in jmx.htm.
Damit der Java EE Application Server (oder jede andere beliebige JMX-fähige Anwendung) über Remote-Zugriff per JMX angesprochen werden kann, muss die jeweilige Anwendung (oder die darin enthaltene JVM) oft mit bestimmten Kommandozeilenparametern gestartet werden. Außerdem müssen Sie die JMX-Portnummer in Erfahrung bringen (oder definieren).
Erläuterungen zur Sun JVM finden Sie hierzu unter http://docs.oracle.com/javase/7/docs/technotes/guides/management/agent.html und zu JRockit unter http://edocs.bea.com/jrockit/jrdocs/refman/optionX.html.
Beispiele:
Java-Kommandozeilenprogramm:
Bestimmen Sie selbst die JMX-Portnummer, zum Beispiel so:
java -Dcom.sun.management.jmxremote.port=4711 MeinJavaProgramm
GlassFish 2:
Zeigt beim Start auf der Konsole eine Ausgabe ähnlich zu:
Domain listens on at least following ports for connections: [8080 8181 4848 3700 3820 3920 8686 ].
Im Logfile erscheint:
Here is the JMXServiceURL for the Standard JMXConnectorServer: [service:jmx:rmi:///jndi/rmi://localhost:8686/jmxrmi].
In der Webkonsole (http://localhost:4848) finden Sie die Portnummer unter
'Configuration' | 'Admin Service' | 'system' | 'JMX Connector' | 'Port: 8686'.
WebLogic 10 mit Sun JVM:
Starten Sie WebLogic mit einer vorgeschalteten Batchdatei mit zum Beispiel folgenden zwei Zeilen:
set JAVA_OPTIONS=-Djavax.management.builder.initial=weblogic.management.jmx.mbeanserver.WLSMBeanServerBuilder -Dcom.sun.management.jmxremote -Dcom.sun.management.jmxremote.port=7091 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false
C:\WebLogic\user_projects\domains\MeineDomain\bin\startWebLogic.cmd
Weiteres hierzu finden Sie in der Doku zum -Dcom.sun.management.jmxremote-Parameter.
WebLogic 10 mit JRockit:
Starten Sie WebLogic mit einer vorgeschalteten Batchdatei mit zum Beispiel folgenden zwei Zeilen:
set JAVA_OPTIONS=-Djavax.management.builder.initial=weblogic.management.jmx.mbeanserver.WLSMBeanServerBuilder -Xmanagement:ssl=false,authenticate=false,autodiscovery=true
C:\WebLogic\user_projects\domains\MeineDomain\bin\startWebLogic.cmd
Beim Start erscheint auf der Konsole eine Ausgabe ähnlich zu:Management server started on port 7091 oder
[INFO ][mgmnt ] Remote JMX connector started at address <MeinPC>:7091
Weiteres hierzu finden Sie in der Doku zum -Xmanagement-Parameter.
Grundsätzliches zu JMX mit WebLogic finden Sie in Developing Custom Management Utilities With JMX for Oracle WebLogic Server.
Remote-Debugging:
Falls Sie den Java EE Application Server remote debuggen wollen, fügen Sie Folgendes Parameter beim Java-Aufruf hinzu:
-agentlib:jdwp=transport=dt_socket,address=8001,server=y,suspend=n
Die aktiven MBeans (Managed Beans) können mit dem JConsole-Programm (jconsole.exe im JDK-bin-Verzeichnis) gelesen und bearbeitet werden. Der Screenshot zeigt beispielsweise einige Attribute der GarbageCollector-MBean:
Java EE Application Server können unterschiedliche JVMs (z.B. JRockit für WebLogic) mit sehr verschiedenen Garbage-Collector-Strategien verwenden, die zudem auch noch umkonfiguriert werden können. Den meisten Garbage-Collector-Strategien ist gemeinsam, dass mehrere verschiedene Garbage Collectoren kombiniert werden. Für Diagnosezwecke wird meistens nur zwischen "kleinen" und "großen" Garbage Collections ("Minor"/"Major") unterschieden. Die "kleinen" Garbage Collections werden meistens häufig ausgeführt und benötigen nur wenig Zeit, da sie nur die einfachen Fälle mit meistens kleinen Speichermengen bearbeiten. Wenn dies nicht reicht, wird die "große" Garbage Collection bemüht, die mehr Ressoucen benötigt, da sie gründlicher aufräumt. Die Namen der "kleinen" und "großen" Garbage Collectoren sind nicht einheitlich. Beispiele:
"Minor GC" | "Major GC" | |
GlassFish 2 | Copy | MarkSweepCompact |
WebLogic 10 (mit JRockit) | Young Collector | Old Collector |
Damit das im Folgenden vorgestellte Programm "JmxServerMonitoring" für verschiedene JVMs und Application Server verwendbar ist, geht es nicht von bestimmten Garbage-Collector-Namen aus, sondern ermittelt sie generisch. Da die für die Differenzermittlung benötigten vorherigen Messwerte auf bestimmte Garbage-Collector-Namen bezogen werden müssen, werden sie in einer HashMap ("lastMeasurement") gespeichert.
Das JmxServerMonitoring-Programm ermittelt in vorgegebenen Zeitintervallen den prozentualen Anteil der einzelnen Garbage Collectoren und den Summenwert. Es kann entweder nur eine einzelne JVM (oder einen Application Server) oder mehrere (z.B. Cluster) überwachen.
Es können verschiedene Ergebnisse erzeugt werden (oder alle zusammen):
Die Parameter können auf drei Arten gesetzt werden:
Beispiele für Aufruf-Kommandozeilen (ein weiteres Beispiel finden Sie unten im Kapitel MeinBoesesMemoryLeak):
Beispiel für Properties-Datei JmxServerMonitoring.properties (für WebLogic mit JRockit-JVM):
console=true csvfile=JmxServerMonitoring.csv servername=MeinServerName url=localhost:7091 attr1=diff; TxPerSec; TransactionTotalCount; com.bea:Name=JTARuntime,Type=JTARuntime,*
Bitte beachten Sie: Falls Sie das Programm von einem entfernten Rechner aus mit vielleicht unüblichen Portnummern (8686, 7091, ...) starten, müssen Sie eventuell diese Portnummern in der Firewall freischalten.
import java.io.*; import java.lang.management.*; import java.net.MalformedURLException; import java.text.*; import java.util.*; import javax.management.*; import javax.management.remote.*; import javax.naming.Context; public class JmxServerMonitoring { static final String HELP_TEXT = "JmxServerMonitoring:\n" + " Ermittlung der prozentualen Garbage Collection per JMX.\n" + "Verschiedene Ergebnisse koennen erzeugt werden:\n" + " 'console=true':\n" + " Ausgabe im Kommandozeilenfenster.\n" + " 'nagiosfile=JmxServerMonitoring.nagios.txt':\n" + " Nur letzte Ergebnisse (z.B. fuer Nagios).\n" + " 'csvfile=JmxServerMonitoring.csv':\n" + " Alle Ergebnisse (.csv-Datei, z.B. fuer Excel).\n" + " 'errorfile=JmxServerMonitoring.error.log':\n" + " Datei fuer Fehlermeldungen (z.B. Exceptions).\n" + " 'periodminutes=10':\n" + " Messintervallzeit in Minuten.\n" + "Als URL kann die Hostadresse oder IP-Adresse angegeben werden, gefolgt von der Portnummer. " + "Es koennen nur eine JVM (bzw. Server) oder mehrere gleichzeitig ueberwacht werden:\n" + " 'url=localhost:8686 usr=admin pwd=adminadmin':\n" + " Ein einzelner Server (z.B. mit GlassFish-Portnummer).\n" + " 'url=srv1:7091,srv2:7092,srv3:7093' usr=xy pwd=yz':\n" + " Drei Server mit gleichem Benutzernamen und gleichem Kennwort.\n" + " 'url=srv1:7091,srv2:7092 usr=u1,u2 pwd=p1,p2':\n" + " Zwei Server mit verschiedenen Benutzernamen/Kennwoertern.\n" + " 'usr=Benutzername pwd=Kennwort':\n" + " Nur notwendig, wenn Authentifizierung erforderlich ist.\n" + "Parameter koennen per Kommandozeile oder Properties-Datei uebergeben werden:\n" + " 'propfile=JmxServerMonitoring.properties':\n" + " Angabe der Properties-Datei.\n" + "Zwei Beispiele fuer Aufrufkommandos:\n" + " java JmxServerMonitoring propfile=JmxServerMonitoring.properties\n" + " java JmxServerMonitoring url=localhost:8686 console=true csvfile=JmxServerMonitoring.csv\n"; static final String KEY_PROPFILE = "propfile"; static final String KEY_PERIODMINUTES = "periodminutes"; static final String KEY_SERVERNAME = "servername"; static final String KEY_URL = "url"; static final String KEY_USR = "usr"; static final String KEY_PWD = "pwd"; static final String KEY_CONSOLE = "console"; static final String KEY_ALLGCVALUES = "allgcvalues"; static final String KEY_NAGIOSFILE = "nagiosfile"; static final String KEY_CSVFILE = "csvfile"; static final String KEY_ERRORFILE = "errorfile"; static final String KEY_ATTR = "attr"; static final String DFLT_PERIODMINUTES = "10"; static final String DFLT_PROPFILE = "JmxServerMonitoring.properties"; static final String DFLT_NAGIOSFILE = "JmxServerMonitoring.nagios.txt"; static final String DFLT_ERRORFILE = "JmxServerMonitoring.error.log"; static final String DFLT_CONSOLE = "true"; static final SimpleDateFormat YYYYMMDD_HHMMSS_STD = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); static final SimpleDateFormat YYYYMMDD_HHMMSS_NAG = new SimpleDateFormat("yyyy-MM-dd_HH.mm.ss"); static final DecimalFormat DECIMAL_FORMAT1 = new DecimalFormat( "0.0" ); static final DecimalFormat DECIMAL_FORMAT2 = new DecimalFormat( "0.00" ); // Main() public static void main( String[] args ) throws Exception { Properties props = readProperties( args, KEY_PROPFILE, DFLT_PROPFILE, new String[] { KEY_PERIODMINUTES, DFLT_PERIODMINUTES, KEY_CONSOLE, DFLT_CONSOLE, KEY_NAGIOSFILE, DFLT_NAGIOSFILE, KEY_ERRORFILE, DFLT_ERRORFILE } ); int periodMinutes = Math.max( Integer.parseInt( props.getProperty( KEY_PERIODMINUTES ) ), 1 ); String serverName = props.getProperty( KEY_SERVERNAME ); String url = props.getProperty( KEY_URL ); String usr = props.getProperty( KEY_USR ); String pwd = props.getProperty( KEY_PWD ); String nagiosFile = props.getProperty( KEY_NAGIOSFILE ); String csvFile = props.getProperty( KEY_CSVFILE ); String errorFile = props.getProperty( KEY_ERRORFILE ); String s = props.getProperty( KEY_CONSOLE ); boolean console = s != null && (s.equals( "1" ) || s.equalsIgnoreCase( "true" )); s = props.getProperty( KEY_ALLGCVALUES ); boolean allGcValues = s != null && (s.equals( "1" ) || s.equalsIgnoreCase( "true" )); System.out.println( "JmxServerMonitoring (periodminutes=" + periodMinutes + ", servername=" + serverName + ", url=" + url + ", usr=" + usr + ", nagiosfile=" + nagiosFile + ", csvfile=" + csvFile + ", errorfile=" + errorFile + ")\n" ); if( url == null || url.trim().length() <= 0 ) { System.out.println( "Fehler: Unvollstaendige Angaben.\n" ); System.out.println( HELP_TEXT ); System.exit( 255 ); } ServerData[] serverDataArr = convertSrvParameter( serverName, url, usr, pwd ); AttributeValueAndName[] attributeNames = convertAttrParameter( props, KEY_ATTR ); writeJmxServerMonitoring( periodMinutes, serverDataArr, attributeNames, console, allGcValues, nagiosFile, csvFile, errorFile ); } // Aufsplittung der Server-Parameter auf mehrere Server static ServerData[] convertSrvParameter( String serverName, String url, String usr, String pwd ) { if( url == null || url.trim().length() <= 0 ) return null; String[] urlArr = url.split( ",|;|\\s" ); String[] usrArr = ( usr != null ) ? usr.split( ",|;|\\s" ) : null; String[] pwdArr = ( pwd != null ) ? pwd.split( ",|;|\\s" ) : null; String[] serverNameArr = ( serverName != null ) ? serverName.split( ",|;|\\s" ) : null; boolean sn = serverName != null && serverNameArr.length == urlArr.length; boolean up = usr != null && usrArr.length == urlArr.length && pwd != null && pwdArr.length == urlArr.length; ServerData[] serverDataArr = new ServerData[urlArr.length]; for( int i = 0; i < serverDataArr.length; i++ ) { serverDataArr[i] = new ServerData(); serverDataArr[i].url = urlArr[i]; serverDataArr[i].usr = ( up ) ? usrArr[i] : usr; serverDataArr[i].pwd = ( up ) ? pwdArr[i] : pwd; serverDataArr[i].serverName = ( sn ) ? serverNameArr[i] : null; serverDataArr[i].serverNameUndUrl = ( serverDataArr[i].serverName != null ) ? serverDataArr[i].serverName + "-" + serverDataArr[i].url : serverDataArr[i].url; if(serverDataArr[i].serverName == null) { serverDataArr[i].serverName = serverDataArr[i].url; } } return serverDataArr; } // Aufsplittung eventueller zusaetzlicher MBean-Attribut-Abfragen static AttributeValueAndName[] convertAttrParameter( Properties props, String key ) { List<AttributeValueAndName> attributeNameList = new ArrayList<AttributeValueAndName>(); for( int i=1; i<1000; i++ ) { String s = props.getProperty( key + i ); if( s == null || s.trim().length() <= 0 ) break; String[] ss = s.split( ";" ); if( ss == null || ss.length < 4 ) break; for( int j=0; j<ss.length; j++ ) if( ss[j] != null ) ss[j] = ss[j].trim(); AttributeValueAndName attributeName = new AttributeValueAndName(); attributeName.diff = ss[0] != null && ss[0].toLowerCase().startsWith( "diff" ); attributeName.title = ss[1]; attributeName.attributeName = ss[2]; attributeName.objectName = ss[3]; if( ss.length > 4 ) attributeName.methodName = ss[4]; if( ss.length > 5 ) { attributeName.methodParms = new String[ss.length - 5]; System.arraycopy( ss, 5, attributeName.methodParms, 0, attributeName.methodParms.length ); } attributeNameList.add( attributeName ); } return attributeNameList.toArray( new AttributeValueAndName[attributeNameList.size()] ); } // Zeitschleife ueber Garbage-Collection-Ermittlung und Ergebnisse-Schreiben static void writeJmxServerMonitoring( int periodMinutes, ServerData[] serverDataArr, AttributeValueAndName[] attributeNames, boolean showConsole, boolean writeAllGcValues, String nagiosFile, String csvFile, String errorFile ) { long periodTime = (new Date()).getTime(); JMXConnector jmxConnector = null; // Zeitschleife: while( true ) { // Schleife ueber alle Server: for( ServerData serverData : serverDataArr ) { try { // JMX- und MBeanServer-Connection: jmxConnector = getJMXConnector( serverData.url, serverData.usr, serverData.pwd ); MBeanServerConnection mBeanServerConn = jmxConnector.getMBeanServerConnection(); // Lies Garbage Collections: serverData.gcGroup = getGarbageCollectionGroup( periodMinutes, serverData.lastMeasurement, mBeanServerConn ); // Lies eventuell zusaetzliche MBean-Attribute: serverData.attributes = getAttributes( attributeNames, periodMinutes, serverData.lastMeasurement, mBeanServerConn ); } catch( Exception ex ) { serverData.lastMeasurement.clear(); serverData.gcGroup = null; serverData.attributes = attributeNames; String s = YYYYMMDD_HHMMSS_STD.format( new Date() ) + ", Url=" + serverData.url + ": "; System.out.println( s ); System.out.println( ex ); writeErrorFile( s, ex, errorFile ); } finally { try { if( jmxConnector != null ) jmxConnector.close(); } catch( Exception ex ) {/*ok*/} jmxConnector = null; } } // Schreibe Ergebnisse: writeConsole( serverDataArr, showConsole ); writeNagiosFile( serverDataArr, nagiosFile ); writeCsvFileOneForAllServers( serverDataArr, csvFile ); writeCsvFilePerServerWithDifferentGcValues( serverDataArr, csvFile, writeAllGcValues ); // Zeitintervall: periodTime += periodMinutes * 60 * 1000; long waitMilliseconds = periodTime - (new Date()).getTime(); if( waitMilliseconds > 0 ) { try { Thread.sleep( waitMilliseconds ); } catch( InterruptedException ex ) {/*ok*/} } } } // Ermittlung einer Gruppe von Garbage-Collection-Werten zu einem einzelnen Server static GarbageCollectionGroup getGarbageCollectionGroup( int periodMinutes, Map<String,Long[]> lastMeasurement, MBeanServerConnection mBeanServerConn ) throws Exception { // Lies bisherige Laufzeit der JVM aus Remote-Runtime-MXBean: long rtUptimeMs = getRuntimeMXBeanFromRemote( mBeanServerConn ).getUptime(); // Lies GarbageCollector-MXBeans von Remote: List<GarbageCollectorMXBean> gcMXBeans = getGarbageCollectorMXBeansFromRemote( mBeanServerConn ); // Verschiedene Garbage-Collector-Arten: GarbageCollectionGroup gcGroup = new GarbageCollectionGroup(); for( GarbageCollectorMXBean gc : gcMXBeans ) { GarbageCollectionSingle gcSingle = new GarbageCollectionSingle(); gcSingle.gcName = gc.getName(); if( gcSingle.gcName != null && gcSingle.gcName.indexOf( "Young" ) > 0 ) { gcSingle.gcName = gcSingle.gcName.substring( gcSingle.gcName.indexOf( "Young" ) ); } if( gcSingle.gcName != null && gcSingle.gcName.indexOf( "Old" ) > 0 ) { gcSingle.gcName = gcSingle.gcName.substring( gcSingle.gcName.indexOf( "Old" ) ); } Long[] gcLast = lastMeasurement.get( gcSingle.gcName ); if( gcLast != null ) { gcSingle.gcCountPerPeriod = gc.getCollectionCount() - gcLast[0].longValue(); gcSingle.gcTimePercent = ((gc.getCollectionTime() - gcLast[1].longValue()) / (periodMinutes * 60)) / 10.; } if( gcLast == null || gcSingle.gcCountPerPeriod < 0 || gcSingle.gcTimePercent < 0 ) { // Erstmaliger Aufruf (oder Server-Reboot): gcSingle.gcCountPerPeriod = gc.getCollectionCount() * periodMinutes * 60 * 1000 / rtUptimeMs; gcSingle.gcTimePercent = (gc.getCollectionTime() * 1000 / rtUptimeMs) / 10.; } lastMeasurement.put( gcSingle.gcName, new Long[] { new Long( gc.getCollectionCount() ), new Long( gc.getCollectionTime() ) } ); gcGroup.gcSingles.add( gcSingle ); gcGroup.gcTimePercentSum += gcSingle.gcTimePercent; } // CPU-Zeit: gcGroup.cpuTimePercent = calculateCpuTimePercent( rtUptimeMs, lastMeasurement, mBeanServerConn ); return gcGroup; } // CPU-Zeit static int calculateCpuTimePercent( long rtUptimeMs, Map<String,Long[]> lastMeasurement, MBeanServerConnection mBeanServerConn ) throws Exception { final String CPUTIME_ATTRIBUTENAME = "ProcessCpuTime"; final String CPUTIME_OBJECTNAME = "java.lang:type=OperatingSystem"; final String CPUTIME_KEY = CPUTIME_ATTRIBUTENAME + "::" + CPUTIME_OBJECTNAME; try { Long cpuTime = (Long) mBeanServerConn.getAttribute( new ObjectName( CPUTIME_OBJECTNAME ), CPUTIME_ATTRIBUTENAME ); if( cpuTime == null ) return -1; Long[] lastCpuTimeVals = lastMeasurement.get( CPUTIME_KEY ); lastMeasurement.put( CPUTIME_KEY, new Long[] { new Long( rtUptimeMs ), cpuTime } ); OperatingSystemMXBean op = getOperatingSystemMXBeanFromRemote( mBeanServerConn ); long cpuCount = Math.max( 1, op.getAvailableProcessors() ); long lastRtUptimeMs = 0; long lastCpuTime = 0; if( lastCpuTimeVals != null && lastCpuTimeVals.length > 1 ) { lastRtUptimeMs = lastCpuTimeVals[0].longValue(); lastCpuTime = lastCpuTimeVals[1].longValue(); } return (int) Math.min( 99, (cpuTime.longValue() - lastCpuTime) / ((rtUptimeMs - lastRtUptimeMs) * cpuCount * 10000) ); } catch( Exception ex ) { return -1; } } // Eventuell zusaetzliche MBean-Attribut-Abfragen static AttributeValueAndName[] getAttributes( AttributeValueAndName[] attributeNames, int periodMinutes, Map<String,Long[]> lastMeasurement, MBeanServerConnection mBeanServerConn ) throws Exception { if( attributeNames == null || attributeNames.length <= 0 ) return null; List<AttributeValueAndName> attributesList = new ArrayList<AttributeValueAndName>(); for( AttributeValueAndName attrNam : attributeNames ) { boolean attrFound = false; createJRockitConsoleMBean( attrNam.objectName, mBeanServerConn ); Set<ObjectName> objectNames = mBeanServerConn.queryNames( new ObjectName( attrNam.objectName.trim() ), null ); for( ObjectName objectName : objectNames ) { Object obj = null; if( attrNam.attributeName.trim().equalsIgnoreCase( "invoke" ) ) { obj = invoke( attrNam.methodName, attrNam.methodParms, objectName, mBeanServerConn ); } else { obj = mBeanServerConn.getAttribute( objectName, attrNam.attributeName.trim() ); } AttributeValueAndName attrVal = new AttributeValueAndName(); attrVal.diff = attrNam.diff; attrVal.title = attrNam.title; attrVal.attributeName = attrNam.attributeName; attrVal.objectName = "" + objectName; long actVal = -1; try { actVal = Long.parseLong( "" + obj ); } catch( Exception ex ) {/*ok*/} if( !attrVal.diff || actVal < 0 || periodMinutes <= 0 ) { // Keine Differenzberechnung: attrVal.value = ( obj instanceof Double ) ? DECIMAL_FORMAT2.format( obj ) : ("" + obj); } else { // Differenzberechnung und Umrechnung auf Anzahl pro Sekunde: String key = attrVal.attributeName + "::" + attrVal.objectName; Long[] lastVal = lastMeasurement.get( key ); lastMeasurement.put( key, new Long[] { new Long( actVal ) } ); long v = 0; if( lastVal != null && lastVal[0] != null && (v = actVal - lastVal[0].longValue()) >= 0 ) { // Es gibt einen gueltigen letzten Wert: v = v / periodMinutes / 6; } else { // Lies bisherige Laufzeit der JVM aus Remote-Runtime-MXBean: long rtUptimeMs = getRuntimeMXBeanFromRemote( mBeanServerConn ).getUptime(); v = actVal * 10000 / rtUptimeMs; } attrVal.value = DECIMAL_FORMAT1.format( v / 10. ); } attributesList.add( attrVal ); attrFound = true; } if( !attrFound ) { attributesList.add( attrNam ); } } return attributesList.toArray( new AttributeValueAndName[attributesList.size()] ); } // Rufe MBean-Methode auf static Object invoke( String methodName, String[] parms, ObjectName on, MBeanServerConnection mBeanServerConn ) throws Exception { if( methodName == null || on == null || mBeanServerConn == null ) return null; Object[] oa = null; String[] sa = null; if( parms != null && parms.length >= 2 ) { final Map<String,Class<?>[]> PRIMITIVE_TYPEN = new HashMap<String,Class<?>[]>(); PRIMITIVE_TYPEN.put( "boolean", new Class<?>[] { boolean.class, Boolean.class } ); PRIMITIVE_TYPEN.put( "int", new Class<?>[] { int.class, Integer.class } ); PRIMITIVE_TYPEN.put( "long", new Class<?>[] { long.class, Long.class } ); PRIMITIVE_TYPEN.put( "double", new Class<?>[] { double.class, Double.class } ); oa = new Object[parms.length / 2]; sa = new String[parms.length / 2]; for( int i = 0; i < parms.length - 1; i++ ) { // Sind Parameter primitive Typen? Class<?>[] classForSigAndObj = PRIMITIVE_TYPEN.get( parms[i] ); // Klassen als Parameter-Typen: if( classForSigAndObj == null ) { classForSigAndObj = new Class<?>[2]; try { classForSigAndObj[0] = Class.forName( "java.lang." + parms[i] ); } catch( ClassNotFoundException ex ) { classForSigAndObj[0] = Class.forName( parms[i] ); } classForSigAndObj[1] = classForSigAndObj[0]; } oa[i/2] = classForSigAndObj[1].getConstructor( String.class ).newInstance( parms[++i] ); sa[i/2] = classForSigAndObj[0].getName(); } } return mBeanServerConn.invoke( on, methodName, oa, sa ); } // JMX-Connection static JMXConnector getJMXConnector( String url, String usr, String pwd ) throws MalformedURLException, IOException { String serviceUrl = "service:jmx:rmi:///jndi/rmi://" + url + "/jmxrmi"; if( usr == null || usr.trim().length() <= 0 || pwd == null || pwd.trim().length() <= 0 ) { return JMXConnectorFactory.connect( new JMXServiceURL( serviceUrl ) ); } Map<String,Object> envMap = new HashMap<String,Object>(); envMap.put( "jmx.remote.credentials", new String[] { usr, pwd } ); envMap.put( Context.SECURITY_PRINCIPAL, usr ); envMap.put( Context.SECURITY_CREDENTIALS, pwd ); return JMXConnectorFactory.connect( new JMXServiceURL( serviceUrl ), envMap ); } // Lies Runtime-MXBean von Remote static RuntimeMXBean getRuntimeMXBeanFromRemote( MBeanServerConnection mBeanServerConn ) throws IOException { return ManagementFactory.newPlatformMXBeanProxy( mBeanServerConn, ManagementFactory.RUNTIME_MXBEAN_NAME, RuntimeMXBean.class ); } // Lies OperatingSystem-MXBean von Remote static OperatingSystemMXBean getOperatingSystemMXBeanFromRemote( MBeanServerConnection mBeanServerConn ) throws IOException { return ManagementFactory.newPlatformMXBeanProxy( mBeanServerConn, ManagementFactory.OPERATING_SYSTEM_MXBEAN_NAME, OperatingSystemMXBean.class ); } // Lies GarbageCollector-MXBeans von Remote static List<GarbageCollectorMXBean> getGarbageCollectorMXBeansFromRemote( MBeanServerConnection mBeanServerConn ) throws MalformedObjectNameException, NullPointerException, IOException { List<GarbageCollectorMXBean> gcMXBeans = new ArrayList<GarbageCollectorMXBean>(); ObjectName gcAllObjectName = new ObjectName( ManagementFactory.GARBAGE_COLLECTOR_MXBEAN_DOMAIN_TYPE + ",*" ); Set<ObjectName> gcMXBeanObjectNames = mBeanServerConn.queryNames( gcAllObjectName, null ); for( ObjectName on : gcMXBeanObjectNames ) { GarbageCollectorMXBean gc = ManagementFactory.newPlatformMXBeanProxy( mBeanServerConn, on.getCanonicalName(), GarbageCollectorMXBean.class ); gcMXBeans.add( gc ); } return gcMXBeans; } // Vor einer Abfrage von JRockit-Attributen muss die JRockitConsole-MBean instanziiert werden static void createJRockitConsoleMBean( String objectName, MBeanServerConnection mBeanServerConn ) throws Exception { if( objectName == null || !objectName.startsWith( "bea.jrockit.management" ) ) return; try { mBeanServerConn.getMBeanInfo( new ObjectName( "bea.jrockit.management:type=JRockitConsole" ) ); } catch( InstanceNotFoundException ex ) { mBeanServerConn.createMBean( "bea.jrockit.management.JRockitConsole", null ); } } // Anzeige im Kommandozeilenfenster static void writeConsole( ServerData[] serverDataArr, boolean showConsole ) { if( serverDataArr == null || serverDataArr.length <= 0 || !showConsole ) return; for( ServerData serverData : serverDataArr ) { if( serverData.gcGroup == null ) continue; System.out.print( YYYYMMDD_HHMMSS_STD.format( serverData.gcGroup.dateTime ) + ": " ); System.out.print( serverData.serverNameUndUrl + ": " ); System.out.println( "GarbageCollectionPercent = " + DECIMAL_FORMAT1.format( serverData.gcGroup.gcTimePercentSum ) + " %" ); } for( ServerData serverData : serverDataArr ) { if( serverData.gcGroup == null ) continue; System.out.print( YYYYMMDD_HHMMSS_STD.format( serverData.gcGroup.dateTime ) + ": " ); System.out.print( serverData.serverNameUndUrl + ": " ); System.out.println( "CpuTimePercent = " + serverData.gcGroup.cpuTimePercent + " %" ); } for( ServerData serverData : serverDataArr ) { if( serverData.attributes == null || serverData.attributes.length <= 0 ) continue; for( AttributeValueAndName attr : serverData.attributes ) { if( attr.value == null || attr.value.length() <= 0 || attr.value.equals( AttributeValueAndName.ERR_VALUE ) ) continue; System.out.print( YYYYMMDD_HHMMSS_STD.format( attr.dateTime ) + ": " ); System.out.print( serverData.serverNameUndUrl + ": " ); System.out.println( attr.title + " = " + attr.value ); } } System.out.println(); } // Schreibe letzte Summenergebnisse in Datei (z.B. fuer Nagios) static void writeNagiosFile( ServerData[] serverDataArr, String nagiosFile ) { if( serverDataArr == null || serverDataArr.length <= 0 || nagiosFile == null || nagiosFile.trim().length() <= 0 ) return; BufferedWriter out = null; try { Date dat = new Date(); out = new BufferedWriter( new OutputStreamWriter( new FileOutputStream( nagiosFile ) ) ); out.write( "SecondsSince1970=" + (dat.getTime() / 1000) ); out.newLine(); out.write( "DateTime=" + YYYYMMDD_HHMMSS_NAG.format( dat ) ); out.newLine(); for( ServerData serverData : serverDataArr ) { if( serverData.gcGroup == null) continue; out.write( serverData.serverName.replaceAll( "[:-]", "." ) + "." ); out.write( "GarbageCollectionPercent=" + DECIMAL_FORMAT1.format( serverData.gcGroup.gcTimePercentSum ).replace( ',', '.' ) ); out.newLine(); } for( ServerData serverData : serverDataArr ) { if( serverData.gcGroup == null) continue; out.write( serverData.serverName.replaceAll( "[:-]", "." ) + "." ); out.write( "CpuTimePercent=" + serverData.gcGroup.cpuTimePercent ); out.newLine(); } for( ServerData serverData : serverDataArr ) { if( serverData.attributes == null || serverData.attributes.length <= 0 ) continue; for( AttributeValueAndName attr : serverData.attributes ) { if( attr.value == null || attr.value.length() <= 0 || attr.value.equals( AttributeValueAndName.ERR_VALUE ) ) continue; out.write( serverData.serverName.replaceAll( "[:-]", "." ) + "." ); out.write( attr.title + "=" + attr.value.replace( ',', '.' ).replaceAll( "\r\n|\r|\n", ". " ) ); out.newLine(); } } } catch( Exception exWrite ) { System.out.println( "Fehler beim Schreiben der Nagios-Datei '" + nagiosFile + "': " + exWrite ); } finally { if( out != null ) try { out.close(); } catch( Exception exClose ) {/*ok*/} } } // Ausgabe in einzelne CSV-Datei (Comma Separated Values, z.B. fuer Excel): // Fuer alle Server eine gemeinsame CSV-Datei (GC: nur mit den Summenwerten) static void writeCsvFileOneForAllServers( ServerData[] serverDataArr, String csvFile ) { if( serverDataArr == null || serverDataArr.length <= 0 || csvFile == null || csvFile.trim().length() <= 0 ) return; BufferedWriter out = null; try { boolean exists = (new File( csvFile )).exists(); out = new BufferedWriter( new OutputStreamWriter( new FileOutputStream( csvFile, true ) ) ); if( !exists ) { out.write( "Datum/Zeit;" ); for( ServerData serverData : serverDataArr ) { out.write( " " + (( serverData != null ) ? ("GC-" + serverData.serverName) : "?") + ";" ); } for( ServerData serverData : serverDataArr ) { out.write( " " + (( serverData != null ) ? ("CPU-" + serverData.serverName) : "?") + ";" ); } for( ServerData serverData : serverDataArr ) { if( serverData != null && serverData.attributes != null ) { for( AttributeValueAndName attr : serverData.attributes ) { out.write( " " + attr.title + "-" + serverData.serverName + ";" ); } } } out.newLine(); } out.write( YYYYMMDD_HHMMSS_STD.format( new Date() ) + ";" ); for( ServerData serverData : serverDataArr ) { double d = ( serverData != null && serverData.gcGroup != null ) ? serverData.gcGroup.gcTimePercentSum : -0.1; out.write( " " + DECIMAL_FORMAT1.format( d ) + ";" ); } for( ServerData serverData : serverDataArr ) { out.write( (( serverData != null && serverData.gcGroup != null ) ? (" " + serverData.gcGroup.cpuTimePercent) : " -0.1") + ";" ); } for( ServerData serverData : serverDataArr ) { if( serverData != null && serverData.attributes != null ) { for( AttributeValueAndName attr : serverData.attributes ) { out.write( " " + attr.value.replaceAll( "\r\n|\r|\n", ". " ) + ";" ); } } } out.newLine(); } catch( Exception exWrite ) { System.out.println( "Fehler beim Schreiben der CSV-Datei '" + csvFile + "': " + exWrite ); } finally { if( out != null ) try { out.close(); } catch( Exception exClose ) {/*ok*/} } } // Ausgabe in mehrere CSV-Dateien (Comma Separated Values, z.B. fuer Excel): // Pro Server eine CSV-Datei mit allen einzelnen GC-Werten static void writeCsvFilePerServerWithDifferentGcValues( ServerData[] serverDataArr, String csvFileOhneUrl, boolean writeAllGcValues ) { if( !writeAllGcValues || serverDataArr == null || serverDataArr.length <= 0 || csvFileOhneUrl == null || csvFileOhneUrl.trim().length() <= 0 ) return; for( ServerData serverData : serverDataArr ) { String urlInsert = "-" + serverData.url.replace( ':', '.' ); int e = csvFileOhneUrl.lastIndexOf( '.' ); String csvFileMitUrl = ( e > 0 && e < csvFileOhneUrl.length() - 1 ) ? csvFileOhneUrl.substring( 0, e ) + urlInsert + csvFileOhneUrl.substring( e ) : csvFileOhneUrl + urlInsert + ".csv"; BufferedWriter out = null; try { if( serverData.gcGroup == null || serverData.gcGroup.gcSingles == null || serverData.gcGroup.gcSingles.size() <= 0 ) continue; boolean exists = (new File( csvFileMitUrl )).exists(); out = new BufferedWriter( new OutputStreamWriter( new FileOutputStream( csvFileMitUrl, true ) ) ); if( !exists ) { out.write( "Datum/Zeit; " ); for( GarbageCollectionSingle gcSingle : serverData.gcGroup.gcSingles ) { int n; String s = gcSingle.gcName; if( s == null ) s = ""; if( (n = s.lastIndexOf( " Collector" )) > 1 ) s = s.substring( 0, n ); s = s.trim(); if( s.length() > 0 ) s = s + "-"; out.write( s + "CountPerPeriod; " + s + "TimePercent; " ); } out.write( "TimePercentSum;" ); out.newLine(); } out.write( YYYYMMDD_HHMMSS_STD.format( serverData.gcGroup.dateTime ) + "; " ); for( GarbageCollectionSingle gcSingle : serverData.gcGroup.gcSingles ) { out.write( gcSingle.gcCountPerPeriod + "; " + DECIMAL_FORMAT1.format( gcSingle.gcTimePercent ) + "; " ); } out.write( DECIMAL_FORMAT1.format( serverData.gcGroup.gcTimePercentSum ) + "; " ); out.newLine(); } catch( Exception exWrite ) { System.out.println( "Fehler beim Schreiben der CSV-Datei '" + csvFileMitUrl + "': " + exWrite ); } finally { if( out != null ) try { out.close(); } catch( Exception exClose ) {/*ok*/} } } } // Schreibe Exceptions in Fehlerdatei static void writeErrorFile( String s, Exception ex, String errorFile ) { if( errorFile == null || errorFile.trim().length() <= 0 ) return; BufferedWriter out = null; try { out = new BufferedWriter( new OutputStreamWriter( new FileOutputStream( errorFile, true ) ) ); out.newLine(); if( s != null ) out.write( s ); out.newLine(); if( ex != null ) out.write( ex.toString() ); out.newLine(); out.close(); } catch( Exception exWrite ) { System.out.println( "Fehler beim Schreiben in die Datei '" + errorFile + "': " + exWrite ); } finally { if( out != null ) try { out.close(); } catch( Exception exClose ) {/*ok*/} } } // Lies Parameter in ein Properties-Objekt: // a) Parameter aus der Kommandozeile (haben Vorrang), // b) Parameter aus einer Properties-Datei, // c) Eventuell Default-Parameter (falls Parameter nicht anderweitig gesetzt wurde). // Falls ueber die Kommandozeile eine Properties-Datei definiert wurde, muss diese vorhanden und lesbar sein // (Fehler fuehren zu Fehlermeldungen). // Falls nicht: Es wird nach der als Methodenparameter uebergebenen (Default-)Properties-Datei gesucht. // Gibt es diese nicht, erscheint keine Fehlermeldung. static Properties readProperties( String[] args, String keyPropFile, String propFileTry, String[] defaultProps ) throws Exception { Properties props = new Properties(); String propFilePrio = null; String s; // Wenn Properties-Dateiname als Kommandozeilenparameter uebergeben wurde, dann hat dies Vorrang: if( args != null && args.length > 0 && keyPropFile != null && keyPropFile.trim().length() > 0 && args[0].toLowerCase().startsWith( keyPropFile + "=" ) ) { propFilePrio = args[0].substring( keyPropFile.length() + 1 ); } // Der als Methodenparameter uebergebene Properties-Dateiname wird nur verwendet, // wenn diese Datei existiert (ansonsten keine Fehlermeldung): if( propFilePrio == null && propFileTry != null && propFileTry.trim().length() > 0 && (new File( propFileTry )).exists() ) { propFilePrio = propFileTry; } // Falls ein Properties-Dateiname ermittelt werden konnte: // Lies 'Key=Value'-Paare aus Property-Datei (Fehler fuehrt zu Fehlermeldung): if( propFilePrio != null && propFilePrio.trim().length() > 0 ) { try { props.load( new FileInputStream( propFilePrio ) ); System.out.println( "Property-Datei: '" + propFilePrio + "'." ); } catch( FileNotFoundException ex ) { throw new Exception( "Fehler: Property-Datei '" + propFilePrio + "' fehlt: ", ex ); } catch( IOException ex ) { throw new Exception( "Fehler beim Lesen der Datei '" + propFilePrio + "': ", ex ); } } // Lies 'Key=Value'-Paare aus Kommandozeilenparametern // (koennen Parameter aus obiger Property-Datei ueberschreiben): for( int i = 0; args != null && i < args.length; i++ ) { int delimPos = args[i].indexOf( '=' ); if( delimPos > 0 && delimPos <= args[i].length() - 2 ) { props.put( args[i].substring( 0, delimPos ).trim().toLowerCase(), args[i] .substring( delimPos + 1 ).trim() ); } } // Lade eventuell Defaultwerte: if( defaultProps != null && defaultProps.length / 2 > 0 ) { for( int i = 0; i < defaultProps.length / 2; i++ ) { if( (s = props.getProperty( defaultProps[i * 2] )) == null || s.trim().length() == 0 ) { props.put( defaultProps[i * 2], defaultProps[i * 2 + 1] ); } } } return props; } } // Zugangsparameter und Ergebnisse eines Servers class ServerData { String serverNameUndUrl; String serverName; String url; String usr; String pwd; Map<String,Long[]> lastMeasurement = new HashMap<String,Long[]>(); GarbageCollectionGroup gcGroup = null; AttributeValueAndName[] attributes = null; } // Gruppe von Garbage-Collection-Werten (verschiedene Garbage-Collection-Arten) und CPU-Zeit class GarbageCollectionGroup { Date dateTime = new Date(); List<GarbageCollectionSingle> gcSingles = new ArrayList<GarbageCollectionSingle>(); double gcTimePercentSum; long cpuTimePercent; } // Messwerte einer einzelnen Garbage-Collection-Art class GarbageCollectionSingle { String gcName; long gcCountPerPeriod; double gcTimePercent; } // Falls Abfrage zusaetzlicher MBean-Attribute: Namen und Messwert class AttributeValueAndName { static final String ERR_VALUE = "-0,1"; Date dateTime = new Date(); boolean diff; String value = ERR_VALUE; String title; String attributeName; String objectName; String methodName; String[] methodParms; }
Eine Anwendung und ein Beispiel für einen Kommandozeilenaufruf finden Sie im folgenden Kapitel MeinBoesesMemoryLeak.
Mit dem folgendem Programm "MeinBoesesMemoryLeak" können Sie hohen temporären Speicherverbrauch (häufige String-Concatenation bei meinString += i) und ein stetig anwachsendes Memory Leak (in meineListe) simulieren. Passen Sie den Schleifenzählerwert "i<1000" so an, dass das Programm genügend lange (z.B. 6 Minuten) läuft, bevor es sich mit OutOfMemoryError verabschiedet. Die Pause von 5 Sekunden zu Beginn ("Thread.sleep(5000)") dient nur dazu, schnell genug Monitoring-Programme aktivieren zu können.
import java.util.*; public class MeinBoesesMemoryLeak { public static void main( String[] args ) throws InterruptedException { Thread.sleep( 5000 ); String meinString = ""; List<String> meineListe = new ArrayList<String>(); while( true ) { for( int i=0; i<1000; i++ ) { meinString += i; } meineListe.add( meinString ); long meinVerbrauchterSpeicher = 0; for( String str : meineListe ) { meinVerbrauchterSpeicher += str.length(); } System.out.println("Verbrauchter Speicher: " + (meinVerbrauchterSpeicher / 1024) + " KByte; Laenge letzter String / 1000: " + (meinString.length() / 1000) ); } } }
Erzeugen Sie MeinBoesesMemoryLeak.java und JmxServerMonitoring.java in einem src-Unterverzeichnis, erzeugen Sie ein bin-Unterverzeichnis und kompilieren Sie die Sourcen:
javac -d bin src/*.java
Starten Sie dieses Memory-Leak-Programm, das obige Monitoring-Programm und JConsole kurz hintereinander mit folgenden Kommandos (passen Sie die Classpathes "-cp bin" an):
start java -Xmx64M -Dcom.sun.management.jmxremote.port=4711 -Dcom.sun.management.jmxremote.authenticate=false -Dcom.sun.management.jmxremote.ssl=false -cp bin MeinBoesesMemoryLeak
start java -cp bin JmxServerMonitoring periodminutes=1 console=true allgcvalues=true csvfile=JmxServerMonitoring.csv url=localhost:4711
start jconsole -interval=1 localhost:4711
Circa einmal pro Minute während MeinBoesesMemoryLeak läuft:
type JmxServerMonitoring.nagios.txt
type JmxServerMonitoring-localhost.4711.csv
Nach Beendigung von MeinBoesesMemoryLeak die .csv-Datei zum Beispiel in Excel o.ä. laden:
start JmxServerMonitoring-localhost.4711.csv
In der .csv-Datei JmxServerMonitoring-localhost.4711.csv finden Sie alle GC-Ergebnisse: Zusätzlich zur summarischen prozentualen Garbage-Collection-Zeit auch die Zeiten aufgeteilt nach Minor und Major Collection und auch die Anzahl der Collections. Diese Datei können Sie in Excel laden und Kurvenverlaufsgrafiken erstellen, wie oben beschrieben ist.
Die prozentuale GC-Zeit sollte normalerweise deutlich kleiner als 10 % sein. Bei diesem Beispiel steigt sie bis über 90 %, bevor sich MeinBoesesMemoryLeak mit OutOfMemoryError verabschiedet:
2008-09-06 00:00:00: localhost:4711: GarbageCollectionPercent = 0.4 %
2008-09-06 00:01:00: localhost:4711: GarbageCollectionPercent = 63.0 %
2008-09-06 00:02:00: localhost:4711: GarbageCollectionPercent = 45.4 %
2008-09-06 00:03:00: localhost:4711: GarbageCollectionPercent = 34.8 %
2008-09-06 00:04:00: localhost:4711: GarbageCollectionPercent = 75.1 %
2008-09-06 00:05:00: localhost:4711: GarbageCollectionPercent = 90.7 %
Bitte beachten: JmxServerMonitoring meldet die Collection-Anzahl und die verbrauchte Zeit pro Messintervall, während JConsole die Summe seit JVM-Start anzeigt.
In jmx.htm finden Sie eine längere Version von MeinBoesesMemoryLeak, welche zusätzlich auch intern die prozentuale Garbage Collection und die CPU-Auslastung ermittelt.
Im Javamagazin-Artikel Überwachung von JEE-Applikationen bei 1&1 stellt Dr. Dietmar Posselt vor, wie Zustandsinformationen aus den JEE-Applikationenen von einem EJB Timer Service gesteuert periodisch über MBeans gesammelt und in einer Datenbank aggregiert und gespeichert werden können.
Hierzu finden Sie eine Liste von Matthew Quinlan unter: http://www.jboss.org/community/docs/DOC-11440.