Java Outofmemoryerror Strange Behaviour

Java OutOfMemoryError: Strange behavior

The second version will also run out of memory eventually. (If you don't get bored and kill it first.)

The problem is that the print statement is repeatedly printing a list that is getting longer and longer.

By the time you have added N items to the list in the second version, you will have printed lists of sizes 1 + 2 + 3 + ... + N. That is N * (N + 1) / 2 copies of the string "My String" ... and some more. That is O(N^2).

If you are writing that to a /dev/null it will take a long time. If you write it to a file, longer. If you write it to a console displaying on your screen ... come back in a few hours.


As an experiment, replace

    System.out.println(myList);

with

    System.out.print("X");

Now you will only print O(N) characters instead of O(N^2) ... and the OOME will happen a lot sooner.

Java OutOfMemoryError strange behaviour

To keep things in perspective, consider running this code with -Xmx64m:

static long sum;
public static void main(String[] args) {
System.out.println("Warming up...");
for (int i = 0; i < 100_000; i++) test(1);
System.out.println("Main call");
test(5_500_000);
System.out.println("Sum: " + sum);
}

static void test(int size) {
// for (int i = 0; i < 1; i++)
{
long[] a2 = new long[size];
sum += a2.length;
}
long[] a1 = new long[size];
sum += a1.length;
}

Depending on whether you do the warmup or skip it, it will blow or not blow. This is because the JITted code properly nulls out the var, whereas the interpreted code doesn't. Both behaviors are acceptable under the Java Language Specification, which means that you are at the mercy of the JVM with this.


Tested with Java HotSpot(TM) 64-Bit Server VM (build 23.3-b01, mixed mode) on OS X.

Bytecode analysis

Look at the bytecode with the for loop (simple code, without the sum variable):

static void test(int);
Code:
0: iconst_0
1: istore_1
2: goto 12
5: iload_0
6: newarray long
8: astore_2
9: iinc 1, 1
12: iload_1
13: iconst_1
14: if_icmplt 5
17: iload_0
18: newarray long
20: astore_1
21: return

and without:

static void test(int);
Code:
0: iload_0
1: newarray long
3: astore_1
4: iload_0
5: newarray long
7: astore_1
8: return

No explicit nulling out in either case, but note that in the no-for example the same memory location is actually reused, in contrast with the for example. This would, if anything, lead to the expectation opposite to the observed behavior.

A twist...

Based on what we learned from the bytecode, try running this:

public static void main(String[] args) {
{
long[] a1 = new long[5_000_000];
}
long[] a2 = new long[0];
long[] a3 = new long[5_000_000];
}

No OOME thrown. Comment out the declaration of a2, and it is back. We allocate more, but occupy less? Look at the bytecode:

public static void main(java.lang.String[]);
Code:
0: ldc #16 // int 5000000
2: istore_1
3: ldc #16 // int 5000000
5: newarray long
7: astore_2
8: iconst_0
9: newarray long
11: astore_2
12: ldc #16 // int 5000000
14: newarray long
16: astore_3
17: return

The location 2, used for a1, is reused for a2. The same is true for OP's code, but now we overwrite the location with a reference to an innocuous zero-length array, and use another location to store the reference to our huge array.

To sum it up...

Java Language Specification does not specify that any garbage object must be collected and the JVM spec only says that the "frame" with local variables is destroyed as a whole upon method completion. Therefore all behaviors that we have witnessed are by the book. The invisible state of an object (mentioned in the document linked to by keppil) is just a way to describe what happens to go on in some implementations and under some circumstances, but is in no way any kind of canonical behavior.

Behavior of a Java process in case of OutOfMemoryError

And OutOfMemoryError is handled like any other exception:

  • If it is caught, then nothing more happens.
  • If it is not caught, then either the threads or the threads groups uncaught exception handler handles it. This pretty much always leads to the thread being stopped.

However there are two factors that are not really there in other exceptions:

  • OutOfMemoryError is an Error and not an Exception. This means that it's very unlikely to be caught anywhere: You should not try to catch an Error generally (with very few exceptions) and it's not usually done, so the chances of it being handled are rather low.
  • When an OutOfMemoryError happens and no object become eligible for GC because of that, then you'll still have little memory left and chances are that you'll run into the exact same problem again later on.

And if the thread this happens to is the only non-daemon thread (often, but not necessarily, that's the main thread, that executes the main method), then that thread getting killed results in the whole JVM shutting down (which is often perceived as "a crash").

So the tl;dr is: It will probably kill the thread, and if the memory-issue is not solved, then this can happen to more and more threads.

Very strange OutOfMemoryError

I want to close this question with a summary of the current state.

The last test is now up over 60 hours without any problems. That leads us to the following summary/conclusions:

  • We have a high throughput server using lots of objects that in the end implement "finalize". These objects are mostly JNA memory handles and file streams. Building the Finalizers faster than GC and finalizer thread are able to clean up, this process fails after ~3 hours. This is a well known phenomenon (-> google).
  • We did some optimizations so the server got rid of nearly all the JNA Finalizers. This version was tested with jProfiler attached.
  • The server died some hours later than our initial attempt...
  • The profiler showed a huge amount of finalizers, this time caused mostly only by file streams. This queue was not cleaned up, even after pausing the server for some time.
  • Only after manually triggering "System.runFinalization()", the queue was emptied. Resuming the server started to refill...
  • This is still inexplicable. We now guess this is some profiler interaction with GC/finalization.
  • To debug what could be the reason for the inactive finalizer thread we detached the profiler and attached the debugger this time.
  • The system was running without noticeable defect... FinalizerThread and GC all "green".
  • We resumed the test (now for the first time again without any agents besides jConsole attached) and its up and fine now for over 60 hours. So apparently the initial JNA refactoring solved the issue, only the profiling session added some indeterminism (greetings from Heisenberg).

Other strategies for managing the finalizers are for example discussed in http://cleversoft.wordpress.com/2011/05/14/out-of-memory-exception-from-finalizer-object-overflow/ (besides the not overly clever "don't use finalizers"..).

Thank's for all your input.

OutOfMemoryError is dangerous, but why more dangerous than other exceptions?

But... that's not exclusive to OutOfMemoryError. Any exception may have left invalid state if it's not properly handled.

Different definitions.

Normal exceptions (i.e. everything except InternalError and OOME) have well defined behaviour: Either the code (by executing the statement throw someException;), or the JVM itself (for example by executing foo.instanceMethod where foo resolves to null, causing the JVM to throw an NPE), throws an exception. That means at the point the exception is thrown, execution of that context stops and execution jumps to the relevant catch block, or if there is none, that method is aborted and the caller now gets this exception thrown, and so on.

This does not cause invalid state unless you suck at programming. Unfortunately, a ton of SO answers and tutorials suck at programming, they do this:

try {
some code here;
} catch (IOException e) {
e.printStackTrace();
}

... which leaves code in an invalid state: The problem is, execution continues after that catch block, which is bad. The right way to write code that doesn't want to care about some exception is instead:

try {
some code here;
} catch (IOException e) {
throw new RuntimeException("unhandled", e);
}

That is merely ugly, vs the e.printStackTrace() version, which is downright broken. Update your IDE's templates!

At any rate, that is what you mean here with 'undefined behaviour' - crappy code. You can say that about any code: Well, that code COULD have bugs, so, what does it do? No body knows! - yeah, okay, but take that argument to its logical conclusion and your only remaining option is to toss your computer out the window and go live with the amish.

Fortunately, the java.* classes and all well-written libraries (most commonly used libraries are well written in this sense) do not do the above - if they throw stuff, the behaviour is not 'undefined'. Generally, all operations are atomic: Either it works, or, it did absolutely nothing and instead threw an exception (vs the above crappy code which acts like it works, in the sense that the method returns normally, but there's a stack trace on your console and the method did not do what its javadoc says it should be doing).

What the community means specifically when they say that OOME causes undefined behaviour is that OOME is a special snowflake, in this regard:

OOME can occur 'within' otherwise protected execution contexts: Unlike almost any other exception, it can be thrown inside the innards of the JVM that aren't expecting to deal with exceptions, and thus, it leaves e.g. a file handle half-open and in an undefined fragile state (where interacting with that handle causes bizarre exceptions or worse, undefined behaviours), and so on.

That's the difference. If, say, writing to an OutputStream you got from Files.newOutputStream causes some IOException, then behaviour is still well defined. Nothing changed in this record. Normally, given:

try (var out = Files.newOutputStream(.. path ..)) {
out.write(5);
out.write(11);
}

At the .write(5) command, either [A] that returns normally at which point you know that the '5' is at the very least in the OS's core file caches (no longer in your JVM's file caches), or [B] it throws an IOException that will accurately represent the issue at hand.

Let's say that .write(5) threw some exception, you caught it, and then you want to just continue to the .write(11) line. Nothing changed: You STILL have the same setup: Either that does work (let's say the exception was: You have no write access, and in the catch of that you prompt the user to fix it and sleep the thread for a while, and the user actually sudo chmod fixes it, then depending on how the OS and java works that might mean future .write calls will now work just fine!) - or most likely it won't, but you still get exceptions 'as expected': IOExceptions that properly describe the problem.

In contrast to OOME: If .write(5) throw an OOM, then .write(11) may cause your computer to blast Beethoven's Ode to Joy from the speakers as far as you know - the system is in an unstable state and the OutputStream you have may now behave in ways that the javadoc do not cover.

Note that this isn't universal. Not all OOMEs lead to an 'unstable' system. For example, this code is perfectly fine:

byte[] bigArray;
try {
bigArray = new byte[100000000];
} catch (OutOfMemoryError e) {
// handle the fact that you can't make em that big
}

Essentially, if any method invoke throws OOME you need to accept that various things can now be in a state that the javadoc do not describe and whose behaviour is dependent on all sorts of weird factors (OS, JVM vendor, java version, phase of the moon...), but a few primitives, mostly the new arr[size] operation, you know what happened and can reliably respond to the OOME.

Strange behavior of break inside while loop

Simplify a bit and eliminate the unnecessary overhead of the grep.

while : # : is a synonym for true
do echo "Starting tail-Loop"
while read line
do echo "Inside tail-Loop"
case "$line" in
*java.lang.OutOfMemoryError*)
echo "Error! java.lang.OutOfMemoryError"
# restart tomcat here
break
;;
esac
done < <( tail -fn0 ${LOGFILE} )
echo "Left tail-Loop"
done


Related Topics



Leave a reply



Submit