Buffer-Implementierungen in Java

Bereits mit der Version 1.4 von Java wurde das nio-Paket  eingeführt. Dies enthält als Hauptbestandteil Klassen, die einen Buffer, also einen Container für Daten enthalten. Diese Buffer-Implementierungen in Java können für eine sehr effiziente In-Memory-Datenhaltung genutzt werden, insbesondere kann aber auch das in einem vorherigen Artikel bereits vorgestellte Off-Heap-Memory benutzt werden. 

Ein Buffer im Heap-Speicher

Im Package java.nio sind verschiedene-Buffer-Implementierungen vorhanden, beispielsweise auch ein ByteBuffer. Mit diesem können wir nun eine weitere MegyByte-Klassen definieren:

package org.javacream.util.memory;
import java.nio.ByteBuffer;
public class ByteBufferMegaByte implements MegaByte {
	private ByteBuffer byteBuffer;
	public ByteBufferMegaByte(int megaByte) {
		byteBuffer = ByteBuffer.allocate(megaByte * 1024 * 1024);
	}
	@Override
	public void set(int i, byte value) throws NoSuchFieldException, IllegalAccessException {
		byteBuffer.put(i, value);
	}
	@Override
	public int get(int i) throws NoSuchFieldException, IllegalAccessException {
		return byteBuffer.get(i);
	}
	@Override
	public long size() {
		return byteBuffer.capacity();
	}
}

Auch hier wird der normale Heap-Speicher der JVM genutzt:

usedMemoryBefore: 1384304, usedMemoryAfter: 526779224

usedMemoryBefore: 1384304, usedMemoryAfterGc: 1385032

Mit einer minimalen Änderung wird nun aber der Buffer-Speicher im Off-Heap genutzt:

byteBuffer = ByteBuffer.allocateDirect(megaByte * 1024 * 1024);

Das nio-API sieht übrigens keine explizite Möglichkeit vor, den Speicher des Buffers wieder freizugeben. Dies übernimmt finalize, eine zugegebenermaßen etwas wacklige und undefinierte Arbeitsweise. Inwieweit zukünftige Java-Versionen hier Abhilfe schaffen ist noch nicht abzusehen.


Quellcode

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


Seminare zum Thema

Weiterlesen

Off-Heap Memory in Java

Die Java Virtual Machine organisiert und verwaltet die Objekte einer Anwendung selbst im so genannten Heap-Speicher. Diese wurde im Beitrag über Referenzen und Objekte beschrieben.  Ein automatischer Hintergrundprozess, die Garbage Collection, entfernt automatisch nicht mehr referenzierbare Objekte und bereinigt somit den Speicher. 

“Native Memory” bzw Off-Heap Memory in Java ist jedoch ebenfalls möglich! 

Was ist “Off-Heap Memory”?

Das Off-Heap Memory ist, wie der Name auch sagt, außerhalb der des Heaps angesiedelt und wird deshalb nicht von der Garbage Collection bereinigt. Weiterhin ist es für einen Programmierer nicht möglich, Objekte direkt im Off-Heap Memory zu instanzieren. Damit ist die Benutzung deutlich komplexer  und auch mit mehr Fehlerpotenzial behaftet als das Referenzen-Modell.

Warum “Off-Heap Memory”?

Damit scheint die Benutzung dieses Speichers ein Rückschritt zu sein: Der Programmierer hat deutlich mehr Verantwortung, nicht mehr notwendige Daten zu identifizieren und dann auch selber zu löschen. Weiterhin müssen auch alle abhängigen Objekte erkannt werden. Es stellt sich deshalb die Frage, was denn Szenarien sein könnten, dieses native Memory zu benutzen. 

Dafür kann aber sofort ein eingängiges Beispiel gefunden werden: Ein Java-basierter Cache!

Zur Ablage der Cache-Einträge ist das Native Memory nämlich wunderbar geeignet:

  • Die Gültigkeit von Daten, die im Cache abgelegt werden, ist mit Stunden oder sogar noch viel länger aus Sicht der Frequenz der Garbage-Collection wirklich sehr lange. Damit prüft der Collector diese Objekte immer wieder darauf, ob sie nun endlich gelöscht werden können. Eine sinnlose und damit ineffiziente Verschwendung von CPU-Ressourcen. 
  • Cache-Daten sind häufig eher flache Datenstrukturen und nicht komplexe Objektgrafen. 
  •  Der Lebenszyklus der Daten ist simpel: Mit put werden die Daten geschrieben, mit get gelesen und mit remove gelöscht. Eine einfach zu programmierende Sequenz.

In zwei anderen Artikeln wird das Off-Heap-Memory mit Java-Beispiele genutzt: Einmal mit der internen Klasse Unsafe und dann mit den Buffern des NIO.


Seminare zum Thema

Weiterlesen

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

Weiterlesen

Garbage Collection

Garbage Collection und Java Virtual Machine

Alle erzeugten Objekte werden im Heap-Speicher der JVM (Java Virtual Machine) abgelegt. Da aber dieser Speicherbereich nicht unendlich groß ist, müssen die nicht mehr benötigten Objekte wieder entfernt werden. Diese Aufgabe könnte uns als Entwickler obliegen. Doch wir haben Glück: Java kennt eine automatische Speicherbereinigung, die Garbage Collection (Übersetzt: Müllabfuhr). Alle nicht referenzierten Objekte werden aus dem Heap-Speicher entfernt.
Trotzdem kann es durch unachtsame Programmierung zu Speicherlecks (Memory Leaks) kommen. Wenn eine nicht mehr benötigte Referenz noch ein Objekt, dies kann auch eine sehr große Collection sein, referenziert, wird dieses Objekt eben nicht aus dem Speicher entfernt.

Beispiel

Anhand eines kleinen Beispiels möchte ich die Garbage Collection erläutern:

public class Person {

  private String name;

  private Person partner;

  public Person( String name ) { this.name = name; }

  public Person getPartner() { return partner;       }

  public void setPartner(Person partner) { this.partner = partner; }

  public static void main(String[] args) {

    Person [ ] personen = { new Person( "Margit"), new Person("Hans" ) };

  }

}

Anschließend wollen wir unserer Person Margit einen Partner zuweisen.

public static void main(String[] args) {

  Person [ ] personen = { new Person("Margit"), new Person("Hans") };

 

  personen[0].setPartner( personen[1] );

}

Was passiert aber, wenn wir das Array an der Stelle 0 mit null initialisieren?

public static void main(String[] args) {

  Person [ ] personen = { new Person("Margit"), new Person("Hans") };

 

  personen[0].setPartner( personen[1] );

           

  personen[0] = null;

}

Arbeitsweise

Der Garbage Collector wird ausgehend von unserer Klasse Person auf dem Stack die main-Methode untersuchen. Welche Objekte können von hier erreicht werden? Über die lokale Variable personen wird das Array erreicht. An der Stelle 1 im Array wird die Person Hans referenziert und somit werden weder das Array noch die Person Hans aus dem Heap-Speicher entfernt. Der Speicher der Person Margit ist ohne Referenzierung und wird somit freigegeben.

Wie sieht es im nächsten Fall aus?

public static void main(String[] args) {

  Person [ ] personen = { new Person("Margit"), new Person("Hans") };

  personen[0].setPartner( personen[1] );

  personen[1].setPartner( personen[0] );

}

Nun initialisieren wir die lokale Variable personen mit null.

public static void main(String[] args) {

  Person [ ] personen = { new Person("Margit"), new Person("Hans") };

  personen[0].setPartner( personen[1] );

  personen[1].setPartner( personen[0] );

            

  personen = null;

}

Jetzt sieht die Sache etwas anders aus. Der Garbage Collector beginnt in der main-Methode und findet die null-Referenz in der lokalen Variablen personen, Somit ist von hier kein Objekt erreichbar. Obwohl Margit und auch Hans jeweils referenziert sind, werden sie aus dem Heap-Speicher entfernt, da sie aus der main-Methode unerreichbar sind.

 

Im nächsten Beispiel sehen wir eine zusätzliche lokale Variable misterX. Wenn nun die lokale Variable personen mit null initialisiert wird, entfernt der Garbage Collector nur das Array, nicht aber die Personen Margit und Hans, da es noch eine Referenz misterX auf die Person Margit gibt.

In der Praxis sind dies oft nicht mehr benötigte oder vergessene Referenzen, so dass die Garbage Collection diese Objekte im Speicher belässt. Man spricht hier von Memory Leaks oder Speicherlecks. Dies könnte man hier durch Dereferenzieren der lokalen Variablen misterX leicht verhindern.

public static void main(String[] args) {

  Person [ ] personen = { new Person("Margit") , new Person("Hans") };

  personen[0].setPartner( personen[1] );

  personen[1].setPartner( personen[0] );

            

  Person misterX = personen[ 0 ].getPartner( );

            

  personen = null;

}

Bevor der Garbage Collector ein Objekt aus dem Speicher entfernt, wird immer noch die Methode finalize( ) durchlaufen. Diese ist in der Klasse Object implementiert und kann überschrieben werden. Da der Entwickler aber weder weiß wann die Garbage Collection läuft, noch ob sie überhaupt läuft, ist diese Methode mit Vorsicht zu genießen. Seit JAVA 9 ist sie deprecated gekennzeichnet, weil viele Programmierer sie fehlerhaft eingesetzt haben.

Zusammenfassung

Dies war eine kurze einfache Darstellung der Garbage Collection. Alle nicht mehr erreichbaren Objekte ausgehend von der Root-Klasse und der main werden automatisch in parallelen Threads aus dem Heap-Speicher entfernt. Der Zeitpunkt ist für uns Entwickler nicht vorhersehbar.

In unserem Seminar Java Erweiterungen II (03323) wird das Thema mit schwachen (Weak Reference) und starken Referenzen vertieft.

 

Seminar zum Thema

 

Bildnachweis: Photo by Melodi2 at Morguefile.com

Weiterlesen

Referenzen und Objekte

Objekt-orientierte Programmierung ist bekanntermaßen ein sehr verbreitetes Konzept zur Modellierung komplexer Anwendungen. Die Programmiersprache Java hat bereits seit ihrer Einführung 1995 dieses Konzept aufgenommen. Auch die Laufzeitumgebung, also die Java Virtual Machine (JVM) berücksichtigt dieses Konzept und implementiert deshalb ein Speichermodell, das auf Referenzen und Objekte ausgerichtet ist.

Stack und Heap

Der Speicher der Java Virtual Machine ist in einen Stack- und einen Heap-Bereich unterteilt.

Stack und Heap der JVM

Diese Unterteilung ist zwar etwas grob, genügt aber für die folgende Betrachtung völlig.

Jede Java-Methode bekommt ihren eigenen Stack-Bereich zugeordnet. Innerhalb des Stacks werden sämtliche lokalen Variablen abgelegt. Wie im Objekt-orientierten notwendig sind diese Variablen in Java stets Referenzen auf Objekte. Diese werden im Heap-Bereich abgelegt.

Dies sei am Beispiel der folgenden main-Methode aufgezeigt:

public static void main(String[] args){
String message = "Hello";
String[] names = {"John", "Fritz"};
}

Im Speicher der JVM ergibt sich das folgende Bild:

Die main-Methode mit den lokalen Variablen message und names

Methodenaufruf

Nun definieren wir eine zweite Methode, called, diesmal mit 2 Parametern

public static void called(String str, String[] list){

}

Die methoden called mit den beiden Parametern str und list

Diese Methode wird aus main heraus aufgerufen:

public static void main(String[] args){
String message = "Hello";
String[] names = {"John", "Fritz"};
called(message, names);
}

Wie erfolgt nun jedoch die Übergabe der lokalen Variablen message und names in die Parameter str und list?

Wie werden die Parameter belegt?

Dazu wird die JVM die Werte der Referenzen als Kopie übergeben, also ein “CallByValue”-Verhalten auf der Ebene der Referenzen:

Copy By Value mit Referenzen

Im Endeffekt zeigen die Referenzen auf die selben Objekte im Heap.

Zuweisung von Werten

Was passiert aber nun, wenn in der aufgerufenen Methode den Parametern neue Werte zugewiesen werden? Hier ergibt sich ein feiner aber unglaublich wichtiger Unterschied: Belegen wir den Parameter str einfach mit einem neuen Wert, z.B. mit

str = "Goodbye";

so wird die Variable im Stack einfach mit einer Referenz auf das neue Objekt im Heap überschrieben:

Zuweisung eines neuen Wertes an den Parameter str

Wichtig hier ist, dass in der aufrufenden main-Methode die Variable message davon völlig unbeeinflusst bleibt.

Was passiert aber, wenn wir die Referenz auf das Array benutzen, um beispielsweise den Wert des zweiten Elements mit Index 1 auf “Paul” zu setzen?

list[1] ="Paul";

Nun ergibt sich ein anderes Bild:

Zuweisung eines neuen Wertes an das zweite Element der Liste

Hier ist klar ersichtlich, dass diese Änderung des Array-Objekts innerhalb der Methode called auch direkt Auswirkungen auf die aufrufende main-Methode hat: Nachdem die Methode called terminiert ist referenziert auch die Variable message auf das Array, dessen zweites Element nun auf “Paul” zeigt.

Also innerhalb von main

public static void main(String[] args){
String message = "Hello";
String[] names = {"John", "Fritz"};
called(message, names);
System.out.println(names[1]); //-> Paul
}

Zusammenfassung und Ausblick

Durch die Betrachtung des Speichermodells der Java Virtual Machine wird die Arbeitsweise eines Objekt-orientierten Programms unter Verwendung von Referenzen klar und verständlich. Das komplette Beispiel liegt als fertiges Eclipse-Projekt vor und enthält zusätzlich Konsolen-Ausgaben, die den Ablauf des Programms demonstrieren. Selbstverständlich kann aber auch der Eclipse-Debugger benutzt werden, um die Ausführung nachzuvollziehen.

Allerdings sind sicherlich auch noch einige Punkte nicht fundiert behandelt worden:

  • Das vorgestellte Speichermodell ist stark vereinfacht. Wie schaut es denn tatsächlich aus?
  • Was passiert eigentlich mit dem armen Objekt “Fritz”, das nun komplett losgelöst im Heap vorhanden ist?

Diese beiden Punkte werden in einem folgenden Artikel, der den Garbage Collector der JVM als Thema hat, behandelt.


Seminar zum Thema

Weiterlesen