Статьи

JMX и весна — часть 3

Эта статья является последней из этой серии. Взгляните на часть 1 и часть 2 .

В этой последней статье серии я покажу, как использовать встроенную поддержку JMX в JDK для реализации механизма уведомлений, который предупреждает слушателя, когда объем памяти HEAP превышает определенный порог.

Как обсуждалось в моей предыдущей статье, этот подход идеален, потому что он толкает, а не тянет, не навязчив и требует минимальных вычислительных затрат для вашего приложения.

Это ключевые компоненты решения, показанного в этой статье:

  • MemoryWarningService: этот компонент действует как прослушиватель и регистрируется в памяти MBean для получения уведомлений. Это настраивается с порогом в виде процента от 0 до 1 (где 1 — 100%)
  • MemoryThreadDumper: этот компонент вызывается, когда MemoryWarningService уведомляется о том, что использование HEAP превышает пороговое значение, и его обязанность — записать дамп потока в файл.
  • MemoryWarningServiceConfigurator: этот компонент является MBean и предоставляет метод для изменения порога MemoryWarningService.

Решение также предоставляет класс MemoryHeapFiller, используемый для заполнения HEAP при тестировании приложения, и класс MemTest для начальной загрузки среды Spring.

Во время работы приложения (игра с настройками MemoryHeapFiller) Вы можете запустить JConsole по адресу URL: service: jmx: rmi: // localhost / jndi / rmi: // localhost: 8888 / jemosJmxConnector, подключившись как jemosAdmin / secure, и изменить порог к различным значениям.

Код не предназначен для производства: он не является надежным, отсутствуют многочисленные комментарии, а имя файла для записи дампа потока жестко запрограммировано; однако, это хорошая отправная точка.

Код прилагается ниже. Вам понадобится Maven, чтобы построить его.

Загрузить Jemos-jmx-эксперименты-0.0.1-SNAPSHOT-проект

Я пробовал сценарий с начальным порогом, равным 0,5, я изменил его до 0,3, а затем до 0,8. Результаты показаны ниже:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
2011-08-15 21:53:21 ClassPathXmlApplicationContext [INFO] Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@a4a63d8: startup date [Mon Aug 15 21:53:21 BST 2011]; root of context hierarchy
2011-08-15 21:53:21 XmlBeanDefinitionReader [INFO] Loading XML bean definitions from class path resource [jemos-jmx-appCtx.xml]
2011-08-15 21:53:21 PropertyPlaceholderConfigurer [INFO] Loading properties file from class path resource [jemos-jmx.properties]
2011-08-15 21:53:21 PropertyPlaceholderConfigurer [INFO] Loading properties file from URL [file:/C:/Users/mtedone/.secure/jmxconnector-credentials.properties]
2011-08-15 21:53:21 ThreadPoolTaskScheduler [INFO] Initializing ExecutorService  'myScheduler'
2011-08-15 21:53:21 ClassPathXmlApplicationContext [INFO] Bean 'myScheduler' of type [class org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
2011-08-15 21:53:21 DefaultListableBeanFactory [INFO] Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@30296f76: defining beans [propertyConfigurer,loggerConfigurator,memoryWarningServiceConfigurator,memoryHeapFiller,memoryThreadDumper,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springframework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,mbeanExporter,jemosJmxServer,rmiRegistry,clientConnector,memoryMxBean,memoryWarningService,org.springframework.scheduling.annotation.internalAsyncAnnotationProcessor,org.springframework.scheduling.annotation.internalScheduledAnnotationProcessor,myScheduler]; root of factory hierarchy
2011-08-15 21:53:21 AnnotationMBeanExporter [INFO] Registering beans for JMX exposure on startup
2011-08-15 21:53:21 RmiRegistryFactoryBean [INFO] Looking for RMI registry at port '8888'
2011-08-15 21:53:23 RmiRegistryFactoryBean [INFO] Could not detect RMI registry - creating new one
2011-08-15 21:53:23 ConnectorServerFactoryBean [INFO] JMX connector server started: javax.management.remote.rmi.RMIConnectorServer@4355d3a3
2011-08-15 21:53:23 AnnotationMBeanExporter [INFO] Bean with name 'jemosJmxServer' has been autodetected for JMX exposure
2011-08-15 21:53:23 AnnotationMBeanExporter [INFO] Bean with name 'loggerConfigurator' has been autodetected for JMX exposure
2011-08-15 21:53:23 AnnotationMBeanExporter [INFO] Bean with name 'memoryWarningServiceConfigurator' has been autodetected for JMX exposure
2011-08-15 21:53:23 AnnotationMBeanExporter [INFO] Located managed bean 'loggerConfigurator': registering with JMX server as MBean [jemos.mbeans:type=config,name=LoggingConfiguration]
2011-08-15 21:53:23 AnnotationMBeanExporter [INFO] Located MBean 'jemosJmxServer': registering with JMX server as MBean [jemos.mbeans:name=jemosJmxServer,type=RMIConnectorServer]
2011-08-15 21:53:23 AnnotationMBeanExporter [INFO] Located managed bean 'memoryWarningServiceConfigurator': registering with JMX server as MBean [jemos.mbeans:type=config,name=MemoryWarningServiceConfiguration]
2011-08-15 21:53:23 MemoryWarningService [INFO] Percentage is: 0.5
2011-08-15 21:53:23 MemoryWarningService [INFO] Listener added to JMX bean
Adding data...
Adding data...
Adding data...
Adding data...
Adding data...
Adding data...
Adding data...
2011-08-15 21:53:37 MemoryWarningService [INFO] Percentage is: 0.3
2011-08-15 21:53:37 MemoryWarningServiceConfigurator [INFO] Memory threshold set to 0.3
Adding data...
2011-08-15 21:53:38 MemoryWarningService [WARN] Memory usage low!!!
2011-08-15 21:53:38 MemoryWarningService [WARN] percentageUsed = 0.3815679398794023
2011-08-15 21:53:38 MemoryThreadDumper [WARN] Stacks dumped to: C:/tmp/stacks.dump
Adding data...
Adding data...
Adding data...
2011-08-15 21:53:45 MemoryWarningService [INFO] Percentage is: 0.8
2011-08-15 21:53:45 MemoryWarningServiceConfigurator [INFO] Memory threshold set to 0.8
Adding data...
Adding data...
Adding data...
Adding data...
Adding data...
Adding data...
Adding data...
2011-08-15 21:54:01 MemoryWarningService [WARN] Memory usage low!!!
2011-08-15 21:54:01 MemoryWarningService [WARN] percentageUsed = 0.8383333266727508
2011-08-15 21:54:02 MemoryThreadDumper [WARN] Stacks dumped to: C:/tmp/stacks.dump
Adding data...
Adding data...
Adding data...
Exception in thread "JMX server connection timeout 24" java.lang.OutOfMemoryError: Java heap space

Служба предупреждений памяти

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
package uk.co.jemos.experiments.jmx;
 
import java.lang.management.ManagementFactory;
import java.lang.management.MemoryNotificationInfo;
import java.lang.management.MemoryPoolMXBean;
import java.lang.management.MemoryType;
 
import javax.annotation.PostConstruct;
import javax.management.Notification;
import javax.management.NotificationEmitter;
import javax.management.NotificationListener;
 
import org.springframework.beans.factory.annotation.Autowired;
 
/**
 * A component which sends notifications when the HEAP memory is above a certain
 * threshold.
 *
 * @author mtedone
 *
 */
public class MemoryWarningService implements NotificationListener {
 
    /** This bean's name */
    public static final String MBEAN_NAME = "jemos.mbeans:type=monitoring,name=MemoryWarningService";
 
    /** The application logger */
    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger
            .getLogger(MemoryWarningService.class);
 
    @Autowired
    private NotificationEmitter memoryMxBean;
 
    @Autowired
    private MemoryThreadDumper threadDumper;
 
    /** A pool of Memory MX Beans specialised in HEAP management */
    private static final MemoryPoolMXBean tenuredGenPool = findTenuredGenPool();
 
    /**
     * {@inheritDoc}
     */
    @Override
    public void handleNotification(Notification notification, Object handback) {
 
        if (notification.getType().equals(
                MemoryNotificationInfo.MEMORY_THRESHOLD_EXCEEDED)) {
            long maxMemory = tenuredGenPool.getUsage().getMax();
            long usedMemory = tenuredGenPool.getUsage().getUsed();
            LOG.warn("Memory usage low!!!");
            double percentageUsed = (double) usedMemory / maxMemory;
            LOG.warn("percentageUsed = " + percentageUsed);
            threadDumper.dumpStacks();
 
        } else {
            LOG.info("Other notification received..."
                    + notification.getMessage());
        }
 
    }
 
    /**
     * It sets the threshold percentage.
     *
     * @param percentage
     */
    public void setPercentageUsageThreshold(double percentage) {
        if (percentage <= 0.0 || percentage > 1.0) {
            throw new IllegalArgumentException("Percentage not in range");
        } else {
            LOG.info("Percentage is: " + percentage);
        }
        long maxMemory = tenuredGenPool.getUsage().getMax();
        long warningThreshold = (long) (maxMemory * percentage);
        tenuredGenPool.setUsageThreshold(warningThreshold);
    }
 
    @PostConstruct
    public void completeSetup() {
        memoryMxBean.addNotificationListener(this, null, null);
        LOG.info("Listener added to JMX bean");
    }
 
    /**
     * Tenured Space Pool can be determined by it being of type HEAP and by it
     * being possible to set the usage threshold.
     */
    private static MemoryPoolMXBean findTenuredGenPool() {
        for (MemoryPoolMXBean pool : ManagementFactory.getMemoryPoolMXBeans()) {
            // I don't know whether this approach is better, or whether
            // we should rather check for the pool name "Tenured Gen"?
            if (pool.getType() == MemoryType.HEAP
                    && pool.isUsageThresholdSupported()) {
                return pool;
            }
        }
        throw new AssertionError("Could not find tenured space");
    }
 
}

Дампер нитей памяти

001
002
003
004
005
006
007
008
009
010
011
012
013
014
015
016
017
018
019
020
021
022
023
024
025
026
027
028
029
030
031
032
033
034
035
036
037
038
039
040
041
042
043
044
045
046
047
048
049
050
051
052
053
054
055
056
057
058
059
060
061
062
063
064
065
066
067
068
069
070
071
072
073
074
075
076
077
078
079
080
081
082
083
084
085
086
087
088
089
090
091
092
093
094
095
096
097
098
099
100
101
102
103
104
105
package uk.co.jemos.experiments.jmx;
 
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.Writer;
import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
 
import org.apache.commons.io.IOUtils;
import org.springframework.stereotype.Component;
 
/**
 * This component dumps the thread stack to the file system.
 *
 * @author mtedone
 *
 */
@Component
public class MemoryThreadDumper {
 
    /** The application logger */
    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger
            .getLogger(MemoryThreadDumper.class);
 
    /**
     * It dumps the Thread stacks
     *
     * @throws IOException
     */
    public void dumpStacks() {
 
        // hard-coded: This needs to be changed to a property or something
        String stackFileName = "C:/tmp/stacks.dump";
 
        ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
        ThreadInfo[] threadInfos = mxBean.getThreadInfo(
                mxBean.getAllThreadIds(), 0);
        Map<Long, ThreadInfo> threadInfoMap = new HashMap<Long, ThreadInfo>();
        for (ThreadInfo threadInfo : threadInfos) {
            threadInfoMap.put(threadInfo.getThreadId(), threadInfo);
        }
 
        File dumpFile = new File(stackFileName);
        BufferedWriter writer = null;
        try {
            writer = new BufferedWriter(new FileWriter(dumpFile));
            this.dumpTraces(mxBean, threadInfoMap, writer);
 
            LOG.warn("Stacks dumped to: " + stackFileName);
 
        } catch (IOException e) {
            throw new IllegalStateException(
                    "An exception occurred while writing the thread dump");
        } finally {
            IOUtils.closeQuietly(writer);
        }
 
    }
 
    private void dumpTraces(ThreadMXBean mxBean,
            Map<Long, ThreadInfo> threadInfoMap, Writer writer)
            throws IOException {
        Map<Thread, StackTraceElement[]> stacks = Thread.getAllStackTraces();
        writer.write("Dump of "
                + stacks.size()
                + " thread at "
                + new SimpleDateFormat("yyyy/MM/dd HH:mm:ss z")
                        .format(new Date(System.currentTimeMillis())) + "\n\n");
        for (Map.Entry<Thread, StackTraceElement[]> entry : stacks.entrySet()) {
            Thread thread = entry.getKey();
            writer.write("\"" + thread.getName() + "\" prio="
                    + thread.getPriority() + " tid=" + thread.getId() + " "
                    + thread.getState() + " "
                    + (thread.isDaemon() ? "deamon" : "worker") + "\n");
            ThreadInfo threadInfo = threadInfoMap.get(thread.getId());
            if (threadInfo != null) {
                writer.write("    native=" + threadInfo.isInNative()
                        + ", suspended=" + threadInfo.isSuspended()
                        + ", block=" + threadInfo.getBlockedCount() + ", wait="
                        + threadInfo.getWaitedCount() + "\n");
                writer.write("    lock=" + threadInfo.getLockName()
                        + " owned by " + threadInfo.getLockOwnerName() + " ("
                        + threadInfo.getLockOwnerId() + "), cpu="
                        + mxBean.getThreadCpuTime(threadInfo.getThreadId())
                        / 1000000L + ", user="
                        + mxBean.getThreadUserTime(threadInfo.getThreadId())
                        / 1000000L + "\n");
            }
            for (StackTraceElement element : entry.getValue()) {
                writer.write("        ");
                writer.write(element.toString());
                writer.write("\n");
            }
            writer.write("\n");
        }
    }
 
}

Конфигурация службы памяти MBean

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package uk.co.jemos.experiments.jmx.mbeans;
 
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.jmx.export.annotation.ManagedOperation;
import org.springframework.jmx.export.annotation.ManagedOperationParameter;
import org.springframework.jmx.export.annotation.ManagedOperationParameters;
import org.springframework.jmx.export.annotation.ManagedResource;
import org.springframework.stereotype.Component;
 
import uk.co.jemos.experiments.jmx.MemoryWarningService;
 
@Component
@ManagedResource(objectName = MemoryWarningServiceConfigurator.MBEAN_NAME, //
description = "Allows clients to set the memory threshold")
public class MemoryWarningServiceConfigurator implements
        ApplicationContextAware {
 
     
 
    /** The application logger */
    private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger
            .getLogger(MemoryWarningServiceConfigurator.class);
 
    public static final String MBEAN_NAME = "jemos.mbeans:type=config,name=MemoryWarningServiceConfiguration";
 
    private ApplicationContext ctx;
 
    @ManagedOperation(description = "Sets the memory threshold for the memory warning system")
    @ManagedOperationParameters({ @ManagedOperationParameter(description = "The memory threshold", name = "memoryThreshold"), })
    public void setMemoryThreshold(double memoryThreshold) {
 
        MemoryWarningService memoryWarningService = (MemoryWarningService) ctx
                .getBean("memoryWarningService");
        memoryWarningService.setPercentageUsageThreshold(memoryThreshold);
 
        LOG.info("Memory threshold set to " + memoryThreshold);
    }
 
    @Override
    public void setApplicationContext(ApplicationContext applicationContext)
            throws BeansException {
        ctx = applicationContext;
 
    }
 
 
}

Конфигурация Spring

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
<?xml version="1.0" encoding="UTF-8"?>
 
 
    <bean id="propertyConfigurer" class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer"  >
        <property name="locations">
            <list>
                <value>classpath:jemos-jmx.properties</value>
                <value>file:///${user.home}/.secure/jmxconnector-credentials.properties
                </value>
            </list>
        </property>
    </bean>
 
    <context:component-scan base-package="uk.co.jemos.experiments.jmx" />
 
    <context:mbean-export default-domain="jemos.mbeans" />
 
    <bean id="jemosJmxServer" class="org.springframework.jmx.support.ConnectorServerFactoryBean"      
        depends-on="rmiRegistry">
        <property name="objectName" value="connector:name=rmi" />
        <property name="serviceUrl"
            value="service:jmx:rmi://localhost/jndi/rmi://localhost:${jemos.jmx.rmi.port}/jemosJmxConnector" />
        <property name="environment">
            <!-- the following is only valid when the sun jmx implementation is used -->
            <map>
                <entry key="jmx.remote.x.password.file" value="${user.home}/.secure/jmxremote.password" />
                <entry key="jmx.remote.x.access.file" value="${user.home}/.secure/jmxremote.access" />
            </map>
        </property>
    </bean>
 
    <bean id="rmiRegistry" class="org.springframework.remoting.rmi.RmiRegistryFactoryBean">
        <property name="port" value="${jemos.jmx.rmi.port}" />
    </bean>
 
    <bean id="clientConnector" class="org.springframework.jmx.support.MBeanServerConnectionFactoryBean"      
        depends-on="jemosJmxServer">
        <property name="serviceUrl"
            value="service:jmx:rmi://localhost/jndi/rmi://localhost:${jemos.jmx.rmi.port}/jemosJmxConnector" />
        <property name="environment">
            <map>
                <entry key="jmx.remote.credentials">
                    <bean
                        factory-method="commaDelimitedListToStringArray">
                        <constructor-arg value="${jmx.username},${jmx.password}" />
                    </bean>
                </entry>
            </map>
        </property>
    </bean>
 
 
    <bean id="memoryMxBean" class="java.lang.management.ManagementFactory"
        factory-method="getMemoryMXBean" />
         
    <bean id="memoryWarningService" class="uk.co.jemos.experiments.jmx.MemoryWarningService">
      <property name="percentageUsageThreshold" value="0.5" />
    </bean>
         
 
    <task:annotation-driven scheduler="myScheduler" />  
 
    <task:scheduler id="myScheduler" pool-size="10" />   
         
 
 
</beans>

Ссылка: JMX и Spring — часть 3 от нашего партнера по JCG