如下代码:
public class Example045 { public static void main(String[] args) { long start = System.currentTimeMillis(); workHard(); System.out.println("递归花费时间:" + (System.currentTimeMillis() - start) / 1000.0); } private static void workHard() { try { workHard(); } finally { workHard(); } } }
结果说明:
要不是有 try-finally 语句,该程序的行为将非常明显: workHard 方法递归地调用它自身, 直到程序抛出 StackOverflowError,在此刻它以这个未捕获的异常而终止。但是,try-finally 语句把事情搞得复杂了。当它试图抛出StackOverflowError 时,程序将会在 finally 语句块的 workHard 方法中终止,这样,它就递归调用了自己。这看起来确实就像是一个无限循环的秘方,但是这个程序真的会无限循环下去吗?如果你运行它,它似乎确实是这么做的,但是要想确认的唯一方式就是分析它的行为。
代码分析:
Java 虚拟机对栈的深度限制到了某个预设的水平。当超过这个水平时, VM 就抛出 StackOverflowError。为了让我们能够更方便地考虑程序的行为, 我们假设栈的深度为 3(这比它实际的深度要小得多)。 现在让我们来跟踪其执行过程。main 方法调用 workHard,而它又从其 try 语句块中递归地调用了自己,然后它再一次从其 try 语句块中调用了自己。在此时,栈的深度达到3。当 workHard 方法再次试图从其 try 语句块中再次调用自己时,该调用立即就会以StackOverflowError 而失败。这个错误是在最内部的 finally 语句块中被捕获的,在此处栈的深度已经达到了 3。在那里,workHard 方法试图递归地调用它自己,但是该调用却以 StackOverflowError 而失败。这个错误将在上一级的finally 语句块中被捕获,在此处栈的深度是 2。该 finally 中的调用将与相对应的 try 语句块具有相同的行为:最终都会产生一个 StackOverflowError。
下图详细描述了上述程序的执行过程:
这张图展示了一个深度为 0 的调用(即 main 中的调用), 两个深度为 1 的调用,四个深度为 2 的调用,和八个深度为 3 的调用,总共是 15 个调用。那八个深度为 3 的调用每一个都会立即产生 StackOverflowError。 至少在把栈的深度限制为 3 的 VM 上,该程序不会是一个无限循环:它在 15 个调用和 8 个异常之后就会终止。
但是对于真实的 VM 又会怎样呢?它仍然不会是一个无限循环。其调用图与前面的图相似,只不过要大得多得多而已。那么, 究竟大到什么程度呢?有一个快速的试验表明许多 VM 都将栈的深度限制为 1024,因此,调用的数量就是 1+2+4+8…+2^1024=2^1025-1,而抛出的异常的数量是 2^1024个。 假设我们的机器可以在每秒钟内执行 10^10 个调用,并产生 10^10个异常, 按照当前的标准,这个假设的数量已经相当高了 。在这样的假设条件下,程序将在大约 1. 7× 10^291 年后终止。为了让你对这个时间有直观的概念, 我告诉你, 我们的太阳的生命周期大约是 10^10 年,所以我们可以很确定, 我们中没有任何人能够看到这个程序终止的时刻。 尽管它不是一个无限循环,但是它也就算是一个无限循环吧。
从技术角度讲,调用图是一棵完全二叉树,它的深度就是 VM 的栈深度的上限。程序的执行过程等于是在先序遍历这棵树。在先序遍历中,程序先访问一个节点,然后递归地访问它的左子树和右子树。对于树中的每一条边,都会产生一个调用,而对于树中的每一个节点,都会抛出一个异常。
注:本【java解惑】系列均是博主阅读《java解惑》原书后将原书上的讲解和例子部分改编然后写成博文进行发布的。所有例子均亲自测试通过并共享在github上。通过这些例子激励自己惠及他人。同时本系列所有博文会同步发布在博主个人微信公众号搜索“爱题猿”或者“ape_it”方便大家阅读。如果文中有任何侵犯原作者权利的内容请及时告知博主以便及时删除如果读者对文中的内容有异议或者问题欢迎通过博客留言或者微信公众号留言等方式共同探讨。
源代码地址https://github.com/rocwinger/java-disabuse