这篇帖子是关于一个应用程序的示例,其中解决每个IT问题的第一个解决方案(“您尝试过关闭并重新打开它”)可能适得其反,弊大于利。
我们没有关闭或重新打开设备的方法,而是拥有一个可以自愈的应用程序:它在一开始就失败了,但过了一段时间便开始平稳运行。 为了举例说明这种应用的实际应用,我们以最简单的形式重新创建了该应用, 并从Heinz Kabutz的Java Newsletter已有5年历史的帖子中汲取了灵感 :
package eu.plumbr.test;public class HealMe {private static final int SIZE = (int) (Runtime.getRuntime().maxMemory() * 0.6);public static void main(String[] args) throws Exception {for (int i = 0; i < 1000; i++) {allocateMemory(i);}}private static void allocateMemory(int i) {try {{byte[] bytes = new byte[SIZE];System.out.println(bytes.length);}byte[] moreBytes = new byte[SIZE];System.out.println(moreBytes.length);System.out.println("I allocated memory successfully " + i);} catch (OutOfMemoryError e) {System.out.println("I failed to allocate memory " + i);}}
}
上面的代码在一个循环中分配两个大块内存。 这些分配中的每一个都等于总可用堆大小的60%。 由于分配是在同一方法中按顺序进行的,因此人们可能希望此代码不断抛出java.lang.OutOfMemoryError:Java堆空间错误,并且永远不会成功完成allocateMemory()方法。
因此,让我们从对源代码的静态分析开始,看看我们的期望是否正确:
- 从第一次快速检查起,该代码确实无法完成,因为我们尝试分配的内存超过了JVM可用的内存。
- 如果我们仔细观察,我们会注意到第一次分配发生在有作用域的块中,这意味着在此块中定义的变量仅对该块可见。 这表明在完成块后,这些字节应符合GC的条件。 因此,我们的代码实际上应该从一开始就可以正常运行,因为当它尝试分配更多 字节时 ,先前的分配字节应该已失效。
- 如果现在查看编译的类文件,将看到以下字节码:
private static void allocateMemory(int);Code:0: getstatic #3 // Field SIZE:I3: newarray byte5: astore_1 6: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;9: aload_1 10: arraylength 11: invokevirtual #5 // Method java/io/PrintStream.println:(I)V14: getstatic #3 // Field SIZE:I17: newarray byte19: astore_1 20: getstatic #4 // Field java/lang/System.out:Ljava/io/PrintStream;23: aload_1 24: arraylength 25: invokevirtual #5 // Method java/io/PrintStream.println:(I)V
---- cut for brevity ----
在这里我们看到,在偏移量3-5上,第一个数组被分配并存储到索引为1的局部变量中。然后,在偏移量17上,另一个数组将被分配。 但是第一个数组仍由局部变量引用,因此第二个分配应始终因OOM而失败。 字节码解释器只是不能让GC清理第一个数组,因为它仍然被强烈引用。
我们的静态代码分析向我们表明,由于两个根本原因,所提供的代码不应成功运行,而在一种情况下,应该可以成功运行。 这三者中哪一个是正确的? 让我们实际运行它,自己看看。 事实证明,这两个结论都是正确的。 首先,应用程序无法分配内存。 但是一段时间后(在我的Java 8的Mac OS X上,它发生在第255次迭代中),分配开始成功:
java -Xmx2g eu.plumbr.test.HealMe
1145359564
I failed to allocate memory 0
1145359564
I failed to allocate memory 1… cut for brevity ...I failed to allocate memory 254
1145359564
I failed to allocate memory 255
1145359564
1145359564
I allocated memory successfully 256
1145359564
1145359564
I allocated memory successfully 257
1145359564
1145359564
Self-healing code is a reality! Skynet is near...
为了了解实际发生的事情,我们需要思考一下,程序执行期间会发生什么变化? 当然,显而易见的答案是可以进行即时编译。 您还记得吗,即时编译是JVM的一种内置机制,可以优化代码热点。 为此,JIT监视正在运行的代码,并且在检测到热点时,JIT会将您的字节码编译为本机代码,在过程中执行不同的优化,例如方法内联和消除无效代码。
通过打开以下命令行选项并重新启动程序,看看是否是这种情况:
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -XX:+LogCompilation
这将生成一个日志文件,在本例中为hotspot_pid38139.log,其中38139是Java进程的PID。 在此文件中,可以找到以下行:
<task_queued compile_id='94' method='HealMe allocateMemory (I)V' bytes='83' count='256' iicount='256' level='3' stamp='112.305' comment='tiered' hot_count='256'/>
这意味着,在执行256次allocateMemory()方法后,C1编译器已决定将该方法排队以进行C1层3编译。 您可以在此处获得有关分层编译级别和不同阈值的更多信息。 因此,我们的前256次迭代是在解释模式下运行的,在该模式下,字节码解释器(作为简单的堆栈计算机)无法预先知道是否会继续使用某些变量(在这种情况下为字节)。 但是JIT可以立即看到整个方法,因此可以推断出不再使用字节,并且实际上可以使用GC。 因此,垃圾收集最终可以发生,并且我们的程序神奇地自我修复了。 现在,我只希望没有读者真正负责在生产中调试这种情况。 但是,如果您希望使某人的生活陷入困境,那么将这样的代码引入生产环境将是实现此目标的肯定方法。
翻译自: https://www.javacodegeeks.com/2014/12/self-healing-jvm.html