JAVA使用堆外内存导致swap飙高

本文记录了一次因未正确关闭GZIPInputStream导致的堆外内存占用问题排查过程,通过使用google-perftools和BTrace成功定位到了具体代码。
部署运行你感兴趣的模型镜像

近期遇到一个堆外内存导致swap飙高的问题,这类问题比较罕见,因此将整个排查过程记录下来了

现象描述

最近1周线上服务器时不时出现swap报警(swap超过内存10%时触发报警,内存是4G,因此swap超过400M会触发报警),每次都是童鞋们通过重启tomcat解决的;
但导致的根本原因是什么呢?必须找到根本原因才行,总是这么重启就有点low了

问题排查

于是找了1台占用了swap但还未触发报警的服务器进行了排查
以下是当时通过top命令观察到的结果

23:03:22 swap占用了354M的内存
top-swap

23:55:42 swap占用了398M的内存
top-swap-2

原因分析

到底是什么原因导致swap飙高呢?肯定是tomcat,因为每次重启tomcat就解决了;但根本原因是?

谁占用了swap

通过以下脚本 swap.sh

#!/bin/bash
# Get current swap usage for all running processes
# Erik Ljungstrom 27/05/2011
do_swap () {
SUM=0
OVERALL=0
for DIR in `find /proc/ -maxdepth 1 -type d | egrep "^/proc/[0-9]"` ; do
PID=`echo $DIR | cut -d / -f 3`
PROGNAME=`ps -p $PID -o comm --no-headers`
for SWAP in `grep Swap $DIR/smaps 2>/dev/null| awk '{ print $2 }'`
do
let SUM=$SUM+$SWAP
done
echo "PID=$PID - Swap used: $SUM - ($PROGNAME )"
let OVERALL=$OVERALL+$SUM
SUM=0

done
echo "Overall swap used: $OVERALL"
}
do_swap  |awk -F[\ \(] '{print $5,$1,$8}' | sort -n | tail -3

可以看出PID=19911这个进程使用了324M的swap

swap-sh

通过grep进程号19911可以看出确实是tomcat占用swap最多
grep-pid

进程19911占用总的物理内存是3.1G,java占用的堆内内存大小为2.78G,剩下的320M是堆外内存占用的

Max memory = [-Xmx] + [-XX:MaxPermSize] + number_of_threads * [-Xss]

2779M=2048M+268M+463*1M

sudo -u tomcat ./jinfo -flag MaxPermSize 19911
-XX:MaxPermSize=268435456
java -XX:+PrintFlagsFinal -version | grep ThreadStackSize
     intx CompilerThreadStackSize                   = 0               {pd product}
     intx ThreadStackSize                           = 1024            {pd product}
     intx VMThreadStackSize                         = 1024            {pd product}
java version "1.7.0_45"
Java(TM) SE Runtime Environment (build 1.7.0_45-b18)
Java HotSpot(TM) 64-Bit Server VM (build 24.45-b08, mixed mode)
java -XX:+PrintFlagsFinal -version | grep -i permsize
    uintx AdaptivePermSizeWeight                    = 20              {product}
    uintx MaxPermSize                               = 85983232        {pd product}
    uintx PermSize                                  = 21757952        {pd product}
java version "1.7.0_45"
Java(TM) SE Runtime Environment (build 1.7.0_45-b18)
Java HotSpot(TM) 64-Bit Server VM (build 24.45-b08, mixed mode)

哪行代码占用了堆外内存

堆内内存溢出可以直接通过MAT分析堆信息就可以定位到具体的代码,但是对于堆外内存就必须通过BTrace来解决

google-perftools 定位类名和方法名

如何安装和使用google-perftools见这里
由于要启动google-perftools需要重启tomcat,所以重启tomcat后,PID从19911变成了9176

重启tomcat后,会自动生成heap文件,文件名的命名规范是gperf_pid.xxx.heap,所以我们只需要关注gperf_9176.*即可

[xxxx@xxxx   /home/xxx/logs]$ ll *.heap
…...
-rw-r--r-- 1 tomcat tomcat    5048 May  6 10:46 gperf_9171.0001.heap
-rw-r--r-- 1 tomcat tomcat    5036 May  6 10:46 gperf_9173.0001.heap
-rw-r--r-- 1 tomcat tomcat    5055 May  6 10:46 gperf_9174.0001.heap
-rw-r--r-- 1 tomcat tomcat    5352 May  6 10:46 gperf_9175.0001.heap
-rw-r--r-- 1 tomcat tomcat 1048563 May  6 10:46 gperf_9176.0001.heap
-rw-r--r-- 1 tomcat tomcat 1048564 May  6 10:46 gperf_9176.0002.heap
-rw-r--r-- 1 tomcat tomcat 1048563 May  6 10:47 gperf_9176.0003.heap
-rw-r--r-- 1 tomcat tomcat 1048565 May  6 10:47 gperf_9176.0004.heap
-rw-r--r-- 1 tomcat tomcat 1048574 May  6 10:49 gperf_9176.0005.heap
-rw-r--r-- 1 tomcat tomcat 1048574 May  6 10:50 gperf_9176.0006.heap
-rw-r--r-- 1 tomcat tomcat 1048568 May  6 10:51 gperf_9176.0007.heap
-rw-r--r-- 1 tomcat tomcat 1048572 May  6 10:53 gperf_9176.0008.heap
-rw-r--r-- 1 tomcat tomcat 1048564 May  6 10:55 gperf_9176.0009.heap
-rw-r--r-- 1 tomcat tomcat 1048560 May  6 10:58 gperf_9176.0010.heap
-rw-r--r-- 1 tomcat tomcat 1048563 May  6 11:00 gperf_9176.0011.heap
-rw-r--r-- 1 tomcat tomcat 1048564 May  6 11:03 gperf_9176.0012.heap
…...

分析heap文件

/home/google-perftools/bin/pprof  --text  /home/java  /home/logs/gperf_9176.0010.heap
Using local file /home/java.
Using local file /home/logs/gperf_9176.0010.heap.
Total: 186.4 MB
91.2 48.9% 48.9% 91.2 48.9% updatewindow
52.5 28.2% 77.1% 52.5 28.2% os::malloc
38.0 20.4% 97.4% 38.0 20.4% inflateInit2_
3.0 1.6% 99.0% 3.0 1.6% init
0.8 0.4% 99.5% 0.8 0.4% ObjectSynchronizer::omAlloc
0.4 0.2% 99.7% 0.4 0.2% readCEN
0.3 0.2% 99.9% 38.3 20.5% Java_java_util_zip_Inflater_init
0.1 0.1% 100.0% 0.1 0.1% _dl_allocate_tls
0.0 0.0% 100.0% 0.0 0.0% _dl_new_object
0.0 0.0% 100.0% 1.1 0.6% Thread::Thread
0.0 0.0% 100.0% 0.0 0.0% CollectedHeap::CollectedHeap
0.0 0.0% 100.0% 0.0 0.0% Events::init
0.0 0.0% 100.0% 0.4 0.2% ZIP_Put_In_Cache0
0.0 0.0% 100.0% 0.0 0.0% read_alias_file
0.0 0.0% 100.0% 0.0 0.0% _nl_intern_locale_data

可以看出是java.util.zip.Inflater的init()占用了比较多的内存

通过BTrace定位代码调用方

编写代码BtracerInflater.java对init方法进行拦截

import static com.sun.btrace.BTraceUtils.*;
import com.sun.btrace.annotations.*;

import java.nio.ByteBuffer;
import java.lang.Thread;

@BTrace public class BtracerInflater{
   @OnMethod(
      clazz="java.util.zip.Inflater",
      method="/.*/"
   )
   public static void traceCacheBlock(){
      println("Who call java.util.zip.Inflater's methods :");
     jstack();
   }
}

运行BTrace

[xxxx@l-xxx.xx.xx /home/xxx/btrace-bin/bin]$ sudo -u tomcat ./btrace -cp ../build 9176 BtracerInflater.java|more
Who call java.util.zip.Inflater's methods :
java.util.zip.Inflater.<init>(Inflater.java:102)
java.util.zip.GZIPInputStream.<init>(GZIPInputStream.java:76)
java.util.zip.GZIPInputStream.<init>(GZIPInputStream.java:90)
com.xxx.OrderDiffUtil.ungzip(OrderDiffUtil.java:54)
com.xxx.OrderDiffUtil.parse(OrderDiffUtil.java:32)
com.xxx.FaxOrderEventListener.takeSectionChangedInfo(FaxOrderEventListener.java:87)
com.xxx.FaxOrderEventListener.onMessage(FaxOrderEventListener.java:46)
.......

可以看出是OrderDiffUtil的ungzip()调用了java.util.zip.Inflater的init()
看看OrderDiffUtil.ungzip()

    private static String ungzip(String encodeJson) {
        ByteArrayOutputStream out = new ByteArrayOutputStream(encodeJson.length() * 5);
        ByteArrayInputStream in = null;
        try {
            in = new ByteArrayInputStream(Base64.decode(encodeJson));
        } catch (UnsupportedEncodingException e) {
            return "{}";
        }
        try {
            GZIPInputStream gunzip = new GZIPInputStream(in);
            byte buffer[] = new byte[1024];
            int len = 0;
            while ((len = gunzip.read(buffer)) != -1) {
                out.write(buffer, 0, len);
            }
        } catch (IOException e) {
            return "{}";
        }
        try {
            return out.toString("ISO-8859-1");
        } catch (UnsupportedEncodingException e) {
        }
        return "{}";
    }

可见gunzip未被close
所以根本原因是未调用GZIPInputStream的close()关闭流导致堆外内存占用

总结

  • 堆外内存的问题使用google-prefile和BTrace定位问题非常方便
  • 堆外内存主要出现在使用DirectByteBuffer或者gzip不当时出现

参考资料

http://m.blog.youkuaiyun.com/blog/whuoyunshen88/19508075
http://itindex.net/detail/11709-perftools-内存-hbase
http://outofmemory.cn/code-snippet/1713/Btrace-usage-introduction

您可能感兴趣的与本文相关的镜像

Anything-LLM

Anything-LLM

AI应用

AnythingLLM是一个全栈应用程序,可以使用商用或开源的LLM/嵌入器/语义向量数据库模型,帮助用户在本地或云端搭建个性化的聊天机器人系统,且无需复杂设置

Java 应用程序中,堆内存(Heap Memory)是 JVM 用于分配对象的运行时内存区域。我们可以通过 JVM 启动参数来设置堆内存的大小,以优化程序性能或避免内存溢出(OutOfMemoryError)。 ### 常见的堆内存设置参数: - `-Xms<size>`:设置初始堆内存大小(默认较小,可以根据需求设置更大的初始值以减少动态扩容带来的性能损耗) - `-Xmx<size>`:设置堆内存最大值(防止内存无限制增长) - `-Xmn<size>`:设置年轻代(Young Generation)大小(年轻代越大,GC频率可能越低,但老年代就越小) - `-XX:MaxPermSize=<size>`:设置永久代大小(仅适用于 Java 8 及之前版本) - `-XX:MaxMetaspaceSize=<size>`:设置元空间大小(Java 9+ 用来替代永久代) --- ### 示例:Java 启动时设置堆内存参数 ```bash java -Xms512m -Xmx2g -Xmn1g -jar your_application.jar ``` 解释: - 初始堆内存为 512MB(`-Xms512m`) - 最大堆内存为 2GB(`-Xmx2g`) - 年轻代大小为 1GB(`-Xmn1g`) --- ### Java 代码示例:查看 JVM 堆内存信息 以下是一个 Java 程序,用于查看当前 JVM 的堆内存设置: ```java public class HeapMemoryInfo { public static void main(String[] args) { Runtime runtime = Runtime.getRuntime(); long initialMemory = runtime.totalMemory(); long maxMemory = runtime.maxMemory(); long freeMemory = runtime.freeMemory(); System.out.println("初始堆内存 (totalMemory): " + (initialMemory / 1024 / 1024) + " MB"); System.out.println("最大堆内存 (maxMemory): " + (maxMemory / 1024 / 1024) + " MB"); System.out.println("空闲堆内存 (freeMemory): " + (freeMemory / 1024 / 1024) + " MB"); } } ``` #### 输出示例(根据你的 JVM 参数不同结果会变化): ``` 初始堆内存 (totalMemory): 247 MB 最大堆内存 (maxMemory): 3601 MB 空闲堆内存 (freeMemory): 238 MB ``` --- ### 注意事项: 1. **不要设置过大的堆内存**:虽然 `-Xmx` 可以设置为非常大的值,但需要确保物理内存足够,否则会导致系统交换(swap)或OOM。 2. **Java 8 和 Java 9+ 的区别**: - Java 8 及之前:使用 `-XX:PermSize` 和 `-XX:MaxPermSize` - Java 9+:使用 `-XX:MaxMetaspaceSize`,元空间默认不限制大小,建议设置上限以防止内存耗尽 3. **GC 性能影响**:堆内存设置过大可能导致 Full GC 时间变长,需要结合 GC 算法进行调优 --- ### 示例:Java 9+ 设置 Metaspace ```bash java -Xms512m -Xmx2g -XX:MaxMetaspaceSize=256m -jar your_application.jar ``` ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值