背景

某项目weblogic AdminServer在启动后会慢慢消耗内存,并且超过-Xmx设置的大小,最终消耗完整个服务器的内存。

  • 内存使用情况
[root@esb-osb1 ~]# free -m
              total        used        free      shared  buff/cache   available
Mem:          31996       30878         229         195         888         358
Swap:          8063        5793        2270
  • 各进程使用内存情况
Tasks: 355 total,   1 running, 354 sleeping,   0 stopped,   0 zombie
%Cpu(s):  7.4 us,  1.3 sy,  0.0 ni, 91.3 id,  0.0 wa,  0.0 hi,  0.0 si,  0.0 st
KiB Mem : 32763912 total,   229628 free, 31621300 used,   912984 buff/cache
KiB Swap:  8257532 total,  2327596 free,  5929936 used.   364928 avail Mem
 Unknown command - try 'h' for help
   PID USER      PR  NI    VIRT    RES    SHR S  %CPU %MEM     TIME+ COMMAND
128235 oracle    20   0   24.8g  14.9g   6272 S  94.1 47.7   2390:44 java
  2951 oracle    20   0  800088  71008    480 S  17.6  0.2   4581:11 gsd-color
101629 root      20   0  162236   2372   1532 R   5.9  0.0   0:00.03 top
......

进程128235就是Admin Server,显示已经消耗47.1%的内存,并且CPU很高,cpu问题猜测可能是内存不足导致的并发症。

预备知识

java内存

一个jvm进程消耗的物理内存不单单-Xmx设置的大小,-Xmx只是设置了堆(heap)内存的大小,也就是java代码运行时各种变量,数据保存的地方,堆内存里面又分了年轻代,老年代,持久代,这个就不展开,除了堆以外,还包括PermGen(永久代)和线程堆栈,对应的启动参数为-XX:MaxPermSize-Xss,Xss默认是1M,MaxPermSize没有默认值,因此正常情况下,是需要设置MaxPermSize,简而言之,jvm消耗内存公式为:

Max memory = [-Xmx] + [-XX:MaxPermSize] + 线程数 * [-Xss]

除此之外,我们知道,java可以通过JNI调用本地程序,本地程序可以是C/C++或者汇编等,那么在本地程序中申请的内存jvm是无法限制,因此如果有JNI调用的情况下

Max memory = [-Xmx] + [-XX:MaxPermSize] + 线程数 * [-Xss] + JNI内存
注意

JDK版本从1.8后,不在支持MaxPermSize和PermSize,替换为另外两个参数MaxMetaspaceSizeMetaspaceSize,这两个并不等同,但作用类似,PermSize是固定内存,MetaspaceSize是动态增长

RES

在top命令中,进程列表有个参数RES(resident memory usage),表示常驻内存,比如程序申请了100M,实际只使用了10M,那么RES就是10M,所以这个代表应用程序真正使用的物理内存,在上面的top命令中显示PID 128235使用的物理内存为14.9g,这个显然是不正常的。

pmap

pmap命令可以显示进程的内存信息

Address           Kbytes     RSS   Dirty Mode  Mapping
0000000000400000       4       4       0 r-x-- java
0000000000600000       4       4       4 rw--- java
00000000012e5000     132      12      12 rw---   [ anon ]
00000000c0000000  294400  209860  209860 rw---   [ anon ]
00000000d1f80000   55808       0       0 -----   [ anon ]
00000000d5600000  174080       0       0 rw---   [ anon ]
00000000e0000000  349184  349184  349184 rw---   [ anon ]
00000000f5500000  175104  174204  174204 rw---   [ anon ]
  • Address:内存地址(这里是虚拟地址,不是物理内存地址)
  • Kbytes:内存块大小,这里是虚拟内存大小,不一定真正使用这么大
  • RSS:常驻内存大小,和top的RES一样
  • Dirty:脏页大小
  • Mode:读写模式
  • Mapping:映像支持文件,[anon]为已分配内存 [stack]为程序堆栈

我们重点关注RSS这个参数

gdb

gdb是Linux上面自带的应用程序调试工具,功能强大,我们可以借助这个工具将内存dump出来查看。

排查

用jps -v命令查看jvm启动参数

# jps -v|grep AdminServer
128235 Server -Xms1024m -Xmx4096m -Djava.security.egd=file:/dev/./urandom -Dlaunch.use.env.classpath=true -Dweblogic.Name=AdminServer -Djava.security.policy=/u01/fmwhome/fmwinstall/wlserver/server/lib/weblogic.policy -Dweblogic.ProductionModeEnabled=true -Djava.system.class.loader=com.oracle.classloader.weblogic.LaunchClassLoader -Djava.protocol.handler.pkgs=oracle.mds.net.protocol|com.bea.wli.sb.resources.url|oracle.fabric.common.classloaderurl.handler|oracle.fabric.common.uddiurl.handler|oracle.bpm.io.fs.protocol -Dopss.version=12.2.1.3 -Digf.arisidbeans.carmlloc=/u01/fmwhome/fmwcfg/user_projects/domains/soa_domain/config/fmwconfig/carml -Digf.arisidstack.home=/u01/fmwhome/fmwcfg/user_projects/domains/soa_domain/config/fmwconfig/arisidprovider -Doracle.security.jps.config=/u01/fmwhome/fmwcfg/user_projects/domains/soa_domain/config/fmwconfig/jps-config.xml -Doracle.deployed.app.dir=/u01/fmwhome/fmwcfg/user_projects/domains/soa_domain/servers/AdminServer/tmp/_WL_user -Doracle.deployed.app.ext=/- -Dweblogic.alternateTypesDi

设置了最大堆内存-Xmx4096m,千万别以为设置了Xmx就万事大吉了。

用pmap查看内存(内容太多,这里只截取最后几列)

# pmap -x 128235  | sort -n -k3
00007fb570000000   65524   54428   54428 rw---   [ anon ]
00007fb730000000   65508   54564   54564 rw---   [ anon ]
00007fb6d8000000   65520   54748   54728 rw---   [ anon ]
00007fb670000000   65524   54908   54904 rw---   [ anon ]
00007fb6fc000000   65532   55036   55036 rw---   [ anon ]
00007fb608000000   65532   55776   55764 rw---   [ anon ]
00007fb664000000   65536   56072   56072 rw---   [ anon ]
00007fb6b8000000   65528   56916   56916 rw---   [ anon ]
00007fb6cc000000   65516   58632   58592 rw---   [ anon ]
00007fb48c000000   65132   58968   58968 rw---   [ anon ]
00007fb6d0000000   65512   60472   60440 rw---   [ anon ]
00007fb6f0000000   65512   62652   62652 rw---   [ anon ]
00007fb6f8000000   65528   63628   63600 rw---   [ anon ]
00007fb6ec000000   65516   63956   63936 rw---   [ anon ]
00007fb6e4000000   65360   64652   64616 rw---   [ anon ]
00007fb6e0000000   65536   64692   64668 rw---   [ anon ]
00007fb710000000   65512   64804   64804 rw---   [ anon ]
00007fb6e8000000   65536   64852   64824 rw---   [ anon ]
00007fb6f4000000   65532   65052   65048 rw---   [ anon ]
00007fb6dc000000   65508   65464   65452 rw---   [ anon ]
00000007c0000000   90368   89484   89484 rw---   [ anon ]
00007fb718000000  131072   90160   90160 rw---   [ anon ]
0000000000a34000  141928   91096   91096 rw---   [ anon ]
00007fb75d000000  201344  200160  200144 rwx--   [ anon ]
000000076ab00000 1397760 1372188 1372188 rw---   [ anon ]
00000006c0000000 2796544 2765312 2765312 rw---   [ anon ]
total kB         25893432 15534700 15529228

最后5块内存应该对应的是堆内存

00007fb718000000  131072   90160   90160 rw---   [ anon ]
0000000000a34000  141928   91096   91096 rw---   [ anon ]
00007fb75d000000  201344  200160  200144 rwx--   [ anon ]
000000076ab00000 1397760 1372188 1372188 rw---   [ anon ]
00000006c0000000 2796544 2765312 2765312 rw---   [ anon ]

加起来差不多是4G,那么除了这些,存在大量64M(65536)的内存块,正是这些64M内存块大量消耗内存,那么这里面到底存了什么内容。

我们可以选取一块内存快用gdb工具查看,比如00007fb6e0000000,gdb要求指定开始地址和结束地址,这里只有开始地址结束地址怎么计算?只要开始地址加上64M就行,64M=65536KB=67108864B=0x4000000B。那么:

结束地址 = 0x7fb6e0000000 + 0x4000000 = 0x7fb6e4000000
  • gdb导出内存

# gdb attach 128235

dump memory /root/dump.bin 0x7fb6e0000000 0x7fb6e4000000

/root/dump.bin就是dump出来的内存文件,内存里可能是二进制也可能是字符串,二进制基本很难分析,但能从字符串看出一些线索,64M不算大,稍微好一点的文本编辑器都能打开(千万不要用记事本打开),打开看之后发现大量字符内容都是类签名信息和看起来像是jar中MANIFEST.MF文件内容

导出其他内存块也是同样的结果,jvm中类信息和meta信息都是存在在永久代中,也就是PermGen空间里,因此猜测很可能是PermGen未设置导致无节制增长,从而将内存消耗光。

解决并验证

在AdminServer上添加600M PermGen的限制(具体怎么添加下面有专门的章节介绍)后观察内存变化,为了尽快看到效果,这边做了几件事

  • 通过以下命令在控制台上持续打印内存变化情况
for i in {1..100000}; do ps -p 25725 -o rss;sleep 3s; done
  • 通过以下命令在控制台上持续打印GC情况(主要是观察FGC情况)
[oracle]
jstat -gcutil 25725 1000 10000
  • 登录console,不断对应用进行部署取消动作,可以让内存持续增长,因为部署应用weblogic会将包信息加载至PermGen。
  • 以下是在加了600M PermGen限制后的内存,gc变化情况

刚开始内存一直增长,但增长到大约2960000KB的时候开始进行FGC,此后内存消耗稳定在这个水平

按照预测内存应该稳定在2048M+600M=2648M=2711552KB上下,这边多了大概200M左右的内存消耗,可能是线程堆栈,但不确定,有兴趣可以去研究,这不影响本次问题的诊断

接着我们解除了PermGen限制,重启了AdminServer,然后做同样的事,以下是结果的截图:

我们可以看到有7次的FGC,但实际上,weblogic启动过程中已经FGC 7次了,所以后面模拟部署的过程其实一次FGC也没执行,也就是,根本没进行垃圾回收。通过不断的部署,内存消耗很快就超过了3G,并完全没有降的情况,因此我们可以得出结论AdminServer承担着应用部署的任务,如果没有设置PermGen的大小,AdminServer内存的消耗会随着部署次数的增加不断的增长,最终将整个操作系统内存消耗殆尽

weblogic如何配置参数

之所以专门一个章节讲这个,原因是每个项目有每个项目的方法,有直接修改setDomainEnv.sh脚本的,有通过额外脚本setStartupEnv.sh设置,有通过console设置,五花八门的方法都有,但有些方法虽然能达到目的,但我觉得不是最佳实践,存在问题。因此这里推荐一种最佳实践方法。

在console,点击server->配置->服务器启动,有个参数的文本输入框,这里可以配置weblogic的启动参数

比如要配置内存参数可以参考

-Xms8192m -Xmx8192m -XX:MaxPermSize=1024m -XX:PermSize=1024m

JDK>=1.8需要将MaxPermSize改为MaxMetaspaceSize,PermSize改为MetaspaceSize

在console上配置的好处:

  • 查看方便,修改方便
  • 可视化傻瓜式配置

在脚本里修改,有时候涉及的地方很多,都要修改,而且并不是所有开发人员都了解Linux,都了解shell脚本,所以不建议修改脚本的方式。

**注意!!!!**,这里配置的参数,只能通过nodemanager启动才能生效,也就是只有通过console启动服务器,才能生效,你手动在后台通过脚本启动是无法生效的,所以对于manager server,这样配是可以的,**但是** AdminServer一般都是手动在后台启动的,所以怎么办呢?当然,你可以再搭建一台专门用来启动AdminServer的weblogic(真的有客户这么做的),这个成本有点高,但从架构上确实是合理的,对于AdminServer,我们可以修改setDomainEnv.sh脚本。

由于不同版本的weblogic setDomainEnv.sh脚本内容不一样,因此没有统一改的位置,可以通过以下方法找到修改的位置

找到脚本里最后一次出现export MEM_ARGS的位置,然后在后面添加以下内容

if [ "${SERVER_NAME}" = "AdminServer" ] ; then
        MEM_ARGS="${MEM_ARGS} -XX:MaxMetaspaceSize={你希望调整的大小(单位:兆)}m -XX:MetaspaceSize={你希望调整的大小(单位:兆)}m"
        export MEM_ARGS
fi

**注意!!!!**,如果是JDK<=1.7或者jrokit,那么使用以下脚本

if [ "${SERVER_NAME}" = "AdminServer" ] ; then
        MEM_ARGS="${MEM_ARGS} -XX:MaxPermSize={你希望调整的大小(单位:兆)}m -XX:PermSize={你希望调整的大小(单位:兆)}m"
        export MEM_ARGS
fi

如果是JDK>=1.8,那么使用以下脚本

if [ "${SERVER_NAME}" = "AdminServer" ] ; then
        MEM_ARGS="${MEM_ARGS} -XX:MaxMetaspaceSize={你希望调整的大小(单位:兆)}m  -XX:MetaspaceSize={你希望调整的大小(单位:兆)}m"
        export MEM_ARGS
fi

参考

Trackback

no comment untill now

Add your comment now