有时候对内存进行大对象的读写,会引起 JVM 长时间的停顿,有时候则是希望最大程度地提高 JVM 的效率,我们需要自己来管理内存(看起来很像是 Java 像 C++祖宗的妥协吧)。据我所知,很多缓存框架都会使用它,比如我以前使用过的 EhCache(给它包装了个酷一点的名字,叫 BigMemory),以及现在项目中的 Memcached。在 nio 以前,是没有光明正大的做法的,有一个 work around 的办法是直接访问 Unsafe 类。如果你使用 Eclipse,默认是不允许访问 sun.misc 下面的类的,你需要稍微修改一下,给 Type Access Rules 里面添加一条所有类都可以访问的规则:
在使用 Unsafe 类的时候:
Unsafe f = Unsafe.getUnsafe();
发现还是被拒绝了,抛出异常:
java.lang.SecurityException: Unsafe
正如 Unsafe 的类注释中写道:
Although the class and all methods are public, use of this class is limited because only trusted code can obtain instances of it.
于是,只能无耻地使用反射来做这件事;
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe us = (Unsafe) f.get(null);
long id = us.allocateMemory(1000);
其中,allocateMemory 返回一个指针,并且其中的数据是未初始化的。如果要释放这部分内存的话,需要调用 freeMemory 或者 reallocateMemory 方法。Unsafe 对象提供了一系列 put/get 方法,例如 putByte,但是只能一个一个 byte 地 put,我不知道这样会不会影响效率,为什么不提供一个 putByteArray 的方法呢?
从 nio 时代开始,可以使用 ByteBuffer 等类来操纵堆外内存了:
ByteBuffer buffer = ByteBuffer.allocateDirect(numBytes);
像 Memcached 等等很多缓存框架都会使用堆外内存,以提高效率,反复读写,去除它的 GC 的影响。可以通过指定 JVM 参数来确定堆外内存大小限制(有的 VM 默认是无限的,比如 JRocket,JVM 默认是 64M):
-XX:MaxDirectMemorySize=512m
对于这种 direct buffer 内存不够的时候会抛出错误:
java.lang.OutOfMemoryError: Direct buffer memory
千万要注意的是,如果你要使用 direct buffer,一定不要加上 DisableExplicitGC 这个参数,因为这个参数会把你的 System.gc() 视作空语句,最后很容易导致 OOM。
对于 heap 的 OOM 我们可以通过执行 jmap -heap 来获取堆内内存情况,例如以下输出取自我上周定位的一个问题:
using parallel threads in the new generation.
using thread-local object allocation.
Concurrent Mark-Sweep GC
Heap Configuration:
MinHeapFreeRatio = 40
MaxHeapFreeRatio = 70
MaxHeapSize = 2147483648 (2048.0MB)
NewSize = 16777216 (16.0MB)
MaxNewSize = 33554432 (32.0MB)
OldSize = 50331648 (48.0MB)
NewRatio = 7
SurvivorRatio = 8
PermSize = 16777216 (16.0MB)
MaxPermSize = 67108864 (64.0MB)
Heap Usage:
New Generation (Eden + 1 Survivor Space):
capacity = 30212096 (28.8125MB)
used = 11911048 (11.359260559082031MB)
free = 18301048 (17.45323944091797MB)
39.42476549789859% used
Eden Space:
capacity = 26869760 (25.625MB)
used = 11576296 (11.040016174316406MB)
free = 15293464 (14.584983825683594MB)
43.08298994855183% used
From Space:
capacity = 3342336 (3.1875MB)
used = 334752 (0.319244384765625MB)
free = 3007584 (2.868255615234375MB)
10.015510110294118% used
To Space:
capacity = 3342336 (3.1875MB)
used = 0 (0.0MB)
free = 3342336 (3.1875MB)
0.0% used
concurrent mark-sweep generation:
capacity = 2113929216 (2016.0MB)
used = 546999648 (521.6595153808594MB)
free = 1566929568 (1494.3404846191406MB)
25.875968024844216% used
Perm Generation:
capacity = 45715456 (43.59765625MB)
used = 27495544 (26.22179412841797MB)
free = 18219912 (17.37586212158203MB)
60.144962788952604% used
可见堆内存都是正常的,重新回到业务日志里寻找异常,发现出现在堆外内存的分配上:
java.lang.OutOfMemoryError
at sun.misc.Unsafe.allocateMemory(Native Method)
at java.nio.DirectByteBuffer.(DirectByteBuffer.java:101)
at java.nio.ByteBuffer.allocateDirect(ByteBuffer.java:288)
at com.schooner.MemCached.SchoonerSockIOPool$TCPSockIO.(Unknown Source)
对于这个参数分配过小的情况下造成 OOM,不妨执行 jmap -histo:live 看看(也可以用 JConsole 之类的外部触发 GC),因为它会强制一次 full GC,如果堆外内存明显下降,很有可能就是堆外内存过大引起的 OOM。
对于堆外内存的使用率,可以使用 rednaxelafx 做的一个工具来查看:链接。
BTW,如果在执行 jmap 命令时遇到:
Error attaching to process: sun.jvm.hotspot.debugger.DebuggerException: Can't attach to the process
这个算是 JDK 的一个 bug(链接),只要是依赖于 SA(Serviceability Agent)的工具,比如 jinfo/jstack/jmap 都会存在这个问题,但是 Oracle 说了 “won’t fix”……
Ubuntu 10.10 and newer has a new default security policy that affects Serviceability commands. This policy prevents a process from attaching to another process owned by the same UID if the target process is not a descendant of the attaching process.
不过它也是给了解决方案的,需要修改/etc/sysctl.d/10-ptrace.conf:
kernel.yama.ptrace_scope = 0
如果你的操作系统不是 Ubuntu,可以升级一下 JDK 的版本试试,我在 RedHat 上遇到过这样的问题,升级 JDK 版本以后解决了。
堆外内存泄露的问题定位通常比较麻烦,可以借助 google-perftools 这个工具,它可以输出不同方法申请堆外内存的数量。当然,如果你是 64 位系统,你需要先安装 libunwind 库。
最后,JDK 存在一些 direct buffer 的 bug(比如这个和这个),可能引发 OOM,所以也不妨升级 JDK 的版本看能否解决问题。
文章未经特殊标明皆为本人原创,未经许可不得用于任何商业用途,转载请保持完整性并注明来源链接 《四火的唠叨》
×Scan to share with WeChat