JVM调优的陷阱

开这帖的目的是想让大家了解到,所谓“标准参数”是件很微妙的事情。确实有许多前辈经过多年开发积累下了许多有用的调优经验,但向他们问“标准参数”并照单全收是件危险的事情。

前辈们提供的“标准参数”或许适用于他们的应用场景,他们或许也知道这些参数里隐含的陷阱;但听众却不一定知道各种参数背后的缘由。

原则上说,在生产环境使用非标准参数(这里指的是在各JDK/JRE实现特有的、相互之间不通用的参数)应该尽量避免。这些参数与具体实现密切相关,不是光了解很抽象的“JVM原理”就足以理解的;即便在同一系列的JDK/JRE实现中,非标准参数也不保证在各版本间有一样的作用;而且许多人只看名字就猜想参数的左右,做“调优”却适得其反。

非标准参数的默认值在不同版本间或许会悄然发生变化。这些变化的背后多半有合理的理由。设了一大堆非标准参数、不明就里的同学在升级JDK/JRE的时候也容易掉坑里。

下面用Oracle/Sun JDK 6来举几个例子。

======================================================================

1、-XX:+DisableExplicitGC 与 NIO的direct memory

很多人都见过JVM调优建议里使用这个参数,对吧?但是为什么要用它,什么时候应该用而什么时候用了会掉坑里呢?

首先要了解的是这个参数的作用。在Oracle/Sun JDK这个具体实现上,System.gc()的默认效果是引发一次stop-the-world的full GC,对整个GC堆做收集。有几个参数可以改变默认行为,之前发过一帖简单描述过,这里就不重复了。关键点是,用了-XX:+DisableExplicitGC参数后,System.gc()的调用就会变成一个空调用,完全不会触发任何GC(但是“函数调用”本身的开销还是存在的哦~)。

为啥要用这个参数呢?最主要的原因是为了防止某些手贱的同学在代码里到处写System.gc()的调用而干扰了程序的正常运行吧。有些应用程序本来可能正常跑一天也不会出一次full GC,但就是因为有人在代码里调用了System.gc()而不得不间歇性被暂停。也有些时候这些调用是在某些库或框架里写的,改不了它们的代码但又不想被这些调用干扰也会用这参数。

OK。看起来这参数应该总是开着嘛。有啥坑呢?

其中一种情况是下述三个条件同时满足时会发生的:
1、应用本身在GC堆内的对象行为良好,正常情况下很久都不发生full GC;
2、应用大量使用了NIO的direct memory,经常、反复的申请DirectByteBuffer
3、使用了-XX:+DisableExplicitGC
能观察到的现象是:
Log代码
‪1.‬java.lang.OutOfMemoryError: Direct buffer memory
‪2.‬ at java.nio.Bits.reserveMemory(Bits.java:633)
‪3.‬ at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:98)
‪4.‬ at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:288)
‪5.‬...


做个简单的例子来演示这现象:
Java代码
‪1.‬import java.nio.*;
‪2.‬
‪3.‬public class DisableExplicitGCDemo {
‪4.‬ public static void main(String[] args) {
‪5.‬ for (int i = 0; i < 100000; i++) {
‪6.‬ ByteBuffer.allocateDirect(128);
‪7.‬ }
‪8.‬ System.out.println("Done");
‪9.‬ }
‪10.‬}

然后编译、运行之:
Command prompt代码
‪1.‬$ java -version
‪2.‬java version "1.6.0_25"
‪3.‬Java(TM) SE Runtime Environment (build 1.6.0_25-b06)
‪4.‬Java HotSpot(TM) 64-Bit Server VM (build 20.0-b11, mixed mode)
‪5.‬$ javac DisableExplicitGCDemo.java
‪6.‬$ java -XX:MaxDirectMemorySize=10m -XX:+PrintGC -XX:+DisableExplicitGC DisableExplicitGCDemo
‪7.‬Exception in thread "main" java.lang.OutOfMemoryError: Direct buffer memory
‪8.‬ at java.nio.Bits.reserveMemory(Bits.java:633)
‪9.‬ at java.nio.DirectByteBuffer.<init>(DirectByteBuffer.java:98)
‪10.‬ at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:288)
‪11.‬ at DisableExplicitGCDemo.main(DisableExplicitGCDemo.java:6)
‪12.‬$ java -XX:MaxDirectMemorySize=10m -XX:+PrintGC DisableExplicitGCDemo
‪13.‬[GC 10996K->10480K(120704K), 0.0433980 secs]
‪14.‬[Full GC 10480K->10415K(120704K), 0.0359420 secs]
‪15.‬Done

可以看到,同样的程序,不带-XX:+DisableExplicitGC时能正常完成运行,而带上这个参数后却出现了OOM。
例子里用-XX:MaxDirectMemorySize=10m限制了DirectByteBuffer能分配的空间的限额,以便问题更容易展现出来。不用这个参数就得多跑一会儿了。

在这个例子里,main()里的循环不断申请DirectByteBuffer但并没有引用、使用它们,所以这些DirectByteBuffer应该刚创建出来就已经满足被GC的条件,等下次GC运行的时候就应该可以被回收。

实际上却没这么简单。DirectByteBuffer是种典型的“冰山”对象,也就是说它的Java对象虽然很小很无辜,但它背后却会关联着一定量的native memory资源,而这些资源并不在GC的控制之下,需要自己注意控制好。对JVM如何使用native memory不熟悉的同学可以参考去年JavaOne上IBM的一个演讲,“Where Does All the Native Memory Go”。

Oracle/Sun JDK的实现里,DirectByteBuffer有几处值得注意的地方。
1、DirectByteBuffer没有finalizer,它的native memory的清理工作是通过sun.misc.Cleaner自动完成的。

2、sun.misc.Cleaner是一种基于PhantomReference的清理工具,比普通的finalizer轻量些。对PhantomReference不熟悉的同学请参考Bob Lee最近几年在JavaOne上做的演讲,"The Ghost in the Virtual Machine: A Reference to References"。今年的JavaOne上他也讲了同一个主题,内容比前几年的稍微更新了些。PPT可以从链接里的页面下载到。
Java代码
‪1.‬/**
‪2.‬ * General-purpose phantom-reference-based cleaners.
‪3.‬ *
‪4.‬ * <p> Cleaners are a lightweight and more robust alternative to finalization.
‪5.‬ * They are lightweight because they are not created by the VM and thus do not
‪6.‬ * require a JNI upcall to be created, and because their cleanup code is
‪7.‬ * invoked directly by the reference-handler thread rather than by the
‪8.‬ * finalizer thread. They are more robust because they use phantom references,
‪9.‬ * the weakest type of reference object, thereby avoiding the nasty ordering
‪10.‬ * problems inherent to finalization.
‪11.‬ *
‪12.‬ * <p> A cleaner tracks a referent object and encapsulates a thunk of arbitrary
‪13.‬ * cleanup code. Some time after the GC detects that a cleaner's referent has
‪14.‬ * become phantom-reachable, the reference-handler thread will run the cleaner.
‪15.‬ * Cleaners may also be invoked directly; they are thread safe and ensure that
‪16.‬ * they run their thunks at most once.
‪17.‬ *
‪18.‬ * <p> Cleaners are not a replacement for finalization. They should be used
‪19.‬ * only when the cleanup code is extremely simple and straightforward.
‪20.‬ * Nontrivial cleaners are inadvisable since they risk blocking the
‪21.‬ * reference-handler thread and delaying further cleanup and finalization.
‪22.‬ *
‪23.‬ *
‪24.‬ * @author Mark Reinhold
‪25.‬ * @version %I%, %E%
‪26.‬ */

重点是这两句:"A cleaner tracks a referent object and encapsulates a thunk of arbitrary cleanup code. Some time after the GC detects that a cleaner's referent has become phantom-reachable, the reference-handler thread will run the cleaner."
Oracle/Sun JDK 6中的HotSpot VM只会在年老代GC(full GC/major GC或者concurrent GC都算)的时候才会做reference processing,而在young GC/minor GC时不做。
也就是说,做full GC的话会做reference processing,进而能触发Cleaner对已死的DirectByteBuffer对象做清理工作。而如果很长一段时间里没做过GC或者只做了young GC的话则不会触发Cleaner的工作,那么就可能让本来已经死了的DirectByteBuffer关联的native memory得不到及时释放。

3、为DirectByteBuffer分配空间过程中会显式调用System.gc(),以期通过full GC来强迫已经无用的DirectByteBuffer对象释放掉它们关联的native memory:
Java代码
‪1.‬// These methods should be called whenever direct memory is allocated or
‪2.‬// freed. They allow the user to control the amount of direct memory
‪3.‬// which a process may access. All sizes are specified in bytes.
‪4.‬static void reserveMemory(long size) {
‪5.‬
‪6.‬ synchronized (Bits.class) {
‪7.‬ if (!memoryLimitSet && VM.isBooted()) {
‪8.‬ maxMemory = VM.maxDirectMemory();
‪9.‬ memoryLimitSet = true;
‪10.‬ }
‪11.‬ if (size <= maxMemory - reservedMemory) {
‪12.‬ reservedMemory += size;
‪13.‬ return;
‪14.‬ }
‪15.‬ }
‪16.‬
‪17.‬ System.gc();
‪18.‬ try {
‪19.‬ Thread.sleep(100);
‪20.‬ } catch (InterruptedException x) {
‪21.‬ // Restore interrupt status
‪22.‬ Thread.currentThread().interrupt();
‪23.‬ }
‪24.‬ synchronized (Bits.class) {
‪25.‬ if (reservedMemory + size > maxMemory)
‪26.‬ throw new OutOfMemoryError("Direct buffer memory");
‪27.‬ reservedMemory += size;
‪28.‬ }
‪29.‬
‪30.‬}


这几个实现特征使得Oracle/Sun JDK 6依赖于System.gc()触发GC来保证DirectByteMemory的清理工作能及时完成。如果打开了-XX:+DisableExplicitGC,清理工作就可能得不到及时完成,于是就有机会见到direct memory的OOM,也就是上面的例子演示的情况。我们这边在实际生产环境中确实遇到过这样的问题。

教训是:如果你在使用Oracle/Sun JDK 6,应用里有任何地方用了direct memory,那么使用-XX:+DisableExplicitGC要小心。如果用了该参数而且遇到direct memory的OOM,可以尝试去掉该参数看是否能避开这种OOM。

======================================================================

2、-XX:+DisableExplicitGC 与 Remote Method Invocation (RMI)

看了上一个例子有没有觉得-XX:+DisableExplicitGC参数用起来很危险?那干脆完全不要用这个参数吧。又有什么坑呢?

前段时间有个应用的开发来抱怨,说某次升级JDK之前那应用的GC状况都很好,很长时间都不会发生full GC,但升级后发现每一小时左右就会发生一次。经过对比发现,升级的同时也吧启动参数改了,把原本有的-XX:+DisableExplicitGC给去掉了。

观察到的日志有这么一个特征:

(后面回头再补…先睡觉去了)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值