As you may already well know, it’s easy to get started with Java on OCI. Following the Java motto ‘write once, run anywhere’, we are adding an exciting new destination for your applications to run on: powerful 64-bit Arm systems running on the Oracle Cloud Infrastructure (OCI). A perfect match for our high-performant Oracle Java SE runtime for 64-bit Arm systems!

Oracle customers, and the broader Java ecosystem, have been able to rely on high-quality, thoroughly tested Oracle JDK and Oracle OpenJDK releases for their 64-bit Arm systems for several years, across Java SE 8, Java SE 11, Java SE 16 and soon Java SE 17 releases.

We have been working with a broad set of partners in the Arm ecosystem in recent years to bring the performance, power, and scalability benefits of 64-bit Arm systems to the cloud. Notably, we have collaborated with Ampere on optimizing Java for their systems. This collaboration includes Arm-specific improvements to the Z Garbage Collector (ZGC), just-in-time compiler (JIT) and Java Virtual Machine (JVM) runtime, a host of changes to enable Java to work better on Arm overall, as well as continued development and enablement of exciting new features like Project Loom and Project Valhalla for Arm systems in the OpenJDK open source community.

For example, in JDK 16, we have introduced a new Vector API as an incubator module. The jdk.incubator.vector module defines an API for expressing computations that can be reliably compiled at runtime into SIMD instructions, such as AVX instructions on x64, and NEON instructions on AArch64. This new Vector API is a nice example that shows how the latest advances in Java technology are made available simultaneously on both x64 and 64-bit ARM systems, even as they are being developed. This in turn allows users to trust that their Java applications developed using the latest features can work equally well across their current and future development and production systems.

Here’s a simple program to compute a Mandelbrot image into a file using the Vector API:

First, we need to import the Java SE API classes that we are going to use:

import java.awt.*;

import java.awt.image.*;

import java.io.*;

import java.util.*;

import javax.imageio.*;

import jdk.incubator.vector.*;

 

Then we need to create the main class of our example program. Our example only has a single class, so it can be executed directly from the command line, and there is no need to provide a build harness:

 

public class MBV {

We’ll iterate up to 256 times before stopping unless the calculation spirals out of control.

              private static final int maxIteration = 256;

We’ll use 256-bit wide Vectors of Doubles for the simultaneous calculations.

              private static final VectorSpecies<Double> SPECIES = DoubleVector.SPECIES_256;

We’ll calculate four points at once and return the number of iterations for each before the calculation at a given point spirals out of control.

              private static int [] calcValues(double [] x, double [] y) {

final DoubleVector x0 = DoubleVector.fromArray(SPECIES, x, 0);

                            final DoubleVector y0 = DoubleVector.fromArray(SPECIES, y, 0);

                            DoubleVector xs = x0;

                            DoubleVector ys = y0;

                            int [] results = new int [x.length];

 

              for (int i = 0; i <= maxIteration; i++) {

The calculations are performed in parallel on vectors of doubles representing the individual points.

                                          DoubleVector x2 = xs.mul(xs);

                                          DoubleVector y2 = ys.mul(ys);

We use a mask to check if any calculation on a point has already spiraled out of control.

                                          VectorMask<Double> mask = x2.add(y2).lt(4.0).not();

                                          if (mask.anyTrue()) {

                                                         int last = mask.lastTrue();

                                                         for (int j = mask.firstTrue(); j <= last; j++) {

                                                                       if (results[j] == 0 && mask.laneIsSet(j)) {

                                                                                     results[j] = i;

                                                                       }

                                                         }

                                          }

If all calculations have spiraled out of control, then there is no need for computations to go on

                                          if (mask.allTrue())

                                                         return results;

Otherwise, let’s complete this iteration by computing its results in parallel, and move to the next

                                          ys = ys.mul(2).mul(xs).add(y0);

                                          xs = x2.sub(y2).add(x0);

                            }

After all iterations are complete, make sure that the results reflect the number of iterations used

                            for (int i = 0; i < x.length; i++) {

                                          if (results[i] == 0) {

                                                         results[i] = maxIteration;

                                          }

                            }

 

                            return results;

              }

 

The example application accepts an argument that defines the size of the Mandelbrot image to be output to the file out.png in the current directory.

              public static void main(String [] args) {

                            File outputFile = new File (“out.png”);

                            final int SIZE = Integer.parseUnsignedInt(args[0]);

The Mandelbrot Set is located between -2 and +2 on each axis, so we increment each pixel by a fraction.

                            final double INC = 4.0/SIZE;

Let’s use a few shades of blue in our image.

                            final int [] colors = new int [maxIteration];

                            for (int i = 0; i < colors.length; i++)

                                          colors[i] = new Color(i,i,236).getRGB();

We’ll create the image in memory.

                            BufferedImage image = new BufferedImage (SIZE,SIZE,BufferedImage.TYPE_INT_RGB);

For every four pixels, we’ll create arrays of doubles to calculate the iterations with.

                for (int y = 0; y < SIZE; y++) {

                                          double [] cre = new double[] {-2.0+y*INC, -2.0+y*INC, -2.0+y*INC, -2.0+y*INC};

 

                            for (int x = 0; x < SIZE; x+=4) {

                                                         double [] cim = new double[] {-2.0+x*INC, -2.0+(x+1)*INC, -2.0+(x+2)*INC, -2.0+(x+3)*INC};

 

                                                         int [] results = calcValues(cim, cre);

Once the calculation is finished, we’ll color the respective pixels.

                                                         for (int j = 0; x+j < SIZE && j < cre.length; j++) {

                                                                       image.setRGB(x+j,y, results[j] == maxIteration? Color.BLACK.getRGB() : colors[results[j]]);

                                                         }

                            }

                            }

Finally, when the image is complete, we’ll output the image to a file.

                            try {

                                          ImageIO.write(image, “png”, outputFile);

                            } catch (IOException e) {

                                          e.printStackTrace();

                            }                                                      

              }

}

Since it’s a single file, it can be run from the command line without the need to build it first:

$ jdk-16/bin/java –add-modules jdk.incubator.vector MBV.java 2000

WARNING: Using incubator modules: jdk.incubator.vector

warning: using incubating module(s): jdk.incubator.vector

This example code using the latest bleeding edge Vector APIs will produce the exact same output regardless whether it’s being run on JDK 16 on an x64 system or on a 64-bit ARM system. It shows that you can use the latest features transparently across latest systems available to Java developers. You can use the example as a starting point to explore the characteristics of the Vector API implementation on 64-bit ARM systems – for example, try modifying the code to use all available cores on the ARM instances on OCI, or to use DoubleVector.SPECIES_PREFERRED on each platform rather than 256-bit wide Vectors, and compare the results.

With Java, you can take your code, and truly run it anywhere, confident that it will work well regardless of the platform you chose now, or in the future.

We hope that you’ll be as excited about the possibilities of Java on Arm on OCI, as we are.  You can get started with Java on Arm on OCI now by signing up for a free OCI account.

Remember that Oracle Java license and support is included with OCI services that provide access to the operating system.  For separate and on-prem work loads you can get additional details on the license and support with the Oracle Java SE Subscription here.