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.