Optimize the Memory Footprint of an Application
Description
This training explains how to analyze the memory footprint of an application and provides a set of common rules aimed at optimizing both ROM and RAM footprint.
Intended Audience
This training is designed for Java engineers and Firmware integrators who want to execute a MicroEJ Application on a memory-constrained device.
Prerequisites
To get the most out of this training, participants should have a good knowledge of Java programming language.
Introduction
Usually, the application development is already started when the developer starts thinking about its memory footprint. Before jumping into code optimizations, it is recommended to list every area of improvement and estimate for each area how much memory can be saved and how much effort it requires.
Without performing the memory analysis first, the developer might start working on a minor optimization that takes a lot of effort for little improvements. In contrast, he could work on a major optimization, allowing faster and bigger improvements. Moreover, each optimization described hereafter may allow significant memory savings for an application while it may not be relevant for another application.
How to Analyze the Footprint of an Application
This section explains the process of analyzing the footprint of a MicroEJ Application and the tools used during the analysis.
Suggested footprint analysis process:
Build the MicroEJ Application
Analyze
SOAR.map
with the Memory Map AnalyzerAnalyze
soar/*.xml
with an XML editorLink the MicroEJ Application with the BSP
Analyze the map file generated by the third-party linker with a text editor
Footprint analysis tools:
The Memory Map Analyzer allows to analyze the memory consumption of different features in the RAM and ROM.
The Heap Dumper & Heap Analyzer allow to understand the contents of the Java heap and find problems such as memory leaks.
The API Dependency Discoverer allows to analyze a piece of code to detect all its dependencies.
How to Analyze the Files Generated by the MicroEJ Linker
The MicroEJ Application linker generates files useful for footprint analysis, such as the SOAR map file and the SOAR information file. To understand how to read these files, please refer to the SOAR Output Files documentation.
How to Analyze a Map File Generated by a Third-Party Linker
A <firmware>.map
file is generated by the C toolchain after linking the MicroEJ Application with the BSP.
This section explains how a map file generated by GCC is structured and how to browse it. The structure is not the same on every compiler, but it is often similar.
File Structure
This file is composed of 5 parts:
Archive member included to satisfy reference by file
. Each entry contains two lines. The first line contains the referenced archive file location and the compilation unit. The second line contains the compilation unit referencing the archive and the symbol called.Allocating common symbols
. Each entry contains the name of a global variable, its size, and the compilation unit where it is defined.Discarded input sections
. Each entry contains the name and the size of a section that has not been embedded in the firmware.Memory Configuration
. Each entry contains the name of a memory, its address, its size, and its attributes.Linker script and memory map
. Each entry contains a line with the name and compilation unit of a section and one line per symbol defined in this section. Each of these lines contains the name, the address, and the size of the symbol.
Finding the Size of a Section or Symbol
For example, to know the thread stacks’ size, search for the .bss.vm.stacks.java
section in the Linker script and memory map
part. The size associated with the compilation unit is the size used by the thread stacks.
The following snippet shows that the .bss.vm.stacks.java
section takes 0x800 bytes.
.bss.vm.stacks.java
0x20014070 0x800 ..\..\..\..\..\..\..\.microej\CM4hardfp_GCC48\application\microejapp.o
0x20014070 __icetea___6bss_6vm_6stacks_6java$$Base
0x20014870 __icetea___6bss_6vm_6stacks_6java$$Limit
See Core Engine Link documentation for more information on MicroEJ Core Engine sections.
How to Reduce the Image Size of an Application
Generic coding rules can be found in the following training: Best Java Code Practices.
This section provides additional coding rules and good practices to reduce the image size (ROM) of an application.
Application Resources
Resources such as images and fonts take a lot of memory.
For every .list
file, make sure that it does not embed any unused resource.
Only resources declared in a .list
file will be embedded.
Other resources available in the application classpath will not be embedded and will not have an impact on the application footprint.
Fonts
Default Font
By default, in a MicroEJ Platform configuration project, a so-called system font is declared in the microui.xml
file.
When generating the MicroEJ Platform, this file is copied from the configuration project to the actual MicroEJ Platform project. It will later be converted to binary format and linked with your MicroEJ Application, even if you use fonts different from the system font.
Therefore, you can comment the system font from the microui.xml
file to reduce the ROM footprint of your MicroEJ Application if this one does not rely on the system font. Note that you will need to rebuild the MicroEJ Platform and then the application to benefit from the footprint reduction.
See the Display Element section of the Static Initialization documentation for more information on system fonts.
Character Ranges
When creating a font, you can reduce the list of characters embedded in the font at several development stages:
On font creation: see the Removing Unused Characters section of Font Designer documentation.
On application build: see the Fonts section of MicroEJ Classpath documentation.
Pixel Transparency
You can also make sure that the BPP encoding used to achieve transparency for your fonts do not exceed the following values:
The pixel depth of your display device.
The required alpha level for a good rendering of your font in the application.
See the Fonts section of MicroEJ Classpath documentation for more information on how to achieve that.
External Storage
To save storage on internal flash, you can access fonts from an external storage device.
See the External Resources section of the Font Generator documentation for more information on how to achieve that.
Internationalization Data
Implementation
MicroEJ provides the Native Language Support (NLS) library to handle internationalization.
See https://github.com/MicroEJ/Example-NLS for an example of the use of the NLS library.
External Storage
The default NLS implementation fetches text resources from internal flash, but you can replace it with your own implementation to fetch them from another location.
See External Resources Loader documentation for additional information on external resources management.
Images
Encoding
If you are tight on ROM but have enough RAM and CPU power to decode PNG images on the fly, consider storing your images as PNG resources. If you are in the opposite configuration (lots of ROM, but little RAM and CPU power), consider storing your images in raw format.
See Image Generator documentation for more information on how to achieve that.
Color Depth (BPP)
Make sure to use images with a color depth not exceeding the one of your display to avoid the following issues:
Waste of memory.
Differences between the rendering on the target device and the original image resource.
External Storage
To save storage on internal flash, the application can access the images from an external storage device.
See External Resources Loader documentation for more information on how to achieve that.
Application Code
The following application code guidelines are recommended in order to minimize the size of the application:
Check libraries versions and changelogs regularly. Latest versions may be more optimized.
Avoid manipulating String objects:
For example, prefer using integers to represent IDs.
Avoid overriding Object.toString() for debugging purposes. This method will always be embedded even if it is not called explicitly.
Avoid using the logging library or
System.out.println()
, use the trace library or the message library instead. The logging library uses strings, while the trace and message libraries use integer-based error codes.Avoid using the string concatenation operator (
+
), use an explicit StringBuilder instead. The code generated by the+
operator is not optimal and is bigger than when using manualStringBuilder
operations.
Avoid manipulating wrappers such as Integer and Long objects, use primitive types instead. Such objects have to be allocated in Java heap memory and require additional code for boxing and unboxing.
Avoid declaring Java Enumerations (
enum
), declare compile-time constants of primitives types instead (e.g.static final int I = 0;
). The Java compiler creates an Enum object in the Java heap for each enumeration item, as well as complex class initialization code.Avoid using the service library, use singletons or Constants.getClass() instead. The service library requires embedding class reflection methods and the type names of both interfaces and implementations.
Avoid using the Java Collections Framework. This OpenJDK standard library has not been designed for memory constrained devices.
Use raw arrays instead of List objects. The ArrayTools class provides utility methods for common array operations.
Use PackedMap objects instead of Map objects. It provides similar APIs and features with lower Java heap usage.
Use ej.bon.Timer instead of deprecated
java.util.Timer
. When both class are used, almost all the code is embedded twice.Use BON constants in the following cases if possible:
when writing debug code or optional code, use the
if (Constants.getBoolean()) { ... }
pattern. That way, the optional code will not be embedded in the production firmware if the constant is set tofalse
.replace the use of System Properties by BON constants when both keys and values are known at compile-time. System Properties should be reserved for runtime lookup. Each property requires embedding its key and its value as intern strings.
Check for useless or duplicate synchronization operations in call stacks, in order reduce the usage of
synchronized
statements. Each statement generates additional code to acquire and release the monitor.Avoid declaring exit statements (
break
,continue
,throw
orreturn
) that jump out of asynchronized
block. At each exit point, additional code is generated to release the monitor properly.Avoid declaring exit statements (
break
,continue
,throw
orreturn
) that jump out of atry/finally
block. At each exit point, the code of thefinally
block is generated (duplicated). This also applies on everytry-with-resources
block since afinally
block is generated to close the resource properly.Avoid overriding Object.equals(Object) and Object.hashCode(), use
==
operator instead if it is sufficient. The correct implementation of these methods requires significant code.Avoid calling
equals()
andhashCode()
methods directly onObject
references. Otherwise, the method of every embedded class which overrides the method will be embedded.Avoid creating inlined anonymous objects (such as
new Runnable() { ... }
objects), implement the interface in a existing class instead. Indeed, a new class is created for each inlined object. Moreover, each enclosed final variable is added as a field of this anonymous class.Avoid accessing a private field of a nested class. The Java compiler will generate a dedicated method instead of a direct field access. This method is called synthetic, and is identified by its name prefix:
access$
.Replace constant arrays and objects initialization in
static final
fields by immutables objects. Indeed, initializing objects dynamically generates code which takes significant ROM and requires execution time.Check if some features available in software libraries are not already provided by the device hardware. For example, avoid using java.util.Calendar (full Gregorian calendar implementation) if the application only requires basic date manipulation provided by the internal real-time clock (RTC).
MicroEJ Platform Configuration
The following configuration guidelines are recommended in order to minimize the size of the application:
Check MicroEJ Architecture and Packs versions and changelogs regularly. Latest versions may be more optimized.
Configure the Platform to use the tiny capability of the MicroEJ Core Engine. It reduces application code size by ~20%, provided that the application code size is lower than 256KB (resources excluded).
Disable unnecessary modules in the
.platform
file. For example, disable theImage PNG Decoder
module if the application does not load PNG images at runtime.Don’t embed unnecessary pixel conversion algorithms. This can save up to ~8KB of code size but it requires knowing the format of the resources used in the application.
Select your embedded C compilation toolchain with care, prefer one which will allow low ROM footprint with optimal performance. Check the compiler options:
Check documentation for available optimization options (
-Os
on GCC). These options can also be overridden per source file.Separate each function and data resource in a dedicated section (
-ffunction-sections -fdata-sections
on GCC).
Check the linker optimization options. The linker command line can be found in the project settings, and it may be printed during link.
Only embed necessary sections (
--gc-sections
option on GCC/LD).Some functions, such as the
printf
function, can be configured to only implement a subset of the public API (for example, remove-u _printf_float
option on GCC/LD to disable printing floating point values).
In the map file generated by the third-party linker, check that every embedded function is necessary. For example, hardware timers or HAL components may be initialized in the BSP but not used in the application. Also, debug functions such as SystemView may be disconnected when building the production firmware.
Application Configuration
The following application configuration guidelines are recommended in order to minimize the size of the application:
Disable class names generation by setting the
soar.generate.classnames
option tofalse
. Class names are only required when using Java reflection. In such case, the name of a specific class will be embedded only if is explicitly required. See Stripping Class Names from an Application section for more information.Remove UTF-8 encoding support by setting the
cldc.encoding.utf8.included
option tofalse
. The default encoding (ISO-8859-1
) is enough for most applications.Remove
SecurityManager
checks by setting thecom.microej.library.edc.securitymanager.enabled
option tofalse
. This feature is only useful for Multi-Sandbox firmwares.
For more information on how to set an option, please refer to the Defining an Option with SDK 5 or lower section.
Stripping Class Names from an Application
By default, when a Java class is used, its name is embedded too. A class is used when one of its methods is called, for example. Embedding the name of every class is convenient when starting a new MicroEJ Application, but it is rarely necessary and takes a lot of ROM. This section explains how to embed only the required class names of an application.
Removing All Class Names
First, the default behavior is inverted by defining the Application option soar.generate.classnames
to false
.
For more information on how to set an option, please refer to the Defining an Option with SDK 5 or lower section.
Listing Required Class Names
Some class names may be required by an application to work properly.
These class names must be explicitly specified in a *.types.list
file.
The code of the application must be checked for all uses of the Class.forName(), Class.getName() and Class.getSimpleName() methods.
For each of these method calls, if the class name if absolutely required and can not be known at compile-time, add it to a *.types.list
file. Otherwise, remove the use of the class name.
The following sections illustrates this on concrete use cases.
Case of Service Library
The ej.service.ServiceLoader class of the service library is a dependency injection facility. It can be used to dynamically retrieve the implementation of a service.
The assignment between a service API and its implementation is done in *.properties.list
files. Both the service class name and the implementation class name must be embedded (i.e., added in a *.types.list
file).
For example:
# example.properties.list
com.example.MyService=com.example.MyServiceImpl
# example.types.list
com.example.MyService
com.example.MyServiceImpl
Case of Properties Loading
Some properties may be loaded by using the name of a class to determine the full name of the property. For example:
Integer.getInteger(MyClass.class.getName() + ".myproperty");
In this case, it can be replaced with the actual string. For example:
Integer.getInteger("com.example.MyClass.myproperty");
Case of Logger and Other Debugging Facilities
Logging mechanisms usually display the name of the classes in traces. It is not necessary to embed these class names. The Stack Trace Reader can decipher the output.
How to Reduce the Runtime Size of an Application
You can find generic coding rules in the following training: Best Java Code Practices.
This section provides additional coding rules and good practices in order to reduce the runtime size (RAM) of an application.
Application Code
The following application code guidelines are recommended in order to minimize the size of the application:
Avoid using the default constructor of collection objects, use constructors that allow to set the initial capacity. For example, use the ArrayList(int initialCapacity) constructor instead of the default one which will allocate space for ten elements.
Adjust the type of
int
fields (32 bits) according to the expected range of values being stored (byte
for 8 bits signed integers,short
for 16 bits signed integers,char
for 16 bits unsigned integers).When designing a generic and reusable component, allow the user to configure the size of any buffer allocated internally (either at runtime using a constructor parameter, or globally using a BON constant). That way, the user can select the optimal buffer size depending on his use-case and avoid wasting memory.
Avoid allocating immortal arrays to call native methods, use regular arrays instead. Immortal arrays are never reclaimed and they are not necessary anymore when calling a native method.
Reduce the maximum number of parallel threads. Each thread require a dedicated internal structure and VM stack blocks.
Avoid creating threads on the fly for asynchronous execution, use shared thread instances instead (ej.bon.Timer, Executor, MicroUI.callSerially(Runnable), …).
When designing Graphical User Interface:
Avoid creating mutable images (BufferedImage instances) to draw in them and render them later, render graphics directly on the display instead. Mutable images require allocating a lot of memory from the images heap.
Make sure that your Widget hierarchy is as flat as possible (avoid any unnecessary Container). Deep widget hierarchies take more memory and can reduce performance.
MicroEJ Platform Configuration
The following configuration guidelines are recommended in order to minimize the runtime size of the application:
Check the size of the stack of each RTOS task. For example, 1.0KB may be enough for the MicroJVM task but it can be increased to allow deep native calls. See Debugging Stack Overflows section for more information.
Check the size of the heap allocated by the RTOS (for example,
configTOTAL_HEAP_SIZE
for FreeRTOS).Check that the size of the back buffer matches the size of the display. Use a partial buffer if the back buffer does not fit in the RAM.
Debugging Stack Overflows
If the size you allocate for a given RTOS task is too small, a stack overflow will occur. To be aware of stack overflows, proceed with the following steps when using FreeRTOS:
Enable the stack overflow check in
FreeRTOS.h
:
#define configCHECK_FOR_STACK_OVERFLOW 1
Define the hook function in any file of your project (
main.c
for example):
void vApplicationStackOverflowHook(TaskHandle_t xTask, signed char *pcTaskName) { }
Add a new breakpoint inside this function
When a stack overflow occurs, the execution will stop at this breakpoint
For further information, please refer to the FreeRTOS documentation.
Application Configuration
The following application configuration guidelines are recommended in order to minimize the size of the application.
For more information on how to set an option, please refer to the Defining an Option with SDK 5 or lower documentation.
Java Heap and Immortals Heap
Configure the immortals heap option to be as small as possible. You can get the minimum value by calling Immortals.freeMemory() after the creation of all the immortal objects.
Configure the Java heap option to fit the needs of the application. You can get it by using the Heap Usage Monitoring Tool.
Thread Stacks
Configure the maximum number of threads option. This number can be known accurately by counting in the code how many
Thread
andTimer
objects may run concurrently. You can call Thread.getAllStackTraces() or Thread.activeCount() to know what threads are running at a given moment.Configure the number of allocated thread stack blocks option. This can be done empirically by starting with a low number of blocks and increasing this number as long as the application throws a
StackOverflowError
.Configure the maximum number of blocks per thread option. The best choice is to set it to the number of blocks required by the most greedy thread. Another acceptable option is to set it to the same value as the total number of allocated blocks.
Configure the maximum number of monitors per thread option. This number can be known accurately by counting the number of concurrent
synchronized
blocks. This can also be done empirically by starting with a low number of monitors and increasing this number as long as no exception occurs. Either way, it is recommended to set a slightly higher value than calculated.
VM Dump
The LLMJVM_dump()
function declared in LLMJVM.h
may be called to print information on alive threads such as their current and maximum stack block usage.
This function may be called from the application by exposing it in a native function. See Dump the States of the Core Engine section for usage.
More specifically, the Peak java threads count
value printed in the dump can be used to configure the maximum number of threads.
The max_java_stack
and current_java_stack
values printed for each thread can be used to configure the number of stack blocks.
MicroUI Images Heap
Configure the images heap to be as small as possible. You can compute the optimal size empirically. It can also be calculated accurately by adding the size of every image that may be stored in the images heap at a given moment. One way of doing this is to inspect every occurrence of BufferedImage() allocations and ResourceImage usage of
loadImage()
methods.