0

I am working on an application that needs to be compatible with Android 2.3 (Gingerbread), and the device I'm using for development tests is a Motorola Atrix MB860 running Android 2.3.6.

In this device I get roughly 40MB of maximum heap space and as far as I could realize, my app uses around 33MB, but I get an OutOfMemoryError exception anyway.

Basically, the part of my code that matters to this issue creates a large String (8MB - I know it's rather big, but if it's too small it won't satisfy one of the requirements) and then goes on to create 2 threads that use such string to write to a certain memory space concurrently.

Here is the code:

    // Create random string
    StringBuilder sb = new StringBuilder();
    sb.ensureCapacity(8388608); // ensuring 8 MB is allocated on the heap for the StringBuilder object
    for (long i = 0; i < DATA_SIZE; i++) {
            char c = chars[new Random().nextInt(chars.length)];
            sb.append(c);
    }
    String randomByteString = sb.toString();

    ExecutorService executor = Executors.newFixedThreadPool(2);
    for (int i = 0; i < 2; i++) {
        Runnable worker = new SlidingBubbles(param1, param2, randomByteString)
        executor.execute(worker);
    }
    // This will make the executor accept no new threads 
    // and finish all existing threads in the queue 
    executor.shutdown();

    // Wait until all threads are finish 
    while(!executor.isTerminated()) {
        // wait for bubble threads to finish working...
    }

and the threads' routine:

private class SlidingBubbles implements Runnable {
    private int param1, param2;
    private String randomByteString;
    private final Object mSignal = new Object();
    private volatile long tempBytesWritten = 0;
    private volatile long totalBytesWritten = 0;

    public SlidingBubbles(int param1, int param2, String randomByteString) {
        this.param1= param1;
        this.param2= param2;
        this.randomByteString = randomByteString;
    }

    private void doIt() {
        File file = null;
        RandomAccessFile randomAccessFile = null;
        FileChannel fc = null;

        try {
            while(param1> 0) {
                // Instantiate the 1st bubble file
                file = new File(TARGET_DIR, String.valueOf(Calendar.getInstance().getTimeInMillis()));

                while(param2 > 0) {                        
                    randomAccessFile = new RandomAccessFile(file, "rwd");
                    fc = randomAccessFile.getChannel();

                    fc.position(fc.size());
                    synchronized (mSignal) {
                        tempBytesWritten = fc.write(ByteBuffer.wrap(randomByteString.getBytes()));

                        totalBytesWritten += tempBytesWritten;
                    }

       // some other things that don't matter
    }

    @Override
    public void run() {
        wipe();            
    }
}

Awkwardly (to me), on the 30th line of the thread routine (tempBytesWritten = fc.write(ByteBuffer.wrap(randomByteString.getBytes()));), the 2nd thread ("pool-1-thread-2") launches the exception, exits, and the 1st thread ("pool-1-thread-1") continues (actually, starts) executing normally. By the time the JVM is done allocating space for that large String, the app is using 33MB of the heap. As you can see from the code, thatString is created only once, but then used multiple times from both threads.

Shouldn't the threads be using just a reference to the String rather than copying it? (which would, in this case, exceed the 40MB allowance).

I must also point out that it is (or at least seems to be, as far as my understanding goes) impossible to increase this heap space on Gingerbread (previous research).

Is there anything I'm missing? Any help is greatly appreciated.

Community
  • 1
  • 1
Nick Fanelli
  • 265
  • 1
  • 6
  • 19

5 Answers5

1

You can have the 8MB data once statically and never create copies of it. On Android, StringBuilder shares the internal char[] with String but String#getBytes() creates a copy of the data each time.

I assume your characters are plain ASCII, this doesn't work correctly when they are more special.

Random random = new Random(); // once!
byte[] data = new byte[8388608];
for (int i = 0; i < data.length; i++) {
    data[i] = (byte) chars[random.nextInt(chars.length)];
}

above would create the data once without copies. Also note that new Random() 8388608? times in a loop will also cause massive memory usage, they should quickly get garbage collected though.

When you then do

    public SlidingBubbles(int param1, int param2, byte[] data) {
        ...
        synchronized (mSignal) {
            tempBytesWritten = fc.write(ByteBuffer.wrap(data));

You're no longer creating copies of that data, ByteBuffer.wrap does not create a copy of the data. Whatever you do, pass the finished byte[] to SlidingBubbles.

P.s: while(!executor.isTerminated()) { is the wrong way, there is a method for that: How to wait for all threads to finish, using ExecutorService?

Community
  • 1
  • 1
zapl
  • 63,179
  • 10
  • 123
  • 154
  • Some interesting things pointed out. I will try them later. However, replying to the "p.s.", I used `while(!executor.isTerminated())` because I don't know how long the processing of the threads will take, so I had no idea how to set the time parameters of `awaitTermination(long timeout, TimeUnit unit)`. Is there a way to make that flexible? – Nick Fanelli Nov 28 '14 at 22:30
  • @NickFanelli I'd simply set it to "forever", or is there a reason you would want to stop before they are done? forever meaning "Long.MAX_VALUE, TimeUnit.NANOSECONDS" since that's the maximum it can actually take and it's equivalent to about 300 years – zapl Nov 28 '14 at 22:40
  • 1
    @NickFanelli if you really do just `while(!executor.isTerminated()) {}` without any `Thread.sleep()` or similar thing like `wait()` in there you keep the processor busy at 100% load checking that condition over and over again. – zapl Nov 28 '14 at 22:45
  • Got it! That's exactly what I needed. In my ignorance, I thought "Long.MAX_VALUE and TimeUnit.NANOSECONDS" were a way of saying "type here a `long` and `TimeUnit` in nanoseconds", which I would've never figured out how to predict. Thanks for the explanation, @zapl. – Nick Fanelli Dec 03 '14 at 12:13
0

ByteBuffer.wrap(randomByteString.getBytes()) inside a while loop. This is your culprit. Buffers reside in memory. You should get rid of a buffer after using it. Since you are reusing this buffer, move its creation out of the loop. EDIT : try this, keep the array part constant

private static final byte [] arr = randomByteString.getBytes();
for (int i = 0; i < 2; i++) {
        Runnable worker = new SlidingBubbles(param1, param2, arr)
        executor.execute(worker);
    }

in your runnable try this

try {

while(param1> 0) {
// Instantiate the 1st bubble file
file = new File(TARGET_DIR, String.valueOf(Calendar.getInstance().getTimeInMillis()));

while(param2 > 0) {                        
randomAccessFile = new RandomAccessFile(file, "rwd");
fc = randomAccessFile.getChannel();
fc.position(fc.size());
synchronized (mSignal) {
tempBytesWritten = fc.write(ByteBuffer.wrap(arr));
totalBytesWritten += tempBytesWritten;
}
// some other things that don't matter
}
Dexter
  • 1,710
  • 2
  • 17
  • 34
  • Thanks, @Dexter. Moving that instruction out of the loop and sending the `ByteBuffer` itself as a parameter to the threads' constructors helped. The `OutOfMemmoryError` is no longer happening. However, not only the UI still crashes, but for some reason `tempBytesWritten` is showing 0 after each and every iteration. – Nick Fanelli Nov 28 '14 at 20:17
  • Actually, debugging I found out the very first iteration it does get the 8MB from the `fc.write()` call, but the subsequent ones return 0 ... ? – Nick Fanelli Nov 28 '14 at 20:26
  • @NickFanelli that is because a `Buffer` can not be reused that easily, it keeps track of the bytes it has written and expects that you `put` more, see http://tutorials.jenkov.com/java-nio/buffers.html for some explanation. Get rid of the directAllocated one and dimply do `fc.write(ByteBuffer.wrap(arr));` that's not creating a copy of the data, it's just creating a new wrapper around it. – zapl Nov 28 '14 at 21:29
  • @zapl thank-you for pointing that out, I was a little unsure about that :) – Dexter Nov 28 '14 at 21:30
  • There should be a way to return a buffer back into the original state instead of re-wrapping the data, clear (which does not overwrite data with 0) & flip or so some combination of these other commands but I don't actually know how. Buffers are complicated :) – zapl Nov 28 '14 at 21:39
0

I think you have left the StringBuffer hanging around too (since the main thread doesn't exit, sb will still be using up memory)?

So you could save some memory by nulling it out after you've finished using it. Ie

String randomByteString = sb.toString();
sb = null;
user384842
  • 1,946
  • 3
  • 17
  • 24
  • Thanks. Your suggestion might have helped, but it didn't quite resolve the issue by itself. I used it alongside with @Dexter's. – Nick Fanelli Nov 28 '14 at 20:20
  • I just wanted to point out that after further studying, I realized this change doesn't have any effect. The reason is because when you set an object's instance to `null`, all the JVM does is overwrite the address of the pointer in the Stack for that object in the Heap with `null`, but the space previously allocated for such object in the `Heap` is not immediately freed. Freeing that area is the `Garbage Collector`'s responsibility and it doesn't run at the programmer's will. In short, setting the object's instance to `null` doesn't save (or free) any memory space in the `Heap` right away. – Nick Fanelli Dec 03 '14 at 19:43
0

It might relieve your memory problems if you break your string up into an array of 8 X 1MB chunks. This way:

1) Your StringBuilder only needs to be 1MB so uses up less memory - you could reuse it by resetting it with the setLength() method which I think will mean worst case it will only take up 1MB

2) Your ByteBuffer only needs to be 1MB not 8MB

You can just loop round your array when you write the String out to files.

So you'll still need the 8MB for the string, but apart from that you should only need 2MB extra.

user384842
  • 1,946
  • 3
  • 17
  • 24
0

I accepted @Dexter's answer because it was enough to solve my problem with the OutOfMemoryError exception. But as I mentioned in the comments, fc.write() operations were still returning 0 (bytes written) after that had been solved.

Here is what I ended up with (no issues, except performatic ones, to which I'm working around to find the best tune).

    // Create random string
    StringBuilder sb = new StringBuilder();
    sb.ensureCapacity(8388608); // ensuring 8 MB is allocated on the heap for the StringBuilder object
    for (long i = 0; i < DATA_SIZE; i++) {
            char c = chars[new Random().nextInt(chars.length)];
            sb.append(c);
    }
    String output = sb.toString();
    sb = null;

    ByteBuffer randomByteStringBuffer = ByteBuffer.wrap(output.getBytes());

    ExecutorService executor = Executors.newFixedThreadPool(2);
    for (int i = 0; i < 2; i++) {
        Runnable worker = new SlidingBubbles(context, countRuns, originalTotalAvailable, totalBytesWritten, originalBytesAvailable, bubbleSize, randomByteStringBuffer);
        executor.execute(worker);
    }
    // This will make the executor accept no new threads and finish all existing threads in the queue 
    executor.shutdown();

    // Wait until all threads are finish 
    while(!executor.isTerminated()) {
        // wait for bubble threads to finish working...
    }

and the threads' routine...

private class SlidingBubbles implements Runnable {
    private int param1, param2;
    private ByteBuffer randomByteStringBuffer;
    private final Object mSignal = new Object();
    private volatile long tempBytesWritten = 0;
    private volatile long totalBytesWritten = 0;


    public SlidingBubbles(int param1, int param2, ByteBuffer randomByteStringBuffer) {
        this.param1= param1;
        this.param2= param2;
        this.randomByteStringBuffer = randomByteStringBuffer;
    }

    private void doIt() {
        File file = null;
        RandomAccessFile randomAccessFile = null;
        FileChannel fc = null;

        try {

            while(countRuns > 0) {
                // Instantiate the 1st bubble file
                file = new File(TARGET_DIR, String.valueOf(Calendar.getInstance().getTimeInMillis()));                    
                while(originalBytesAvailable > 0) {

                    randomAccessFile = new RandomAccessFile(file, "rw");
                    fc = randomAccessFile.getChannel();

                    fc.position(fc.size());
                    synchronized (mSignal) {
                        tempBytesWritten = fc.write(randomByteStringBuffer);

                        /* This rewind() is what did the trick. fc.write() started working normally again and writing 8MB.*/
                        randomByteStringBuffer.rewind();

                        totalBytesWritten += tempBytesWritten;
                    }
                 // some other things that don't matter
                }
             // some other things that don't matter
            }
        } catch (IOEception ioe) {
               Log.d(LOG_TAG, ioe.getMessage());
          }
    }

    @Override
    public void run() {
        wipe();            
    }
}

Hopefully this will be helpful to someone else as well in the future.

Nick Fanelli
  • 265
  • 1
  • 6
  • 19