Programmieren mit dem Off-Heap-Memory

Das in einem anderen Artikel vorgestellte Native Memory in Java wird im Folgenden an Hand von Beispielen zum Programmieren mit dem Off-Heap Memory  vertieft.

Die Umsetzung des Off-Heap Memories in Java hat eine lange Historie, die verschiedene Lösungsstrategien hervorgebracht hat. 

Benutzung eines RAM-Drives

Als auch heute noch durchaus gebräuchliche Möglichkeit hat sich die Verwendung des Dateisystems etabliert. “Moment”, wird der aufmerksame Leser anmerken, “das hat doch nichts mit RAM zu tun”. Nun, stimmt nicht ganz: Unter Linux ist  /tmp in der Regel immer im RAM abgelegt. Benutzt damit ein Java-Programm dieses Verzeichnis, werden die darin abgelegten Dateien im Endeffekt im Speicher gehalten.  

Unsafe

Diese Lösung benutzt eine sehr skurrile Klasse aus der Java-Bibliothek: sun.misc.Unsafe. Diese Klasse, deren Namen ihre Verwendung demotivieren soll, ermöglichte bereits seit der ersten Java-Version einen sehr direkten Zugriff auf den Speicher der JVM. Ein Objekt ohne Konstruktoraufruf erstellen? Wird gemacht. Und dann thematisch passend zu diesem Beitrag: Off-Heap Memory anfordern und verwalten. Die Benutzung sei an folgendem Beispiel demonstriert. Als erstes programmieren wir eine simple Klasse, die Megabytes an Daten in einem Array allokiert:

package org.javacream.util.memory;

import java.io.Serializable;

public class ByteArrayMegaByte implements Serializable, MegaByte {
	private static final long serialVersionUID = 1L;

	private byte[] megabyte;

	public byte[] getMegabyte() {
		return megabyte;
	}

	public ByteArrayMegaByte(int mByte) {
		megabyte = new byte[mByte * 1024 * 1024];
	}

	@Override
	public void set(int i, byte value) throws NoSuchFieldException, IllegalAccessException {
		megabyte[i] = value;
	}

	@Override
	public int get(int i) throws NoSuchFieldException, IllegalAccessException {
		return megabyte[i];
	}

	@Override
	public long size() {
		return megabyte.length;
	}

}

Im folgenden Testfall wird diese Klasse benutzt, um Heap-Speicher zu belegen. Der Testfall erzeugt hierzu Konsolenausgaben, die die Belegung des Speichers beweisen:

@Test
public void testMegaByteAllocation() {
Runtime runtime = Runtime.getRuntime();
System.gc();
long usedMemoryBefore = runtime.totalMemory() - runtime.freeMemory();
@SuppressWarnings("unused")
MegaByte megaByte = new MegaByte(500);
long usedMemoryAfter = runtime.totalMemory() - runtime.freeMemory();
System.gc();
System.out.println("usedMemoryBefore: " + usedMemoryBefore + ", usedMemoryAfter" + usedMemoryAfter);
megaByte = null;
System.gc();
long usedMemoryAfterGc = runtime.totalMemory() - runtime.freeMemory();
System.out.println("usedMemoryBefore: " + usedMemoryBefore + ", usedMemoryAfterGc" + usedMemoryAfterGc);

}

 

Exemplarische Ausgaben eines Testlaufes könnten sein:

usedMemoryBefore: 1384224, usedMemoryAfter: 526745376
usedMemoryBefore: 1384224, usedMemoryAfterGc: 1384888

Es ist klar ersichtlich, dass das 500MB-Array entsprechenden Heap-Speicher benutzt, der nach der Garbage Collection wieder zur Verfügung ist.

Nun aber die Implementierung des Megabytes mit Off-Heap Memory:

package org.javacream.util.memory;

import java.io.Serializable;
import java.lang.reflect.Field;

import sun.misc.Unsafe;

public class OffHeapMegaByte implements Serializable, MegaByte {
	private static final long serialVersionUID = 1L;
	private Unsafe unsafe;

	{
		try {
			Field f = Unsafe.class.getDeclaredField("theUnsafe");
			f.setAccessible(true);
			unsafe = (Unsafe) f.get(null);
		} catch (Exception e) {
			throw new RuntimeException(e.getMessage());
		}
	}
	private final static int BYTE = 1;
	private long size;
	private long address;

	public OffHeapMegaByte(int mByte) {
		try {
			this.size = mByte * 1024 * 1024;
			address = unsafe.allocateMemory(size * BYTE);
		} catch (Exception e) {
			throw new RuntimeException(e.getMessage());
		}

	}

	@Override
	public void set(int i, byte value) throws NoSuchFieldException, IllegalAccessException {
		unsafe.putByte(address + i * BYTE, value);
	}

	@Override
	public int get(int i) throws NoSuchFieldException, IllegalAccessException {
		return unsafe.getByte(address + i * BYTE);
	}

	@Override
	public long size() {
		return size;
	}

	public void freeMemory() throws NoSuchFieldException, IllegalAccessException {
		unsafe.freeMemory(address);
	}
}

Achten Sie hier insbesondere auf die Methode freeMemory(): Diese muss aufgerufen werden, um den Off-Heap Speicher wieder zu bereinigen!

Ein dem obigen Beispiel entsprechender Test

@Test
public void testMegaByteAllocation() {
  Runtime runtime = Runtime.getRuntime();
  System.gc();
  long usedMemoryBefore = runtime.totalMemory() - runtime.freeMemory();
  @SuppressWarnings("unused")
  OffHeapMegaByte megaByte = new OffHeapMegaByte(500);
  long usedMemoryAfter = runtime.totalMemory() - runtime.freeMemory();
  System.gc();
  System.out.println("usedMemoryBefore: " + usedMemoryBefore + ", usedMemoryAfter: " + usedMemoryAfter);
  megaByte = null;
  System.gc();
  long usedMemoryAfterGc = runtime.totalMemory() - runtime.freeMemory();
  System.out.println("usedMemoryBefore: " + usedMemoryBefore + ", usedMemoryAfterGc: " + usedMemoryAfterGc);
  
}

Zeigt nun aber diese Ausgaben:

usedMemoryBefore: 1384184, usedMemoryAfter: 1447120
usedMemoryBefore: 1384184, usedMemoryAfterGc: 1387328

Hier ist der Heap-Speicher überhaupt nicht betroffen!

Um zu zeigen, dass Off-Heap-Memory benutzt wird, wird der Test mit der JVM-Option

-XX:NativeMemoryTracking=detail gestartet und blockiert, kann mit

jcmd <pid> VM.native_memory

unter Angabe der Process-Id (mit jps)  ein detaillierter Auszug des Gesamtspeichers der JVM ausgegeben werden. Eine exemplarische Ausgabe zeigt die etwa 500MByte des reservierten internen Speichers:

Internal (reserved=512381KB, committed=512381KB)
(malloc=512349KB #1377)
(mmap: reserved=32KB, committed=32KB)

Die Historie der Unsafe-Klasse ist übrigens recht spannend: Schon der Paketname zeigt, dass diese Klasse niemals für ein “offizielles API” gedacht war. Deshalb existiert hierfür auch kein Javadoc. Oracle wollte dann diese Klasse aus der Bibliothek entfernen, was aber auf den massiven Widerstand von Server-Herstellern, insbesondere eben von Caching-Lösungen stieß. Deshalb hat Oracle davon abgesehen, diese Klasse zu entfernen, arbeitet aber an einer offiziellen Lösung, die eine einfache Migration weg von sun.misc.Unsafe ermöglichen soll.  Stand heute ist diese Klasse aber immer noch im aktuellen Java-Release vorhanden. 

Buffer des NIO

Die einfachste Art der Programmierung mit dem Off-Heap-Memory ist jedoch sicherlich das mit Java 1.4 eingeführte Paket java.nio und die darin enthaltenen Buffer-Implementierungen. Diese werden in einem weiteren Artikel beschrieben. 


Quellcode

Der Quellcode der Beispiele ist in einem GitHub-Repository des Autors abgelegt und kann von dort geladen werden.


Seminare zum Thema

Dr. Rainer Sawitzki / Dr. Rainer Sawitzki

Nach seinem Studium der Physik und anschließender Promotion Wechsel in die IT-Branche. Seit mehr als 20 Jahren als Entwickler, Berater und Projektleiter vorwiegend im Bereich Java und JavaScript unterwegs. Parallel dazu in der Entwicklung und Durchführung von hochwertigen Seminaren für die Integrata im Einsatz.

Schreibe einen Kommentar