Java on a single-board computer
What you need to consider when using Java on ARM architecture.
In this article, I am going to tell you about some problems you’ll face or things you need to consider when using Java on ARM architecture. The story is based on my professional experience gained at Silvair.
About 3 years ago, my team faced the challenge of rewriting one of our desktop applications written in C#. The reason for this was very poor quality code, where making even the smallest change was a very complicated task. Additionally, there were no unit tests or no static code analysis . Maintaining this code was a nightmare. We decided to rewrite the application from scratch.
After many discussions, we chose Java as the programming language and a single-board computer like RPi as the target development platform.
Because we knew it best, and we needed the application to be ready as a POC as quickly as possible. Also, we didn’t find any reason why we couldn’t try Java. Of course, everyone will think that C/C++ will fit better here, but the development process using that language would be much, much slower, especially as the team was not very familiar with it. Also, we needed some UI, and it would be much easier to implement it in Java (some REST backend + simple Angular frontend). Besides, I like the approach where you create the smallest possible working system as soon as possible and see how it works and performs. Then you can take it further from there or even make a 180 degree turn. Simply try to make all of your mistakes at the beginning of the development rather than at the end - it is so easy to say…
The Wandboard is a Cortex-A9 ARM device sporting a single, dual, or quad core Freescale i.MX6 SoC
Why a single-board computer? Creating a desktop application is difficult because we have to handle many operating systems and support end users during the process of application installation, configuration and installation of supported drivers. It is much simpler to deliver your solution deployed on a single-board computer like Raspberry Pi. After some time we changed the target platform to an SBC based on the iMX6 processor for security reasons, but this is a whole different story.
We also decided to use Docker as the distribution system for our application and Spring Boot as the base framework.
Let’s see what happened later…
JVM startup time
The first time you use a single-board computer for development, the transfer can be painful, primarily because of its performance. Every developer is used to the fact that running an application locally is a very fast process. Now think of an application that takes about 1 minute and a half to start on an RPi machine. Yes, this is nothing extraordinary, especially when you use a framework such as Spring Boot, which uses a very expensive reflection mechanism. If the application startup time is very important, then my advice is to not use Spring Boot on an ARM target platform. Of course, you can try doing some optimization like excluding dependencies from the auto-configuration, but this will not give you fast startup time. For our purposes, startup time of just above 1 minute was good enough to stop worrying about it and digging into it more.
Our solution also included command line scripts, which were supposed to provide an easy and fast way of debugging current problems. They were written, of course, without Spring Boot integration and the startup time was around 2-3 seconds. Is it fast enough? It depends on how often you will use such a tool. Two times a day is ok, but one hundred times a day would be frustrating. If this is your case, you can always take a shot at tools like NailGun or think about your own workaround.
This is not related to Java in any way, but it is worth a mention. Is it a good idea to use Docker on a single-board computer? Yes, but you need to take care of a couple of things that have a big impact on usability and performance.
Make sure you have the latest Docker version. On RPi, we used HypriotOS image that has Docker included and is updated very frequently. Docker is constantly evolving and getting more and more efficient and stable from version to version. We felt it on our own skin when we built our own image for hardware with iMX6 processor using an older version of Yocto and, as a result, we received an image with some older version of Docker. Lots of performance and stability issues - you do not want to be there.
Double check that your final Docker image is as small as it could be. I know that internet connections are very fast today, but this may not necessarily be true in some factory located in China where your product was sent to. Did you install only the things you use or your application needs? Does your Dockerfile contain the least frequently changing items at the top and the most frequently changing items at the bottom? If not, make sure you understand layers and multistage builds.
Random number generation
After migration from HypriotOS to our own prepared image we found out that the Sping Boot initialization takes much more time (about 5 minutes in total!). We used the Tomcat server, which creates a SecureRandom instance for random number generation at the beginning. By default, this object is created by using
/dev/random on UNIX systems. Unfortunately,
/dev/random blocks when it runs out of entropy and this happens every time the OS boots. To fix this you need to use
/dev/urandom (which is less secure) by using
-Djava.security.egd=file:/dev/./urandom or by replacing Tomcat with a different web server.
We must bear in mind that performance problems on single-board computers are much more visible than on a typical application server. So if your code is not optimal, you will find it out sooner rather than later.
Our system used some external library that did a very simple thing - joined two files using some rules and logic. It used the
System.arraycopy method, which is a native method so it should be efficient, right? But what if you use it to copy a huge number of small arrays? That was our problem. It is much better to allocate one array and expand it only when empty space has been consumed. After switching to
StringBuilder.append, we reduced execution time from 3 seconds to a couple of milliseconds.
Another source of possible performance problems is using reflection too intensively. Every Java developer knows that the reflection mechanism inside JDK is not very efficient, so use it wisely or not at all. If you really need it, you can always implement some simple cache for it as we did, but this of course depends on the code you are working on.
SD card issues
The speed of the underlying file system has a huge impact on the application performance itself, so we need to know how to use the full potential of the single-board computer. To do that we must be sure to use the fastest SD card available. My experience shows that this is very important in the context of system and application startup performance.
Generally, when using an SD card you need to be aware that the card is much more vulnerable to failure, despite the guaranteed 10k or more writes we can read about. But when the SBC loses power during the writing process, it can corrupt much more easily. We observed this in our development every couple of weeks or even at the customer’s site. You can reduce this effect to a minimum in several ways:
- do not write anything on your SD card at all or keep it down to the absolute minimum,
- move items that require frequent writing to a partition stored in RAM,
- switch the system to read-only mode.
Serial port communication
When you start playing with the IoT world, sooner or later you will be forced to use communication through the serial port. There are a couple of Java libraries that can handle it and I found that none of them is perfect but I used two of them.
I used RXTX 2.2pre2 at first and it worked fine for some time. This library is (or was) the most popular, but now it is very old and it is no longer maintained. There is a fork nrjavaserial, but I didn’t use it, so I can’t tell anything about it. A problem with RXTX arose when our system had to support USB hot-plugging. Then, from time to time, the USB device started to hang and stopped responding, and the only thing that helped was system reboot.
Then I quickly tested the JSerialComm library and everything looked very promising. We still had very good performance and the problems with hot-plugging disappeared. We used version 1.3.11 at the time and, as I can see, version 2.4.0 from the end of 2018 is now available, so the library is still under heavy development, which is very good. I can recommend it from the bottom of my heart.
Garbage collector / memory problems
Our system was not very big at the beginning, so we did not even think about memory tuning. We used default settings, which can be checked with the following command:
java -XX:+PrintFlagsFinal -version | grep -iE 'MaxHeapSize'. After some time, we implemented long-term tests in our testing environment and started to observe some strange freezing (pause for ~1-2 seconds) with the application logic. Our system grew so much that suddenly these 216 Mbps were no longer enough and after a couple of hours of uptime the garbage collector worked so intensively that the application could freeze for as long as 2 seconds. Setting more memory with the
-Xmx switch solved this issue. But remember that without our testing environment this bug could be hidden for a longer time, so the investment paid off.
As you can see from the above examples, the problems that my team faced when working in a single-board computer environment are not major issues. I would say these are typical problems we can encounter every day, but have a little more intensity. That’s why you shouldn’t be afraid of Java on SBC platforms. First of all, you have to be much more concerned about efficiency, because the resources will not suddenly increase as it happens in the cloud these days. If you need very fast and demanding processing, I would try to use the Javolution library, which was created especially for this purpose. It sounds very promising.
Oracle Java SE 8 Embedded is the final release series of the “Oracle Java SE Embedded” product. So if you want to switch to at least Java 10 with ARM family processors in the future, you will have to use a different jvm implementation, such as OpenJDK.
If you find this topic interesting and want to ask me about something else that is not covered here, please feel free to contact me at [email protected].