自恢复JVM

分享到:

bd255d41018a00afa720f143c5a00c1c.jpg

我们有一个应用程序,它能够正确地恢复自己,而不是需要关闭并重启。刚开始时它出现错误,但是一段时间后开始平稳运行。为了给出一个这种应用程序的例子,我们从Heinz Kabutz的Java简讯上五年前的一篇文章中获取灵感,尽可能简单地重新构造出了如下的实例:

package eu.plumbr.test;
	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 heap space错误,而且永远不会成功地执行allocateMemory()方法。

那么,让我们通过静态分析源代码,来看看我们的预期是否正确:

  1. 经过快速初步检查,这段代码确实不够完善,因为它尝试申请的内存超过了JVM中可用的数量。
  2. 如果我们仔细的检查就会注意到,第一次内存申请是在一个代码块中,它意味着这个代码块中定义的变量只在块中是可见的。这就表明,变量bytes在代码块结束时应该会被GC回收掉。所以,我们的代码实际上从一开始就能够正确运行,因为当尝试申请moreBytes内存时,前面申请的内存bytes应该已经被销毁了。
  3. 如果我们查看编译后的class文件,将看到下面的字节码:
private static void allocateMemory(int);
Code:
0: getstatic
#3
// Field SIZE:I
3: newarray
byte
5: 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)V
14: getstatic
#3
// Field SIZE:I
17: newarray
byte
19: 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,开始申请另一个数组。但是第一个数组仍然被局部变量引用,因此第二次内存申请应该会因为内存溢出而一直失败。字节码解释器不能简单的让GC清除第一个数组,因为它仍然被引用。

我们的静态代码分析显示,由于两种潜在的原因上面的代码不应该成功运行;而在一种情况下,它应该成功运行。这3种情形中的哪一种是正确的呢?让我们运行代码来看看。结果表明,两种分析结果都是正确的。首先,应用程序申请内存失败了;但是过了一段时间后(在我的Mac OS X系统上、Java 8环境下出现在第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

它将产生一个log文件,在我们的例子中是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'/>

这行内容说明,在执行allocateMemory()方法256次之后,C1编译器决定将这个方法放到C1第3级别编译的队列中。你可以在这里了解更多编译级别和不同阀值的信息。所以,前256次循环中是运行在解释器模式,即一个简单的基于栈的字节码解释器。它不能预先知道哪些变量是否会在将来被用到,比如我们的例子中的bytes。但是JIT一次检查整个方法,并推断出变量bytes不再被使用,能够被GC回收。因此垃圾回收终于出现,我们的程序神奇地自我恢复了。现在,我只希望读者们没有人正在实际的产品中调试类似的问题。但是如果你希望让某个人悲剧一次,把类似的代码加入到产品中将会是一个“靠谱的”方法。

原文链接: javacodegeeks 翻译: ImportNew.com - shenggordon
译文链接: http://www.importnew.com/14137.html

昵    称:
验证码:

相关文档:

  • java反射机制实战示例分享_java
    这篇文章主要介绍了java反射机制实战示例,需要的朋友可以参考下...
  • Java代码生成平台:Sculptor
    Sculptor是一个简单但功能强大的代码生成平台,该平台提供了一个Quick Start来实现模型驱动软件开发(MDSD)。 Sculptor让你只需关心业务需求...
  • java string 转date方法如何实现_java
    在开发应用中经常会使用到java string 转date这种不是很常见的做法,本文将以此问题提供详细解决方案,需要了解的朋友可以参考下...
  • Java表达式引擎 Aviator
    Aviator是一个高性能、轻量级的基于java实现的表达式引擎,它动态地将String类型的表达式编译成Java ByteCode并交给JVM执行。...
  • Java使用String类格式化当前日期实现代码_java
    这篇文章主要介绍了Java使用String类格式化当前日期实现代码,需要的朋友可以参考下...
  • Java 资源本地化与国际化
    在编写应用程序的时候,需要面对的一个问题是如何来处理与locale相关的一些信息。比如,页面上的一些静态文本就希望能够以用户习惯...
  • Java正则多字符串匹配替换_java
    正则表达式异常强大,一直理解不深,用的也不深,这次项目中尝试,体会到了它的强大之处。字符串查找,匹配,替换,正则无不能做...
  • Java5 并发学习
    在Java5之后,并发线程这块发生了根本的变化,最重要的莫过于新的启动、调度、管理线程的一大堆API了。在Java5以后,通过 Executor来启...
  • Java线程中断的本质深入理解_java
    Java的中断是一种协作机制。也就是说调用线程对象的interrupt方法并不一定就中断了正在运行的线程,它只是要求线程自己在合适的时机...
  • 归并排序的实现代码与思路_java
    归并排序是建立在归并操作上的一种有效的排序算法。该算法是采用分治法(Divide and Conquer)的一个非常典型的应用。...
  • java多线程中的异常处理机制简析_java
    在java多线程程序中,所有线程都不允许抛出未捕获的checked exception,也就是说各个线程需要自己把自己的checked exception处理掉,需要了解的...
  • java类加载全过程
    一个java文件从被加载到被卸载这个生命过程,总共要经历4哥阶段: 加载->链接(验证+准备+解析)->初始化(使用前的准备)->...
  • java读取文件字符集示例方法_java
    这篇文章主要介绍了java读取文件字符集的示例,需要的朋友可以参考下...
  • Java ArrayList 和 HaspMap 链式添加的实现
    在application中ArrayList 和 HaspMap 这两个类是经常用到的。而且,一般需要处理的数据量也不会少,因为这两个类是没有实现链式添加元素...
  • Java调用WebService接口的方法_java
    这篇文章主要介绍了Java调用WebService接口的方法,实例分析了有参方法Add的使用技巧,需要的朋友可以参考下...
  • java加密枝术深入理解_java
    java.security包中的MessageDigest类提供了计算消息摘要的方法,本文将详细介绍,需要了解的朋友可以参考下...
  • Java对象的内存模型
    众所周知,函数调用在内存中是通过压栈,退栈实现的,而Java的方法调用则是在JVM栈中通过栈帧实现的,且所有的Java对象都只在堆上分...
  • java socket编程实例代码讲解_java
    这篇文章主要介绍了java socket编程示例讲解,大家参考使用吧...
  • Java设计模式之装饰模式(Decorator模式)介绍_java
    这篇文章主要介绍了Java设计模式之装饰模式(Decorator模式)介绍,本文讲解了为什么使用Decorator、如何使用装饰模式、Jive中的Decorator实现...
  • java代理 jdk动态代理应用案列_java
    java代理有jdk动态代理、cglib代理,这里只说下jdk动态代理,jdk动态代理主要使用的是java反射机制,需要了解的朋友可以参考下...