有时候对内存进行大对象的读写,会引起JVM长时间的停顿,有时候则是希望最大程度地提高JVM的效率,我们需要自己来管理内存(看起来很像是Java像C++祖宗的妥协吧)。据我所知,很多缓存框架都会使用它,比如我以前使用过的EhCache(给它包装了个酷一点的名字,叫BigMemory),以及现在项目中的Memcached。在nio以前,是没有光明正大的做法的,有一个work around的办法是直接访问Unsafe类。如果你使用Eclipse,默认是不允许访问sun.misc下面的类的,你需要稍微修改一下,给Type Access Rules里面添加一条所有类都可以访问的规则:
在使用Unsafe类的时候:
1
|
Unsafe f = Unsafe.getUnsafe();
|
发现还是被拒绝了,抛出异常:
1
|
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.
于是,只能无耻地使用反射来做这件事;
1
2
3
4
|
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等类来操纵堆外内存了:
1
|
ByteBuffer buffer = ByteBuffer.allocateDirect(numBytes);
|
像Memcached等等很多缓存框架都会使用堆外内存,以提高效率,反复读写,去除它的GC的影响。可以通过指定JVM参数来确定堆外内存大小限制(有的VM默认是无限的,比如JRocket,JVM默认是64M):
1
|
-
XX
:MaxDirectMemorySize=512m
|
对于这种direct buffer内存不够的时候会抛出错误:
1
|
java.lang.OutOfMemoryError: Direct buffer memory
|
千万要注意的是,如果你要使用direct buffer,一定不要加上DisableExplicitGC这个参数,因为这个参数会把你的System.gc()视作空语句,最后很容易导致OOM。
对于heap的OOM我们可以通过执行jmap -heap来获取堆内内存情况,例如以下输出取自我上周定位的一个问题:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
|
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
|
可见堆内存都是正常的,重新回到业务日志里寻找异常,发现出现在堆外内存的分配上:
1
2
3
4
5
|
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命令时遇到:
1
|
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:
1
|
kernel.yama.ptrace_scope =
0
|
如果你的操作系统不是Ubuntu,可以升级一下JDK的版本试试,我在RedHat上遇到过这样的问题,升级JDK版本以后解决了。
堆外内存泄露的问题定位通常比较麻烦,可以借助google-perftools这个工具,它可以输出不同方法申请堆外内存的数量。当然,如果你是64位系统,你需要先安装libunwind库。
最后,JDK存在一些direct buffer的bug(比如这个和这个),可能引发OOM,所以也不妨升级JDK的版本看能否解决问题。
转自:http://www.raychase.net/1526