Gemalto is now part of the Thales Group, find out more.

You are here

Optimising J2ME applications

Tutorial, January 18, 2016 - 11:59am, 3021 views

How to increase the performance of my Java application

There are two main ways to increase the performance of your application. One thing is to take care of the memory and avoid needless memory allocation. The other thing is the execution time of your code. For example creating and destroying objects costs processor time and every object needs memory. Both are rare in module devices.

Following tips help to prevent bad performance, but avoid writing hard-to-read and hard-to-maintain code. The approach should be first write, then test, then optimize, then re-test. Just keep clean, maintainable, working code.

Memory usage

If you don't take care of memory usage and run out of it, an OutOfMemoryException can be raised, while allocating and freeing memory costs time. So creating and using only well chosen objects keeps you from losing performance due to avoidable memory management operations.

Benchmarking memory usage

Sometimes it can be necessary to check which objects needs how much memory. The Runtime methods freeMemory() and totalMemory() help to measure it. The first method returns the amount of bytes currently available for new objects while the second method returns the total size of the Java runtime environment. The following example gives the amount of bytes allocated by instantiating MyObject (the explicit call of the garbage collector causes that all unreferenced memory is released):

Runtime rt = Runtime.getRuntime();
long before=0, after=0, size=0;
System.gc();
before = rt.freeMemory();
MyObject obj = new MyObject();
after = rt.freeMemory();
size = before - after;

Note: In case MyObject is instantiated for the first time during the MIDlet lifecycle, the VM's class loader needs to load the MyObject class code prior to creating the instance, hence the first measurement can be inaccurate. To address this, consider to either add

new MyObject();

above the gc() call or take several samples and average, ideally ignoring the first.

Creating objects

It is important not to create unnecessary objects. Every object needs memory and its allocation takes time. Furthermore, the more objects are created, the more frequent the garbage collector is started. It runs in a low priority thread and costs performance. Please create only objects you really need.

Avoid creating objects in a loop like this:

MyObject [] input = MyObject.getArray();
for (int i=0; i<input.length; i++) {
   Processor p = new Processor(input[i]);
   System.out.println(p.process());
}

Here, a new Processor instance is created in every loop. At the end of the loop the object gets out of scope, i.e. at the end of each iteration the memory is filled up with an object that isn't referenced anymore. This could cause the garbage collector to run in every iteration, provided the memory is nearly full. Consider restructuring your code along these lines:

MyObject [] input = MyObject.getArray();
Processor p = new Processor();
for (int i=0; i<input.length; i++) {
   p.setInput(input[i]);
   System.out.println(p.process());
}

freeing memory

Especially on systems with limited resources, it is crucial to release memory as soon as it is not needed anymore. Keeping your application clean increases the performance and prevents OutOfMemoryExceptions. You can release memory by setting all references to the object to null. This allows the now unreferenced instance to be garbage collected.

Pay special attention when working with streams (e.g.: network connections). Hanging connections you assume are closed can cause big problems. They are not only wasting memory but may also reserve peripheral resources like (server-)sockets.

try {
   // open socket, input and outputstream
   // do some stuff
   outstream.write(bytearray);
   instream.read(answerarray);
   // do some other stuff
   outstream.close();
   instream.close();
   socket.close();
} catch(IOException ioex) {
   //handle exception
}

In above example, read() could throw an Exception, causing the catch block to be executed but the streams and the socket are still open. Imagine the wasted resources if you have a server application that deals with multiple connections. Another result is that the connection should be closed but it is still open and so the remote side still believes that the connection is active. The expression in the finally clause of the try-catch-finally statement is executed regardless of any Exceptions being thrown or not.

try {
   // open socket, input and outputstream
   // do some stuff
   outstream.write(bytearray);
   instream.read(answerarray);
   // do some other stuff
} catch(IOException ioex) {
   //handle exception
} finally {
   if (outstream != null) {
      try {
         outstream.close();
      } catch(IOException ioex) {}
      outstream = null;
   }
   if (instream != null) {
      try {
         instream.close();
      } catch(IOException ioex) {}
      instream = null;
   }
   if (socket != null) {
      try {
         socket.close();
      } catch(IOException ioex) {}
      socket = null;
   }
}


The finally block is executed in any case. The try and catch statements guarantee that all close() methods will be called. It ensures that all connections/streams are null and the garbage collector can free the resources. The code appears a bit lengthy, but it avoids memory leaks.

String and StringBuffer

In terms of performance, Strings become a special meaning. String is the only class for which the "+" operator is overloaded. It is convenient to concatenate two Strings with it, but behind the scenes a new object for the resulting string is created each time because Strings are constant data types, they cannot be changed after creation. Each use of "+" involving a String object creates a new object. Sun's Java specification does not cover when the runtime environment creates new objects and how many of them.

If you are using String operations heavily, the class StringBuffer should be considered because its contents can be changed. The class provides many manipulation methods that don't involve creation of new objects (e.g. StringBuffer.append()).

Remember String and StringBuffer both work on character arrays in the background. So if you want full control, consider using character or byte arrays. Implementing the manipulation methods yourself can be quite some extra work, but you can keep absolute control on memory usage, potentially the efficiency of your application is increased. Remember that many APIs require or return Strings, hence frequent conversions to/from Strings may be necessary. In the end, it is a matter of trade-off which depends on your application. The example gives a good overview.

Vector and Hashtable

These classes manage sets of objects, as opposed to arrays, the set's size can vary during runtime. However, dynamical resizing is a costly operation in terms of resources: New objects are created and a lot of copying needs to be done. Instead of the default constructor, use the one with the int signature passing a typical upper bound for the size as argument. This will avoid unnecessary resizing/rehashing saving you performance.

Make sure you really need to use these classes, sometimes a simple, fixed-size array may still be the better option.

OutOfMemoryException

When trying to create a new object while your memory is running full, an OutOfMemoryException will be thrown. Instead of having your application crash, is better to catch this exception thrown by the new operator. In the catch expression, you could try to free some memory, send off/log an error message and orderly shut down your application.

Saving Java Memory

To keep the size of your Java MIDlet small, following measures should be taken:

  • set no-longer-used variables referencing big objects to null and
  • run System.gc() at regular intervals, especially after 'nulling' objects. Be careful: gc() might slow down code execution, best to be run when no timing-critical code is running
  • try to re-use objects: avoid frequent instanciation of objects ("new"), esp. in loops or recursive functions
  • use static or static final variables where possible
  • keep in mind that class code and variables are loaded on demand and will not be gc()ed (for TC45 and TC65)
  • try to reduce number/size of constants by shorten (debug) strings or try to compile them out:
public static final boolean DEBUG_MODULE_ASC = false;
// the next line's code/string will be removed by the
// compiler if static final boolean is false
if (DEBUG_MODULE_ASC)
    System.out.println("temporarily irrelevant debug data regarding ASC");
  • try to reduce the count of objects (potentially stored in arrays or vectors) and use one class working on data directly
// instead implementing
arrayObject[num].do(<args>)
// use
singletonObject.do(num, <args>)
  • Using an obfuscator, e.g. Proguard, one can save up to 50% RAM.
-keep public class com.wmae.MainMIDlet 
-printmapping C:/out/obfus_map.txt 
-dontusemixedcaseclassnames

Recommendations:

  • use the newest Proguard version (V4.1 vs. V3.8 shows 3-4% better shrinking)
  • study the Proguard homepage for new optimation features (options)
  • additional arguments can be set in Eclipse, using this option shrink my example code from ~7000Byte to ~6800Byte. As larger projects have more "shrinking potential", it is highly recommended to use this option.

Proguard eclipse.jpg

Used values: -dontusemixedcaseclassnames -dontnote -overloadaggressively 
             -allowaccessmodification -repackageclasses '' -optimizationpasses 5 -printusage 'dead.log' -microedition

More speed

Use static and/or final fields and methods where possible, the compiler often can optimize references.

Benchmarking speed

To profile execution times use the System method currentTimeMillis(). The method returns the current system time in milliseconds, however for profiling, only the difference of two time stamps is important. Example (for more accuracy, take more samples and average):

long start=0, stop=0, duration=0;
start = System.currentTimeMillis();
methodToTest(); // or code sequence of interest
stop = System.currentTimeMillis();
duration = stop - start; // in milliseconds

loop conditions

In loops, the stop criterion needs to be checked for each iteration. If the criterion includes method calls such as in

i < someVector.size()

it can impact execution time. If possible, assign the method's return value to a local variable and use that in the condition. If you do only a few cycles the difference may be marginal. In other applications, for example complex mathematic algorithms with a huge amount of iterations, it does safe time.

Full example:

for (int i=0; i < objVector.size(); i++) {
   Object o = objVector.elementAt(i);
   // do something (not changing objVector)
}
                    

Instead, use this pattern:

Object o;
int tmp = objVector.size();
for (int i=0; i<tmp; i++) {
   o = objVector.elementAt(i);
   // do something
}

I/O

When working with streams, it is not recommended to read or write data byte-by-byte. Reading from/writing to arrays is more efficient than dealing with single bytes. The I/O streams provide methods to read or write from or to a buffer. The problem is that it is possible not all expected data was processed. If you need to make sure to read or write a certain amount of bytes you can implement an own kind of a buffered reader or writer.

Instead of doing something like this:

byte [] buffer = new byte[size];
int nrBytes = 0, tmp = 0;
do {
   tmp = inStream.read();
   if (tmp == -1) {
      break;
   } else {
      buffer[nrBytes++] = tmp;
   }
} while (nrBytes < size);

consider do this:

byte [] buffer = new byte[size];
int nrBytes = 0, tmp = 0;
do {
   tmp = inStream.read(buffer,nrBytes,(size-nrBytes));
   if(tmp!=-1)nrBytes += tmp;
} while((nrBytes < size) || (tmp != -1));

This kind of implementation is good when there is a certain amount of bytes to read. read() returns when some data was read. The number of read bytes will be returned, you can be sure to read all bytes until the EOF was reached or an exception was thrown. It still looks like introducing some overhead but nevertheless this implementation reading blocks is significantly faster.

Also consider using

Stream.available()

returning the amount of data available for immediate read()ing.

Another possibility is to use readFully() of DataInputStream. Contrary to read(), readFully() blocks until the complete buffer was filled with data. It is ensured that the method only returns when buffer length data was received. If an EOF occurs in the stream, an exception is thrown and the last received data may be lost. When the amount of data is known, it can be used for an error detection mechanism. The speed difference between readFully(byte []) and read(byte[]) is marginal but working on InputStreams is often faster but less comfortable than working on DataInputStreams.

byte [] buffer = new byte[blockSize];
try {
   do {
      dataInputStream.readFully(buffer);
      // proccess the read data in the buffer
   } while(true);
} catch(IOException e) {
   // IO error
} catch(EOFException eof)
   // End of file reached and stream was closed, buffer is not fully written
   // expected amount of bytes was not read
}

Partition your application

The runtime environment doesn't load all classes at the beginning of the application. Classes are loaded as they are needed. If you keep this in mind then you can encapsulate your application in a way that different modules don't affect each other. Let's say your application has two main features. If the structure of your application is well encapsulated then the runtime environment has to load only the classes needed for the current feature and your application runs faster.

Obfuscator

Use an obfuscator to keep your application slim and fast. It reduces the size of your class files and takes care that only needed classes are included in the jar package. A commonly used obfuscator is proguard (see also: http://proguard.sourceforge.net/). If you are using eclipse and ant you can add a target "obfuscating" to your build.xml file (remember to set the path to the obfuscator).

Example

The examples demonstrate the differences in the performance when following above guidelines or not.

Bytes are read from an InputStream and compared to a set of Strings, any matches are displayed. This is frequently used in e.g. compression algorithms.

The application reads 2000 strings, each 5 bytes long, from a file. For every string it checks if it is in a set of 4000 strings (all between 2 and 10 bytes long). The performance optimized example is about 15 times faster than the first one.

Bad Performance
Here is a bad example: Strings are stored in a Vector, single bytes are read from the stream, a new object (String str)) is created in every loop and the comparison is done via String.equals()

start=System.currentTimeMillis();
   for (int i = 0; i < strings; i++) {
      for (int a = 0; a < blockSize; a++) {
         tmp = is.read();
            if(tmp==-1) {
               i=3000;
               break;
            }
            buf[a] = (byte)tmp;
         }
         String str=new String(buf);
         for (int a = 0; a < vStorage.size(); a++) {
         if(((String)vStorage.elementAt(a)).equals(str)) {
            strmatches++;
            break;
         }
      }
   }
   dur1 = System.currentTimeMillis() - start;
   System.out.println("duration using strings: " + dur1);

Good Performance
increased performance because storage and string operations are done with bytearrays, the comparison of the strings is an own implementation and buf size bytes are read from the InputStream.

start=System.currentTimeMillis();
   for (int i = 0; i < strings; i++) {
      tmp = is.read(buf);
      if (tmp == -1) break;
   }
   for (int a = 0; a < size; a++) {
      if (bStorage[a].length == blockSize) {
         for (j = 0; j < blockSize; j++) {
            if (bStorage[a][j] != buf[j]) break;
         }
         if (j == blockSize) {
            bmatches++;
            break;
         }
      }
   }
}
dur2 = System.currentTimeMillis() - start;
System.out.println("duration using byte arrays: " + dur2);


Regards

ALopez
Alopez

Contributors

Profile Picture
Michał - Gemalt...
Profile Picture
mullengers