标签归档:jvm

使用strace定位jvm退出的原因范围

今天遇到的一个tomcat启动过程中jvm退出的问题,不是jvm crash的情况,用户日志配置的不正确导致一些信息没有展现出来,只看到pandora执行了shutdownhook的信息。这可能是启动时的逻辑有触发System.exit,或被系统或人为kill掉了。

根据以往的经验,排除了oom killer或ulimit -t设置不当导致被内核给kill掉的情况,OS级别的signal通常不留机会给jvm执行shutdownhook的。如此一来singal的范围应该就是SIGTERM, SIGINT, SIGHUP这3种(参考这里)。

虽然singal范围缩小,但依然不能确定是因为代码里调用了System.exit还是人为(或被其他进程)kill引起的。直接上大招用systemtap需要安装kernal debuginfo,没有权限的话,还要找到对应的人去做;如果现象较容易重现的话,可以先通过strace命令进一步缩小问题的范围,究竟是因为jvm内部执行了System.exit还是外界的kill引起的。

这里通过启动一个scala的repl来模拟java进程,通过strace attach到jvm进程上,然后观察,如果是外界的kill所致,可以看到下面的信息:

$ sudo strace -p 1947
Process 1947 attached - interrupt to quit
futex(0x7fb7635959d0, FUTEX_WAIT, 1948, NULL) = ? ERESTARTSYS (To be restarted)
--- SIGTERM (Terminated) @ 0 (0) ---
futex(0x7fb762762360, FUTEX_WAKE_PRIVATE, 1) = 1
rt_sigreturn(0x7fb762762360)            = 202
futex(0x7fb7635959d0, FUTEX_WAIT, 1948, NULLPANIC: attached pid 1947 exited with 143
 <unfinished ... exit status 143>

里面的关键信息是SIGTERMexit status 143(即SIGTERM的code)

如果是kill -2或ctrl-c终止repl,可以看到有关SIGINT的信息

$ sudo strace -p 1813
Process 1813 attached - interrupt to quit
futex(0x7fb24d15a9d0, FUTEX_WAIT, 1814, NULL) = ? ERESTARTSYS (To be restarted)
--- SIGINT (Interrupt) @ 0 (0) ---
futex(0x7fb24c327360, FUTEX_WAKE_PRIVATE, 1) = 1
rt_sigreturn(0x7fb24c327360)            = 202
futex(0x7fb24d15a9d0, FUTEX_WAIT, 1814, NULLPANIC: attached pid 1813 exited with 130
 <unfinished ... exit status 130>

如果是jvm自身执行了System.exit比如:

scala> System.exit(0)

那么在跟踪的信息里,是看不到signal的:

$ sudo strace -p 2131
Process 2131 attached - interrupt to quit
futex(0x7fc14adb49d0, FUTEX_WAIT, 2132, NULLPANIC: attached pid 2131 exited with 0
 <unfinished ... exit status 0>

至此我们可以判断出到底是外部还是内部引起的了,如果是内部就不必麻烦Systemtap了,可以从源码去找。

jvm与系统信号(4)

stop 与 cont 信号

这两个信号对于jvm也是可用的,比如让jvm进程停止:

scala> var a=0;

scala>  while(true) { Thread.sleep(2000); println(a); a=a+1 }
0
1
2
3

$ ps -ostat -p `pidof java`
STAT
S+

$ kill -s stop `pidof java`

此时jvm进程被暂停住,进程状态也变为:T (TASK_STOPPED or TASK_TRACED),暂停状态或跟踪状态

$ ps -ostat -p `pidof java`
STAT
T+

发送cont信号恢复进程状态:

$ kill -s cont `pidof java`

repl端会继续输出。

jvm与系统信号(3)

一些相关的参数

1) -Xrs

man java里可以看到这个参数的介绍,大意如下:

这个参数是在java1.3.1 之后增加的,rs是reduce signal的缩写,即忽略系统信号。在java1.3.0添加了 Shutdown Hook,目的是用于在jvm关闭时清除一些代码(比如关闭数据库连接)。对于jvm非正常退出,Sun/Oracle的jvm通过捕获信号来实现shutdown hook。JVM使用 SIGHUP, SIGINT,SIGTERM 来初始化 shutdown hook

JVM使用了相似的机制来实现 pre-1.2 特性,dumping线程栈(用于调试目的)。Sun/Oracle的JVM使用 SIGQUIT 来执行 线程 dump.

应用时常也需要自己捕获SIGINT或SIGTERM,这会导致干扰JVM自己的signal handler,为了避免这种情况, -Xrs 命令行参数在java1.3.1里被增加了进来。当使用 Sun的JVM时, SIGINT, SIGTERM, SIGHUP, SIGQUIT不会被影响JVM,这些信号的handler不会被install。

使用-Xrs要注意这2个后果:

1) SIGQUIT 不再产生 thread dump
2) Shutdown hook将不被执行

尝试一下Xrs参数,使用scala -J-Xrs启动一个repl:

$ scala -J-Xrs

$ kill -3 `pidof java` 

这个时候确实不会产生 thread dump,并且java进程会退出。

有趣的是,我使用 jstack 依然可以看到 scala repl的所有线程,也就是jstack依然会把请求发送到java进程,即使它设置了-Xrs,可能与attach机制有关,这里的细节以后再展开。

2) -XX:-AllowUserSignalHandlers

限于Linux和Solaris,默认不启用。允许为java进程安装信号处理器,信号处理参见类:sun.misc.Signal, sun.misc.SignalHandler

3) -XX:+UseAltSigs

限于Solaris,默认启用。为了防止与其他发送信号的应用程序冲突,允许使用候补信号替代 SIGUSR1和SIGUSR2

jvm与系统信号(2)

core dump 与 thread stack dump

进程处理信号的行为参考这里,很多信号都将导致core dump,比如SIGILL, SIGSEGV等。

比如我们对一个java进程发送SIGILL会让进程退出,并产生core dump:

$ kill -s ILL `pidof java`

在java进程的错误输出流会产生如下信息,并且在home下产生core dump文件:hs_err_pid8385.log

# A fatal error has been detected by the Java Runtime Environment:
#
#  SIGILL (0x4) at pc=0x00007fff89d1ba1a, pid=8385, tid=1287
#
# JRE version: Java(TM) SE Runtime Environment (7.0_60-b15) (build 1.7.0_60-ea-b15)
# Java VM: Java HotSpot(TM) 64-Bit Server VM (24.60-b09 mixed mode bsd-amd64 compressed oops)
# Problematic frame:
# C  [libsystem_kernel.dylib+0x11a1a]  mach_msg_trap+0xa
#
# Failed to write core dump. Core dumps have been disabled. To enable core dumping, try "ulimit -c unlimited" before starting Java again
#
# An error report file with more information is saved as:
# /Users/hongjiang/hs_err_pid8385.log
#
# If you would like to submit a bug report, please visit:
#   http://bugreport.sun.com/bugreport/crash.jsp
#
/data/tools/scala/bin/scala: line 21:  8385 Abort trap: 6           "$@"

对于SIGQUIT(kill -3),jvm会捕获该信号并dump线程栈到标准错误流,不会产生core-dump文件;这个信号的处理方式是系统保留用户无法修改:

scala> import sun.misc._

scala>  Signal.handle(
 |          new Signal("QUIT"),
 |          new SignalHandler(){ 
 |              def handle(sig:Signal){ println("down") }
 |          }
 |      )
java.lang.IllegalArgumentException: Signal already used by VM or OS: SIGQUIT
at sun.misc.Signal.handle(Signal.java:166)
... 38 elided

$ kill -s QUIT `pidof java`

会看到repl输出线程堆栈信息以及heap信息,java进程并不退出。

可被用户设定的singals

对于SIGSEGV(kill -11), SIGFPE(kill -8),SIGILL(kill -4), SIGUSR1(kill -10) 等信号,无法被用户设置:

scala> Signal.handle(new Signal("USR1"), 
 |              new SignalHandler(){ def handle(sig:Signal){ println("down") }}
 |      )

java.lang.IllegalArgumentException: Signal already used by VM or OS: SIGUSR1
    at sun.misc.Signal.handle(Signal.java:166)
    ... 32 elided

对于SIGINT(kill -2),SIGTERM(kill -15),SIGUSR2(kill -12),SIGBUS(kill -7),SIGPIPE(kill -13), 可以被用户设置:

scala> Signal.handle(new Signal("INT"), 
 |              new SignalHandler(){ def handle(sig:Signal){ println("down") }}
 |      )

$ kill -s INT `pidof java`

scala> down // 被捕获,进程不会退出

触发shutdown hook的singals

SIGTERM(kill), SIGINT(kill -2), SIGHUP(kill -1) 会触发shutdown hook的执行。

scala>  Runtime.getRuntime().addShutdownHook(
            new Thread() { override def run() { println("ok") } })

$ kill -s INT `pidof java`

scala> ok 

jvm与系统信号(1)

下面是singal部分列表(POSIX.1-1990):

SIGHUP 1 Terminal line hangup
SIGINT 2 Interrupt program
SIGQUIT 3 Quit program
SIGILL 4 Illegal instruction
SIGABRT 6 Abort
SIGFPE 8 Floating-point exception
SIGKILL 9 Kill program
SIGSEGV 11 Invalid memory reference
SIGPIPE 13 Write on a pipe with no one to read it
SIGALRM 14 Alarm clock
SIGTERM 15 Termination signal
SIGUSR1 30,10,16 User-defined signal 1
SIGUSR2 31,12,17 User-defined signal 2
SIGCHLD 20,17,18 Child stopped or terminated
SIGCONT 19,18,25 Continue if stopped
SIGSTOP 17,19,23 Stop process

其中有多个值的singal,它的含义是与硬件体系相关,参考这里:一般alpha和sparc架构用第一个值,x86,arm架构用中间值,mips架构用第三个值, – 表示相应架构的取值未知。

比如SIGUSR1这个信号,有30,10,16 三种值,在x86上会用第二个值,也就是10.

$ /bin/kill -L | xargs -n2 | grep USR1
10 USR1

后来在POSIX.1-2001有部分新增或修改:

SIGTRAP 5 Trace trap
SIGBUS 10,7,10 Bus error (bad memory access)
SIGSYS 12,31,12 Bad system call
SIGURG 16,23,21 Urgent condition on socket (4.2BSD)
SIGXCPU 24,24,30 CPU time limit exceeded (4.2BSD)

现在看一下jvm里对singal的处理,参考这里 (The mention “optional” means that the signal is not necessary when the -Xrs option is specified):

Signal Description
SIGSEGV, SIGBUS, SIGFPE,
SIGPIPE, SIGILL
Used in the implementation for implicit null check, and so forth.
SIGQUIT Thread dump support: To dump Java stack traces at the standard error stream. (Optional.)
SIGTERM, SIGINT, SIGHUP Used to support the shutdown hook mechanism ( java.lang.Runtime.addShutdownHook) when the VM is terminated abnormally. (Optional.)
SIGUSR1 Used in the implementation of the java.lang.Thread.interrupt method. (Configurable.) Not used starting with Solaris 10 OS. Reserved on Linux.
SIGUSR2 Used internally. (Configurable.) Not used starting with Solaris 10 OS.
SIGABRT The HotSpot VM does not handle this signal. Instead it calls the abort function after fatal error handling. If an application uses this signal then it should terminate the process to preserve the expected semantics.

二月份杭州GreenTea JUG的收获

5.25号的greentea活动有事没去参加,这是2月份那次greentea杭州的活动记录,当时有两个会场,只听了一个会场的。

昨天下午去华星时代广场参加了greentea java用户组,听了周忱的《java程序员也要懂cpu》
莫枢的《Intrinsic Methods in Hotspot VM》,以及王铮的《有关jvm profile的工作》还是很有收获的。
因为相对偏底层,平时不太涉及,尤其王铮的分享,传递了一个观念,硬件的设计与软件是很有差异的;在软件中的性能问题在硬件中或许根本不存在,硬件天生是并行的,软件则是串行的。

Intrinsic方法简单的说就是jvm对某些声明为了intrinsic的方法进行特殊的处理,不按照java里提供的代码逻辑或者jni里的实现,而是按照特定平台优化后的指令来处理,比如System.arraycopy 虽然声明为native(让人误以为可能比java实现更慢),但实际它是一个intrinsic的方法,它实际比你自己在java中来实现数组拷贝要更高效的。甚至在vm里用同样的实现逻辑,被标记为intrinsic的也依然可能比未标记intrinsic的要高效。

jdk中有哪些Intrinsic方法,可以在vmSymbols文件里找到,但温绍说他测试某些intrinsic的方法反而慢了
这个需要在特定的平台,特定的cpu,需要几个条件都符合,而目前并没有每个版本的jdk中哪些intrinsic的详尽描述的文档。

莫枢除了对Intrinsic方法的介绍,还回答了一下避免让两个数据在同一个cache line在java8里的实现:采用
@Contented 注释来避免false sharing的问题,不必在程序中写丑陋的padding 变量来实现了。

另外对Hotspot去除permgen的进度,需要到java8才会去除了。还纠正了我对hotspot jdk7中String.intern一个误区;String.intern 并不是直接存放在老生代,是在heap中,依然会经过新生代到老生代的过程,在young gc后才移到老生代。这也是对permgen移除的一部分工作,在jdk7里先release了这部分。

莫枢还演示了一个工具CLHSDB,除了普通的定位内存地址,还可以采用js来实现扩展它的命令。

分享ppt: jvm内存管理

去年分享的,里面的一些参数与当前线上的实际情况可能已对不上,仅供参考。

slideshare下载,或微盘下载

希望你能借鉴里面使用scala repl(再配合其他工具如jconsole)来做jvm参数调节的测试,比如:

repl很适合做诸如此类的测试。

话说ReferenceQueue

也是几年前写的,在内部邮件列表里发过,在这里保存一下。

看到了这篇帖子: 《WeakHashMap的神话》http://www.javaeye.com/topic/587995
因为Javaeye回帖还要先做个论坛小测验,所以懒得在上面回复了,在这里说下。

以前设计缓存时也曾过用WeakHashMap来实现,对Java的Reference稍做过一些了解,其实这个问题,归根到底,是个Java GC的问题,由垃圾回收器与ReferenceQueue的交互方式决定的。WeakHashMap的实现也是通过ReferenceQueue这个“监听器”来优雅的实现自动删除那些引用不可达的key的。

先看看ReferenceQueue在Java中的描述:

Reference queues, to which registered reference objects are appended by the garbage collector after the appropriate reachability changes are detected.
中文JavaDoc的描述:引用队列,在检测到适当的可到达性更改后,垃圾回收器将已注册的引用对象添加到该队列中

查看源代码会发现它很简单,实现了一个队列的入队(enqueue)和出队(poll还有remove)操作,内部元素就是泛型的Reference,并且Queue的实现,是由Reference自身的链表结构所实现的。

再来看 Reference类的代码,注意,javadoc中有一句,提到了它与GC是紧密相关的:

Because reference objects are implemented in close cooperation with the garbage collector, this class may not be subclassed directly.

从数据结构上看,Reference链表结构内部主要的成员有

private T referent; //就是它所指引的
Reference next;  //指向下一个;

另一个比较重要的内部数据是:

ReferenceQueue<? super T> queue;

这个queue是通过构造函数传入的,表示创建一个Reference时,要将其注册到那个queue上。

Queue的另一个作用是可以区分不同状态的Reference。Reference有4种状态,不同状态的reference其queue也不同:

  1. Active:

     queue = ReferenceQueue with which instance is registered,
     or ReferenceQueue.NULL if it was not registered with a queue; next = null.
    
  2. Pending:

     queue = ReferenceQueue with which instance is registered;
     next = Following instance in queue, or this if at end of list.
    
  3. Enqueued:

     queue = ReferenceQueue.ENQUEUED; next = Following instance 
     in queue, or this if at end of list.
    
  4. Inactive:

     queue = ReferenceQueue.NULL; next = this.
    

那么,当我们创建了一个WeakReference,并且将其referent改变后,究竟发生了什么?先看一段代码:

// eg1
public static void test() throws Exception{
    Object o = new Object();
    // 默认的构造函数,会使用ReferenceQueue.NULL 作为queue
    WeakReference<Object> wr = new WeakReference<Object>(o);
    System.out.println(wr.get() == null);
    o = null;
    System.gc();
    System.out.println(wr.get() == null);
}

结果大家都知道,但其内部是怎么实现的,还需重新看Reference的源码,内部有两点需要注意:

1)pending和 discovered成员:

先看pending对象

/* List of References waiting to be enqueued.  The collector adds
 * References to this list, while the Reference-handler thread removes
 * them.  This list is protected by the above lock object.
 */
private static Reference pending = null;

//这个对象,定义为private,并且全局没有任何给它赋值的地方,
//根据它上面的注释,我们了解到这个变量是和垃圾回收期打交道的。

再看discovered,同样为private,上下文也没有任何地方使用它

transient private Reference<T> discovered;    /* used by VM */
//看到了它的注释也明确写着是给VM用的。

上面两个变量对应在VM中的调用,可以参考openjdk中的hotspot源码,在hotspot/src/share/vm/memory/referenceProcessor.cpp 的ReferenceProcessor::discover_reference 方法。(根据此方法的注释由了解到虚拟机在对Reference的处理有ReferenceBasedDiscoveryRefeferentBasedDiscovery两种策略)

2)ReferenceHandler线程

这个线程在Reference类的static构造块中启动,并且被设置为高优先级和daemon状态。此线程要做的事情,是不断的检查pending 是否为null,如果pending不为null,则将pending进行enqueue,否则线程进入wait状态。

通过这2点,我们来看整个过程:

pending是由jvm来赋值的,当Reference内部的referent对象的可达状态改变时,jvm会将Reference对象放入pending链表。

结合代码eg1中的 o = null; 这一句,它使得o对象满足垃圾回收的条件,并且在后边显式的调用了 System.gc(),垃圾收集进行的时候会标记WeakReference所referent的对象o为不可达(使得wr.get()==null),并且通过 赋值给pending,触发ReferenceHandler线程处理pending

ReferenceHandler线程要做的是将pending对象enqueue,但默认我们所提供的queue,也就是从构造函数传入的是null,实际是使用了ReferenceQueue.NULLHandler线程判断queue为ReferenceQueue.NULL则不进行操作,只有非ReferenceQueue.NULL的queue才会将Reference进行enqueue。

ReferenceQueue.NULL相当于我们提供了一个空的Queue去监听垃圾回收器给我们的反馈,并且对这种反馈不做任何处理。要处理反馈,则必须要提供一个非ReferenceQueue.NULL的queue。

WeakHashMap则在内部提供了一个非NULL的ReferenceQueue

private final ReferenceQueue<K> queue = new ReferenceQueue<K>();

在 WeakHashMap 添加一个元素时,会使用 此queue来做监听器。见put方法中的下面一句:

    tab[i] = new Entry<K,V>(k, value, queue, h, e);

这里Entry是一个内部类,继承了WeakReference

class Entry<K,V> extends WeakReference<K> implements Map.Entry<K,V>

WeakHashMap的 put, size, clear 都会间接或直接的调用到 expungeStaleEntries()方法。

expungeStaleEntries顾名思义,此方法的作用就是将 queue中陈旧的Reference进行删除,因为其内部的referent都已经不可达了。所以也将这个WeakReference包装的key从map中删除。

个人认为:ReferenceQueue是作为 JVM GC与上层Reference对象管理之间的一个消息传递方式,它使得我们可以对所监听的对象引用可达发生变化时做一些处理,WeakHashMap正是利用此来实现的。用图来大致表示如下:

现在,我们再回到那个帖子的问题:http://www.javaeye.com/topic/587995

他开始的测试写法为:

List<WeakHashMap<byte[][], byte[][]>> maps = new ArrayList<WeakHashMap<byte[][], byte[][]>>();
for (int i = 0; i < 1000; i++) {
    WeakHashMap<byte[][], byte[][]> d = new WeakHashMap<byte[][], byte[][]>();
    d.put(new byte[1000][1000], new byte[1000][1000]);
    maps.add(d);
    System.gc();
    System.err.println(i);
}

会造成OOM异常。

注意一下,他在for循环里每次都 new 一个新的WeakHashMap,并且key和value都是大对象,之后,他在 for循环的最后增加了一句访问 WeakHashMap的size(),使得不会造成OOM。

首先上面的代码并不是没有执行GC,而是仅对 WeakHashMap中的key中的byte数组进行了回收,而value依然保持。我们可以先做个试验,把上面的value用小对象代替

for (int i = 0; i < 10000; i++) {
    WeakHashMap<byte[][], Object> d = new WeakHashMap<byte[][], Object>();
    d.put(new byte[1000][1000], new Object());
    maps.add(d);
    System.gc();
    System.err.println(i);
}

上面的代码,即使执行10000次也没有问题,证明key中的byte数组确实被回收了。
那为何key中的referent的数据被GC,却没有触发WeakHashMap去做清除整个key的操作呢?

因为他for循环中每次都new一个新的WeakHashMap,在put操作后,虽然GC将WeakReference的key中的byte数组回收了,并将事件通知到了ReferenceQueue,但后续却没有相应的动作去触发 WeakHashMap 去处理 ReferenceQueue,所以 WeakReference 包装的key依然存在在WeakHashMap中,其对应的value也当然存在。

而在for循环的尾巴增加了一句 d.size()方法,却可以了,是因为

size()里面触发了expungeStaleEntries 操作,它将 ReferenceQueue中的 WeakReference对象从map中删除了,对应着value也一并删除了,使得value也被GC回收了。

classloader问题:import my.package._ 是否会load该包下所有的class?

在scalal REPL下做的实验

scala > import java.util._

通过jconsole观察确实新load了98个class(java.util包下的所有的类)

准备了一个 whj.jar 其package为a,里面有1000个class

scala > import a._

却发现新增只有1,2百,而非全部1000个。不清楚是jvm还是scala对class太多的情况做了处理?