Friday, September 21, 2007

VM errors handling.

Current post is dedicated to behavior of different implementations in extreme cases and particularly in case of stack overflow. JVM spec says (see http://java.sun.com/docs/books/jvms/second_edition/html/Concepts.doc.html#24870):

“The class Error and its standard subclasses are exceptions from which ordinary programs are not ordinarily expected to recover.”

but what VM behavior we can rely on that is the question. First of all let’s understand, that this is actually important. Everybody who writes some resource allocation or synchronization code could face with this problem. Let’s imagine that you have some code like this:




resource.acquire (); //Acquire some resources
//e.g. file handler or lock
Do some work here (which could include calls etc))
resource.release(); //Release resources



this code actually is not so good as might be. If some exception were thrown in main block the resources wouldn’t release which could lead to resource leakage or deadlock. To solve this let’s put resource releasing into the finally block.





try {
resource.acquire();
Do some work here...
} finally
resource.release();
}



Of course that doesn’t guarantee that resources are released, but it will be noticed in this case. To investigate the problem I’ve done some experiments on different implementations of Java 1.5 (Sun JRE 1.5 and JRockit JRE 1.5) and Apache Harmony (which is still haven’t got JCK to pass)

I started experiments with simple example:




public class SOETest1 {
public int i = 0;

public static void main (String[] args) {
SOETest1 test = new SOETest1();
try {
test.foo();
} finally {
System.out.println("The final i value is: " + test.i);
}
}

public void foo() {
try {
i++;
foo();
} finally {
i--;
}
}
}


In this example we have infinite recursion, which apparently will produce StackOverflowError at some step. It was expected that variable i is equal to 0 at the end of the test, but the situation is actually differs for different VMs. In fact SUN JRE 1.5 and Harmony behave as expected whereas JRockit JRE 1.5 value of i was 1025. It seems that in case of stack overflow JRockit silently unwinds a stack frame and thus skips execution for number of finally blocks. Definitely this is not the best way of doing things since one can’t rely on even simplest actions in finally blocks are done.

Then I've tried more complex example with method call in finally block:





import java.util.concurrent.locks.ReentrantReadWriteLock;

public class SOETest2 {
public static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
public static void main (String[] args) {
try {
Thread[] t = new Thread[2];
for (int i = 0; i < t.length; i++) {
t[i] = new SOEThread();
t[i].start();
t[i].join();
}
} catch (Exception e) {
} finally {
System.out.println("The end!");
}
}

static class SOEThread extends Thread {
public int i = 0;
public int j = 0;
public void run() {
System.out.println("Thread: " + this + " started");
try {
foo();
} finally {
System.out.println("Thread: " + this + " finished, final [i,j] values are: [" + i + "," + j + "]");
}
}

public void foo() {
try {
i++;
SOETest2.rwl.writeLock().lock();
j++;
foo();
} finally {
i--;
SOETest2.rwl.writeLock().unlock();
j--;
}
}
}
}




In this example we have 2 threads which acquire lock on the same object, lock is released in finally block. There are two counters i and j; i allows to check that finally block was executed for each foo()and j shows how many times unlock() was successfully called. If lock is released fewer times than obtained then the test gets into the deadlock.



The results for this test are quite expected:

  • SUN JRE 1.5 returns [i, j] = [0, 1]. j = 1 is o.k., because VM just got another SOE while trying to call unlock(). Second thread is failed to finish due to deadlock (since unlock() was called fewer times than lock()).


  • For JRockit JRE 1.5 [i, j] = [507, 507], this behavior conforms to the first case, JRockit again skips number of finally blocks (which produces deadlock situation) and then successfully executes method unlock().


  • Harmony returns [i, j] = [0, 53], throwing new SOE for the deepest unlock() calls. IMO this is the most correct behavior since finally block was executed exactly once for each foo() and new SOE was thrown for every unsuccessful call of unlock(). Anyway test still got into deadlock.


Deeper investigation showed that Harmony’s behavior could be improved. Such a huge number of failures during unlock() execution is due to the method compilation (method is compiled only at the first call). If compilation is accomplished earlier (it could be done by adding preliminary calls of unlock()) then Harmony works just fine. It returns [i, j] = [0, 0] and both threads are finished successfully. SUN’s and JRockit’s behavior hasn’t changed after such change.

This simple investigation shows that handling VM errors is not so straightforward. Different VMs have different and not always predictable behavior. JRockit JRE 1.5 in case of StackOverflowError could skip number of finally blocks which leads to unexpected results. SUN JRE 1.5 and Harmony showed predictable behavior, but still it doesn’t guarantee that calls in finally blocks will be successful, such cases must be handled in specific way.

1 comment:

Anonymous said...

Who knows where to download XRumer 5.0 Palladium?
Help, please. All recommend this program to effectively advertise on the Internet, this is the best program!