OutOfMemoryError warning system with Spring
Join the DZone community and get the full member experience.
Join For FreeEveryone of us who has developed Java applications more involving that the HelloWorld example knows of the OOME. This happens when the Tenured Generation (old space) is filled up and there is no more available memory on the HEAP.
Heinz Kabutz, the world famous Java Champion and low-level, performant Java educator explains this approach in his newsletter 92. This article builds on his newsletter and demonstrates how to introduce, very simply, an OutOfMemoryError warning system in your applications with the help of Spring.
The beauty of this solution is that it uses features natively available in the JDK by using JMX MBean server capabilities.
The OutOfMemoryError Warning class
The bulk of the OOME warning system is the following class:
package uk.co.jemos.experiments.oome; import java.lang.management.ManagementFactory; import java.lang.management.MemoryMXBean; import java.lang.management.MemoryNotificationInfo; import java.lang.management.MemoryPoolMXBean; import java.lang.management.MemoryType; import java.util.ArrayList; import java.util.Collection; import javax.management.Notification; import javax.management.NotificationEmitter; import javax.management.NotificationListener; /** * This memory warning system will call the listener when we exceed the * percentage of available memory specified. There should only be one instance * of this object created, since the usage threshold can only be set to one * number. */ public class MemoryWarningSystem { private final Collection<Listener> listeners = new ArrayList<Listener>(); private static final MemoryPoolMXBean tenuredGenPool = findTenuredGenPool(); public MemoryWarningSystem() { MemoryMXBean mbean = ManagementFactory.getMemoryMXBean(); NotificationEmitter emitter = (NotificationEmitter) mbean; emitter.addNotificationListener(new NotificationListener() { public void handleNotification(Notification n, Object hb) { if (n.getType().equals( MemoryNotificationInfo.MEMORY_THRESHOLD_EXCEEDED)) { long maxMemory = tenuredGenPool.getUsage().getMax(); long usedMemory = tenuredGenPool.getUsage().getUsed(); for (Listener listener : listeners) { listener.memoryUsageLow(usedMemory, maxMemory); } } } }, null, null); } public boolean addListener(Listener listener) { return listeners.add(listener); } public boolean removeListener(Listener listener) { return listeners.remove(listener); } public void setPercentageUsageThreshold(double percentage) { if (percentage <= 0.0 || percentage > 1.0) { throw new IllegalArgumentException("Percentage not in range"); } long maxMemory = tenuredGenPool.getUsage().getMax(); long warningThreshold = (long) (maxMemory * percentage); tenuredGenPool.setUsageThreshold(warningThreshold); } /** * 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"); } }
The responsibility of this class is simple: everytime the JMX infrastructure communicates that the memory threshold has been exceeded this class notifies all its listeners passing as arguments the maximum memory available and the memory used so far.
The Listener
In this example, the listener simply prints out the memory information, but in a real production system it could, for instance, send a notifacation to a monitoring tool for the infrastructure folks to be notified, or send alarms to BlackBerries, etc.
public void memoryUsageLow(long usedMemory, long maxMemory) { System.out.println("Memory usage low!!!"); double percentageUsed = (double) usedMemory / maxMemory; System.out.println("percentageUsed = " + percentageUsed); }
The Spring configuration
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context" xmlns:task="http://www.springframework.org/schema/task" xsi:schemaLocation="http://www.springframework.org/schema/task http://www.springframework.org/schema/task/spring-task-3.0.xsd http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd"> <context:component-scan base-package="uk.co.jemos.experiments.oome" /> <bean id="memoryWarningSystem"> <property name="percentageUsageThreshold" value="0.6" /> </bean> <task:annotation-driven scheduler="myScheduler" /> <task:scheduler id="myScheduler" pool-size="10" /> </beans>
In Spring I simply defines a bean for the Memory warning system. I externalised the declaration in XML (e.g. didn't use the @Component annotation here) because I wanted to pass the threshold percentage as property (which could be retrieved externally). Alternatively, if you want to use the annotations, you could simply declare a bean of type Double, fill it with the property value and inject it in the OOME warning bean.
The rest of the Spring configuration sets up the Task infrastructure.
The Memory HEAP filler
I created a simple task which progressively fills up the HEAP by adding random doubles to a Collection<Double>. This solution uses the new Spring 3 task feature:
package uk.co.jemos.experiments.oome; import java.util.ArrayList; import java.util.Collection; import org.springframework.scheduling.annotation.Scheduled; import org.springframework.stereotype.Component; @Component public class MemoryFillerTaskExecutor { private static final Collection<Double> GARBAGE = new ArrayList<Double>(); @Scheduled(fixedDelay = 1000) public void addGarbage() { System.out.println("Adding data..."); for (int i = 0; i < 100000; i++) { GARBAGE.add(Math.random()); } } }
The Maven Setup
I used Maven to build this project and to create an executable. This is the pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>uk.co.jemos.experiments</groupId> <artifactId>memory-warning-system</artifactId> <version>0.0.1-SNAPSHOT</version> <name>memory-warning-system</name> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-jar-plugin</artifactId> <version>2.3.1</version> <configuration> <archive> <manifest> <addClasspath>true</addClasspath> <mainClass>uk.co.jemos.experiments.oome.MemTest</mainClass> </manifest> </archive> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>2.2.1</version> <executions> <execution> <id>executable</id> <phase>install</phase> <goals> <goal>assembly</goal> </goals> </execution> </executions> <configuration> <outputDirectory>${project.build.directory}/executable</outputDirectory> <descriptors> <descriptor>src/main/assembly/executable.xml</descriptor> </descriptors> </configuration> </plugin> </plugins> </build> <dependencies> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-beans</artifactId> <version>3.0.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>3.0.5.RELEASE</version> </dependency> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-core</artifactId> <version>3.0.5.RELEASE</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.8.2</version> <scope>test</scope> </dependency> <dependency> <groupId>log4j</groupId> <artifactId>log4j</artifactId> <version>1.2.16</version> <scope>test</scope> </dependency> </dependencies> </project>
When running mvn clean install Maven will create a memory-warning-system-0.0.1-SNAPSHOT-jar-with-dependencies.jar file under target/executable. To execute the jar, open a command prompt and point to target/executable then execute:
$ jar -xvf memory-warning-system-0.0.1-SNAPSHOT-jar-with-dependencies.jar $ java -Xmx64m -jar memory-warning-system-0.0.1-SNAPSHOT.jar
The results
This is what I see in my console:
C:\runtime\eclipse-ws\flex-java\memory-warning-system\target\executable>java -Xmx64m -jar memory-warning-system-0.0.1-SNAPSHOT.jar 17-Jul-2011 15:06:16 org.springframework.context.support.AbstractApplicationContext prepareRefresh INFO: Refreshing org.springframework.context.support.ClassPathXmlApplicationContext@1764be1: startup date [Sun Jul 17 15:06:16 BST 2011]; root of context hierarchy 17-Jul-2011 15:06:16 org.springframework.beans.factory.xml.XmlBeanDefinitionReader loadBeanDefinitions INFO: Loading XML bean definitions from class path resource [appCtx.xml] 17-Jul-2011 15:06:16 org.springframework.scheduling.concurrent.ExecutorConfigurationSupport initialize INFO: Initializing ExecutorService 17-Jul-2011 15:06:16 org.springframework.context.support.AbstractApplicationContext$BeanPostProcessorChecker postProcessAfterInitialization INFO: Bean 'myExecutor' of type [class org.springframework.scheduling.config.TaskExecutorFactoryBean] is not eligible for getting processed by all BeanPostProcessors (for exam ple: not eligible for auto-proxying) 17-Jul-2011 15:06:16 org.springframework.context.support.AbstractApplicationContext$BeanPostProcessorChecker postProcessAfterInitialization INFO: Bean 'myExecutor' of type [class org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor] is not eligible for getting processed by all BeanPostProcessors (for e xample: not eligible for auto-proxying) 17-Jul-2011 15:06:16 org.springframework.scheduling.concurrent.ExecutorConfigurationSupport initialize INFO: Initializing ExecutorService 'myScheduler' 17-Jul-2011 15:06:16 org.springframework.context.support.AbstractApplicationContext$BeanPostProcessorChecker postProcessAfterInitialization 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) 17-Jul-2011 15:06:16 org.springframework.beans.factory.support.DefaultListableBeanFactory preInstantiateSingletons INFO: Pre-instantiating singletons in org.springframework.beans.factory.support.DefaultListableBeanFactory@1884174: defining beans [memoryFillerTaskExecutor,memoryWarningListe nerImpl,org.springframework.context.annotation.internalConfigurationAnnotationProcessor,org.springframework.context.annotation.internalAutowiredAnnotationProcessor,org.springf ramework.context.annotation.internalRequiredAnnotationProcessor,org.springframework.context.annotation.internalCommonAnnotationProcessor,memoryWarningSystem,org.springframewor k.scheduling.annotation.internalAsyncAnnotationProcessor,org.springframework.scheduling.annotation.internalScheduledAnnotationProcessor,myExecutor,myScheduler]; root of factor y hierarchy Adding data... Adding data... Adding data... Adding data... Adding data... Adding data... Adding data... Adding data... Adding data... Adding data... Adding data... Adding data... Adding data... Adding data... Adding data... Adding data... Memory usage low!!! percentageUsed = 0.6409345630863504 Adding data... Adding data... Adding data... Adding data... Adding data... Adding data... Adding data... Adding data... Adding data... Adding data... Adding data... Adding data... Adding data... Adding data... Adding data... Adding data... Adding data...
And then the application crashes with OOME.
From http://tedone.typepad.com/blog/2011/07/outofmemoryerror-warning-system-with-spring.html
Opinions expressed by DZone contributors are their own.
Comments