一、堆外内存组成

通常JVM的参数我们会配置

-Xms 堆初始内存 
-Xmx 堆最大内存 
-XX:+UseG1GC/CMS 垃圾回收器 
-XX:+DisableExplicitGC 禁止显示GC 
-XX:MaxDirectMemorySize 设置最大堆外内存,默认是-xmx-survivor,也就是基本上和-xmx大小相等 
-Xss:每个线程的堆栈大小,默认1M 
-Xmn: 年轻代大小(eden区+2 survivor) 
-XX:newRatio: 4 年轻代与老年代1:4 
-XX:survivorRatio: 8Eden区与survivor大小比值

java整个进程占用的内存: 
- 堆内存 
- metaspace(堆内) JDK8使用metaspace来替代了permsize:永久代大小 
- 堆外内存使用 
- 线程栈空间

堆外内存回收: 堆外内存的回收是通过system.gc()来的,依赖于目前的gc机制。 
通常是通过DirectByteBuffer对象来分配堆外内存,gc的时候就是判断这个对象是否被引用,来决定是否回收。

二、堆外内存参数配置

-XX:InitialCodeCacheSize=64M \
 -XX:CodeCacheExpansionSize=1M \
 -XX:CodeCacheMinimumFreeSpace=1M \
 -XX:ReservedCodeCacheSize=200M \
 -XX:MinMetaspaceExpansion=1M \
 -XX:MaxMetaspaceExpansion=8M \
 -XX:MaxDirectMemorySize=96M \
 -XX:CompressedClassSpaceSize=256M \

 三、问题排查

3.1、首先确认堆占用

1、用jmap,jmap 查看heap内存使用情况


jmap -heap pid


可以查看到MetaspaceSize,CompressedClassSpaceSize,MaxMetaSize 
jmap和jdk版本有关系,有些jdk版本会查看不到内存信息,可以使用jstat来查看统计信息

2、jstat 收集统计信息


jstat -gc pid 1000 S0C/S0U S1C/S1U EC/EU CCSC/CCSU YGC/YGCT FGC/FCGT GCT survivor0容量和使用 survivor1容量和使用 Eden jdk8是meta,以前应该是PC,PC young gc次数和耗时 full gc次数和耗时 total gc时间


如果能排除掉heap的问题,就要分析堆外内存情况了。

3.2、分析堆外情况

NMT(native memory tracking)
使用
在JVM参数中添加 -XX:NativeMemoryTracking=[off | summary | detail]

在JVM运行过程中,使用jcmd获取相关信息 
jcmd pid VM.native_memory [summary | detail | baseline | summary.diff | detail.diff | shutdown] [scale= KB | MB | GB]


jcmd pid VM.native_memory detail


baseline个基准,之后会输出diff参数,来和这个基线版本进行比较,可以两次的内存差

NMT报告会显示内存使用情况

类别                  含义
Java Heap        堆大小
Thread              线程
Thread Stack    线程栈

NMT可以得到线程栈大小,排除栈空间影响。

pmap 查看进程内存地址空间


pmap -x pid | sort xx


可以结合pmap,和nmt得到内存地址空间。和堆外占用情况了。

接下来需要做的就是分析堆外内存的内容了。

gdb dump查看内存空间内容

gdb dump查看内存空间内容


(gdb) dump binary memory ./file BEGIN_ADDRESS END_ADDRESS


将内存内容dump到文件中,就可以查看到文件中的内容了。 
但是这种方式不直观,所以可以使用其他工具

gperf 
google的,使用gperf2.5即可,网上很多安装都说一定要安装libunwind,其实都是瞎抄抄,老版本确实需要,2.5的版本不需要了。

 
另外一个注意点就是虽然heap文件只有1M,但是可以分析出堆外内存的大小。 
不过我在实际使用过程中,gperf并没有分析出实际的堆外内存情况,通过pmap可以看出堆外内存占用有几个G,但是gperf始终只有200M

Jemalloc 
https://github.com/jemalloc/jemalloc/releases  安装

./configurate –enable-prof
make
sudo make install
配置

export LD_PRELOAD=/usr/local/lib/libjemalloc.so 
export MALLOC_CONF=prof:true,lg_prof_interval:31,lg_prof_sample:17,prof_prefix:/output/jeprof
https://github.com/jemalloc/jemalloc/wiki/Getting-Started

 

 

环境:基于B\S的点子考试系统,为了发现客户端能实时地从服务端接收考试数据,系统使用了逆向AJAX技术(也称Comet或Server Side Push),选用CometD1.1.1作为服务端推送框架,服务器是Jetty7.1.4,硬件为一台普通PC机,Core i5 CPU,

        4G内存,运行32位Windows操作系统。

说明:测试期间发现服务端不定时抛出内存溢出异常,服务器不一定每次都会出现异常,但是假如正式考试时奔溃一次,那估计整场考试都会全乱套,网站管理员尝试过把堆开到最大,32位系统最多到1.6GB基本无法再加大了,而且开大量也基本没效果,抛出

        内存溢出异常好像更加繁琐了。加入-XX:+HeapDumpOnOutOfMemoryError,居然也没有任何反应,抛出内存溢出异常时什么文件都没产生。无奈之下只好挂着jstat使劲盯屏幕,发现GC并不频繁,Eden区,Survivor区,老年代及拥挤代内存全部

       表示"情绪稳定,压力不大",但是照样不停的抛出内存溢出异常,管理员鸭梨很大。最后,在内存溢出后从系统日志中找到异常堆栈。

分析:大家都知道操作系统对每个进程能管理的内存是有限的,这台服务器使用的32位Windows平台的限制是2GB,其中给了Java堆1.6GB,而Direct Memory 并不算在1.6GB的堆之内,因此它只能在剩余的0.4GB空间分出一部分。在此应用中导致内 

        存溢出的关键是:垃圾收集进行时,虚拟机虽然会对Direct Memory进行回收,但是Direct Memory 却不能像新生代和老年代那样,发现空间不足了就通知收集器进行垃圾回收,他只能等到抛出内存溢出异常时,先catch掉,再在catch块里面“大喊”

        “System.gc”.要是虚拟机还是不听(如:打开了-XX:+DisableExplicitGC开关),那就只能眼睁睁地看着堆中还有许多空闲内存,自己却不得不抛出内存异常了。而本案例中使用的Comet1.1.1框架,正好有大量的NIO操作需要用到Direct Memory。

总结:从实践经验来看,除了java堆和永久代之外,我们注意到下面这些区域也会占用较多的内存,这里所有的内存总和会受到操作系统进程最大内存的限制:

        1.Direct Memory:可以通过-XX:MaxDirectMemorySize调整大小,内存不足时抛出OutOfMemoryError或OutOfMemoryError:Direct buffer memory。

        2.线程堆栈:可通过-Xss调整大小内存不足时抛出StackoverflowErroe(纵向无法分配,即无法分配新的栈帧)或OutOfMemoryError:unable to create new native thread(横向无法分配,即无法建立新的线程)。

        3.Socket缓存区:每个Socket连接都Receive和Send两个缓存区,分别占大约37KB和25KB的内存,连接多的话这块内存占用也比较可观。如果无法分配,则可能会抛出IOException:Too many open files异常。

        4.JNI代码:如果代码中使用JNI调用本地库,那么本地库使用内存也不在堆中

        5.虚拟机和GC:虚拟机和GC的代码执行也要消耗一定的内存。