Blue Sky Thinking In Java
Java does not allow strong interactions with the memory manager other than via extensions (like JVMTI). However, for some JVMs (the Oracle/Sun JVM for example) all is not lost.
The challenge I faced was to move over to swapping out very large data structures to disk when the main RAM was being exhausted. Relying of the operating system's swapping system proved not to work because the memory which needed swapping out was too poorly localised. In other words, when the memory you need rarely is mixed in the same pages as that you use all the time, OS swapping achieves nothing but killing the disk.
So, how did I do it?
On the JVM 1.7 from Oracle, all those horror warnings about finalizers not being run can be ignored most of the time. For large objects which are definitely garbage collected during the lifetime of the program, finalisers are run. So, I used finalisers in the solution. If one were to hit issues with finalisers then
phantom references could be used instead (they are enqueue as part of the standard). So, we can track when large objects are created and we can track when they are destroyed. So, now we need to work out how many are live and when their storage goes above a given limit.
I did all this for the sound data in Sonic Field. Whist creating large generative pieces (running up to an hour) like
Helio-1, storing audio for the entire generation sequence was exhausting even the 16G of memory on my Mac Book. So, here is the code for the SFData object which stores audio data
(this code is
AGPL 3.0 so do not copy it unless you are sure of the implications):
package com.nerdscentral.audio;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import com.nerdscentral.audio.pitch.CubicInterpolator;
import com.nerdscentral.sfpl.SFMaths;
import com.nerdscentral.sfpl.SFPL_RuntimeException;
public class SFData implements Serializable
{
private static final long serialVersionUID = 1L;
private final float[] data;
private final int length;
@SuppressWarnings("unused")
private final MemoryManager memMan;
private SFData(int lengthIn)
{
data = new float[lengthIn];
this.length = lengthIn;
memMan = new MemoryManager(lengthIn);
}
public final static SFData build(int size)
{
return new SFData(size);
}
public final SFData replicate()
{
SFData data1 = new SFData(this.getLength());
for (int i = 0; i < this.getLength(); ++i)
{
data1.setSample(i, this.getSample(i));
}
return data1;
}
public final SFData replicateEmpty()
{
return new SFData(this.getLength());
}
public final double getSample(int index)
{
return this.data[index];
}
public final double getSampleLinear(double index)
{
double s = SFMaths.floor(index);
double e = SFMaths.ceil(index);
if (s < 0 || e >= data.length)
{
return 0;
}
if (s == e) return data[(int) s];
double a = data[(int) s];
double b = data[(int) e];
return ((index - s) * b + (e - index) * a);
}
public final double getSampleCubic(double index)
{
int s = (int) SFMaths.floor(index);
int e = (int) SFMaths.ceil(index);
if (s < 0 || e >= data.length)
{
return 0;
}
if (s > data.length - 3 || index < 1)
{
if (s == e) return data[s];
double a = data[s];
double b = data[e];
return ((index - s) * b + (e - index) * a);
}
return CubicInterpolator.getValue(data[s - 1], data[s], data[s + 1], data[s + 2], index - s);
}
public final double setSample(int index, double value)
{
return this.data[index] = (float) value;
}
public final int getLength()
{
return this.length;
}
public static final SFData build(float[] input)
{
SFData data = new SFData(input.length);
for (int i = 0; i < input.length; ++i)
{
data.setSample(i, input[i]);
}
return data;
}
public void setAt(int pos, SFData data2) throws SFPL_RuntimeException
{
int pos2 = pos;
if (pos2 + data2.length > length) throw new SFPL_RuntimeException(Messages.getString("SFData.0"));
int end = pos2 + data2.length;
for (int index = pos2; index < end; ++index)
{
data[index] = data2.data[index - pos2];
}
}
public void setFrom(int pos, SFData data2) throws SFPL_RuntimeException
{
int pos2 = pos;
if (pos2 + length > data2.length) throw new SFPL_RuntimeException(Messages.getString("SFData.1"));
for (int index = 0; index < length; ++index)
{
data[index] = data2.data[index + pos];
}
}
public void fastLoad(SFData inData, int offset)
{
float[] in = inData.data;
System.arraycopy(in, 0, data, offset, in.length);
}
public static class MemoryManager implements Serializable
{
private static final long serialVersionUID = 1L;
static volatile long totalMemory;
private static double swapLimit = 4;
static final double oneGig = 1024 * 1024 * 1024;
static synchronized void incrementTotalMemory(long ammount)
{
totalMemory += ammount;
}
static synchronized void decrementTotalMemory(long ammount)
{
totalMemory -= ammount;
}
public static long getTotalMemory()
{
return totalMemory;
}
private void writeObject(ObjectOutputStream out) throws IOException
{
out.writeLong(this.ammount);
decrementTotalMemory(ammount);
ammount = 0;
}
private void readObject(ObjectInputStream in) throws IOException
{
ammount = in.readLong();
incrementTotalMemory(ammount);
}
private long ammount;
public MemoryManager(final long memoryUsed)
{
incrementTotalMemory(memoryUsed);
ammount = memoryUsed;
}
@Override
protected void finalize()
{
decrementTotalMemory(ammount);
}
public static boolean underLimit(long bias)
{
double used = (getTotalMemory() / oneGig);
double toBeUsed = used + (bias / oneGig);
boolean x = toBeUsed < swapLimit;
if (!x)
{
System.out.println(Messages.getString("SFData.6") + toBeUsed + Messages.getString("SFData.7") + swapLimit);
System.gc();
}
return x;
}
public static void setSwapLimit(double limit)
{
swapLimit = limit;
}
}
}
Light at the end of the tunnel. - Copyright Dr Alexander J Turner All Rights Reserved
An SFData object stores audio data in a float array. The size can be worked out from the size of this array at 4 bytes an element. The size of this array is so dominant as to make it OK to just consider this size. The key feature is that the SFData object has one and one only MemoryManager object inside it. This object is the thing which keeps track of memory:
static volatile long totalMemory;
private static double swapLimit = 4;
static final double oneGig = 1024 * 1024 * 1024;
static synchronized void incrementTotalMemory(long ammount)
{
totalMemory += ammount;
}
static synchronized void decrementTotalMemory(long ammount)
{
totalMemory -= ammount;
}
When an SFData object us created totalMemory is upped.
@Override
protected void finalize()
{
decrementTotalMemory(ammount);
}
When an SFData object is finalized totalMemory is decreased.
private void writeObject(ObjectOutputStream out) throws IOException
{
out.writeLong(this.ammount);
decrementTotalMemory(ammount);
ammount = 0;
}
private void readObject(ObjectInputStream in) throws IOException
{
ammount = in.readLong();
incrementTotalMemory(ammount);
}
When an SFData object is serialised or deserialised the size removed from or add to the totalMemory count as appropriate.
public static boolean underLimit(long bias)
{
double used = (getTotalMemory() / oneGig);
double toBeUsed = used + (bias / oneGig);
boolean x = toBeUsed < swapLimit;
if (!x)
{
System.out.println(Messages.getString("SFData.6") + toBeUsed + Messages.getString("SFData.7") + swapLimit);
System.gc();
}
return x;
}
When the swap system wants to know if it should swap out a SFData object it can call
underLimit to find out. If swapping does start happening, garbage collection is requested. This is not an excessive cost because swapping is costly, for forcing GC to avoid it could save a lot of time and will not add much compared to the cost of swapping when swapping cannot be avoided.