Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / Java

Swap Based Memory Management In Java

0.00/5 (No votes)
23 Nov 2012CPOL2 min read 9.7K  
Blue Sky Thinking In JavaJava does not allow strong interactions with the memory manager other than via extensions (like JVMTI).

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):

/* For Copyright and License see LICENSE.txt and COPYING.txt in the root directory */
/**
 * 
 */
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;
/**
 * @author a1t
 * 
 */
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];
  }
  /**
   * Returns a linearly interpolated sample based on the samples either side of the the passed index. This is used for super
   * sampling or pitch effects.
   * 
   * @param index
   * @return
   */
  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);
  }
  /**
   * Returns a cubic interpolated sample based on the samples either side of the the passed index. This is used for super
   * sampling or pitch effects. Cubic interpolation uses two samples either side of the required point and so at the ends of
   * the sample this will fall back to linear interpolation.
   * 
   * @param index
   * @return
   */
  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")); //$NON-NLS-1$
    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")); //$NON-NLS-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;
      // System.out.println("Memory up  to " + (totalMemory / (1024 * 1024)));
    }
    static synchronized void decrementTotalMemory(long ammount)
    {
      totalMemory -= ammount;
      // System.out.println("Memory down to " + (totalMemory / (1024 * 1024)));
    }
    public static long getTotalMemory()
    {
      return totalMemory;
    }
    private void writeObject(ObjectOutputStream out) throws IOException
    {
      //System.out.println(Messages.getString("SFData.4")); //$NON-NLS-1$
      out.writeLong(this.ammount);
      decrementTotalMemory(ammount);
      ammount = 0; // don't double count in finalizer
    }
    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); //$NON-NLS-1$ //$NON-NLS-2$
        System.gc(); // Request garbage collection
      }
      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;
  // System.out.println("Memory up  to " + (totalMemory / (1024 * 1024)));
}
static synchronized void decrementTotalMemory(long ammount)
{
  totalMemory -= ammount;
  // System.out.println("Memory down to " + (totalMemory / (1024 * 1024)));
}

When an SFData object us created totalMemory is upped.

@Override
protected void finalize()
{
  decrementTotalMemory(ammount);
}

When an SFData object is finalized totalMemory is decreased.

C#
private void writeObject(ObjectOutputStream out) throws IOException
{
  //System.out.println(Messages.getString("SFData.4")); //$NON-NLS-1$
  out.writeLong(this.ammount);
  decrementTotalMemory(ammount);
  ammount = 0; // don't double count in finalizer
}

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.

C#
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); //$NON-NLS-1$ //$NON-NLS-2$
    System.gc(); // Request garbage collection
  }
  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.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)