排查load过高/gc过久问题

转载自:http://ju.outofmemory.cn/entry/149977
当jvm的gc很久+很频繁时,会导致服务load高,服务5xx响应明显增多等问题。
通过dump+MAT可以排查为什么生成了这么多garbage。(jmap使用:https://blog.youkuaiyun.com/ligeforrent/article/details/70474203
MAT中Overview可能很小只有几十M,是因为Overview只显示有GCRoot的对象。
unreachable_objects可能会有成百上千M,里面都是可被回收的garbage。可以和jmap -histo pid 给出的占用信息相互印证。但是也有可能都是eden区的不必过分担心的对象,混淆视野。所以需要在fgc前,和fgc后ygc前,分别jmap一次,进行比对。
unreachable_objects无法分析持有关系,但是通过Overview中的持有关系,可以推测garbage生前的情况。
以下是原文。


mem dump中unreachable objects分析

Xunjun Yin 2014-12-08 291 阅读

dump

现象

  • 某个大版本上线后,某服务频繁报警: load过高,但很快就会恢复,即间歇性load过高,原因难以定位。

观察

  • 从报警系统观察报警时间分布,有一些周期性,但周期会在1小时到2小时之间,不稳定
    • 初步排除是定时任务导致
  • 观察gc日志,发现报警时间点与gc时间开销较高的点比较吻合,因此着重观察GC
  • 发现JVM GC相关的配置不是很合理,做了简单的优化:将old区的并行收集改为CMS,情况有所改善,但问题依然存在
  • 该现象是上线后才出现的,可以认定是业务逻辑所致。着重查看了业务逻辑的diff,因业务逻辑改动较多,未能直接找到原因。
  • 既然是GC所致,就先看看GC为何这么慢吧!

初步定位

  • 用jmap做了两份内存的dump,一份是old快满(用jstat来实时观察)即即将发生full gc之前的dump,另一份是GC刚刚结束的dump
    • 这样可以明显地对二者做对比。
  • 用mat打开(在机群上开了个vncserver,在本地用vncviewer连接上)两份dump,发现两份dump中reachable objects均只有400m不到,而unreachable objects有3G多
  • 观察unreachable objects,按shallow heap大小排序,忽略byte和char等基本类型(复杂类型数据都是由基本类型组成的)发现TreeMap占了300M+,让人怀疑。
  • 然而因为是unreachable的,无法直接查看这些TreeMap对象的上用。如何才有知道到底谁引用它们呢?在代码里grep了一下,发现自己项目中没有任何地方用到TreeMap,从页抢断应该是第三方库用到的。
  • 从另外一个角度考虑,我可以先看一下reachable objects中TreeMap的引用。
  • 发现其中对treeMap有引用的对象中,个数较多的是JDBC4ResultSet和ShardedJedis。先看前者在unreachable objects中的情况,共有22158个垃圾对象。
  • 在mat的dominator tree中看JDBC4ResultSet的大小,可以知道每个JDBC4ResultSet的retained heap大小为4.928k(也有不同,但出入不大),从而22158个的retained heap大小为22158 * 5k 约100M,远小于heap区的大小,因此不是造成问题的原因,忽略。
  • 再看后者ShardedJedis,从unreachable objects中可以看到共有4214个垃圾对象。
  • 同时从dominator tree中可以看出每个ShardedJedis的Retained heap大小为323k,从而4214个的retained heap大小的4214*323k约为1.4G
  • 至此,可以基本认定sharedeJedis为主要的垃圾,load间歇性过高的原因正是因为这些垃圾快速的生成和回收所致。

进一步分析

  • 看一下我们对SharededJedisPool的配置:

      #最大分配的对象数
      redis.pool.maxActive=500
      #最大能够保持idel状态的对象数
      redis.pool.maxIdle=100
      #当池内没有返回对象时,最大等待时间
      redis.pool.maxWait=200
      #当调用borrow Object方法时,是否进行有效性检查
      redis.pool.testOnBorrow=false
      #当调用return Object方法时,是否进行有效性检查
      redis.pool.testOnReturn=false
  • 该配置与dump中的pool配置一致。
  • 通过netstat grep redisserver的端口,可以查到只有100个连接,即可预估在“够用”的情况下,redis的连接池中通常只会有100个左右(大多为空闲)
  • 那么问题来了:为什么会有这么多ShardedJedis的垃圾呢?
  • 我们redis连接池是用GenericObjectPool来管理的, 需要时从里面借(borrow object),用完了再还回去(return object)。如果借的时候不够,就会new一个SharededJedis来给借用者。同时还有一个清理机制,每隔一段时间去把多余的空闲对象清理掉。从上图中可以看出,这个清理机制的间隔时间_timeBetweenEvictionRunsMills是30000ms,即30s.
  • 进一步,这些ShardedJedis能度过Young GC并进入old区,必然是在young区gc时没有被清理,从而可推断该进程做一次young GC的周期会远小于GenericObjectPool的清理周期。用jstat –gctuil查看:

      [love@love21 ~]$ jstat -gcutil 21416 1000
        S0     S1     E      O      P     YGC     YGCT    FGC    FGCT     GCT
    .67   0.00  43.38  16.74  52.69  56012 2064.246   143   24.948 2089.193
    .67   0.00  77.59  16.74  52.69  56012 2064.246   143   24.948 2089.193
    .00   7.91   1.92  16.84  52.69  56013 2064.278   143   24.948 2089.225
    .00   7.91  34.35  16.84  52.69  56013 2064.278   143   24.948 2089.225
    .00   7.91  64.58  16.84  52.69  56013 2064.278   143   24.948 2089.225
    .00   7.91  78.19  16.84  52.69  56013 2064.278   143   24.948 2089.225
    .43   0.00   0.00  16.90  52.69  56014 2064.311   143   24.948 2089.258
    .43   0.00  22.36  16.90  52.69  56014 2064.311   143   24.948 2089.258
    .43   0.00  55.95  16.90  52.69  56014 2064.311   143   24.948 2089.258
    .43   0.00  90.86  16.90  52.69  56014 2064.311   143   24.948 2089.258
    .00   8.99  18.07  16.95  52.69  56015 2064.344   143   24.948 2089.292
    .00   8.99  51.25  16.95  52.69  56015 2064.344   143   24.948 2089.292
    .00   8.99  72.06  16.95  52.69  56015 2064.344   143   24.948 2089.292
    .00   8.99  92.20  16.95  52.69  56015 2064.344   143   24.948 2089.292
    .41   0.00  30.01  17.07  52.69  56016 2064.379   143   24.948 2089.326
    .41   0.00  65.24  17.07  52.69  56016 2064.379   143   24.948 2089.326
    .41   0.00  97.34  17.07  52.69  56016 2064.379   143   24.948 2089.326
  • 可以看到约5s左右就会进行一次young GC,与推断相符。
  • 现在,基本有了一些思路:1. 业务中有向GenericObjectPool borrow大量的object(应该远大于100)的逻辑,2. 并且很可能是定时性的任务(crontab 或 scheduleTask).
  • 结合上述两点并结合代码最新的改动,发现有地方将获取50个数据对象改为了获取250个,而为了提交性能,会用future模式开多个线程并发向一个ThreadPoolExecutor 提交大量的任务,并且该任务对redis有请求操作,而这个ThreadPoolExecutor的coreSize是200(注意,远大于redis pool的100):

      private static final ThreadPoolExecutor executor = new ThreadPoolExecutor(200, 400, 3600, TimeUnit.SECONDS, 
              new LinkedBlockingQueue<Runnable>(500), new ThreadFactory() {
                  private AtomicInteger id = new AtomicInteger(0);
                  @Override
                  public Thread newThread(Runnable r) {
                      Thread thread = new Thread(r);
                      thread.setName("Love-" + id.addAndGet(1));
                      return thread;
                  }
              }, new ThreadPoolExecutor.CallerRunsPolicy());
  • 再追溯这个50到250的改动,实际上是可以很简单的做优化的。
  • 至此,问题定位到,优化并上线,不再频繁出现load过高的问题,即GC不再如此频繁

经验

  • 解决一个性能问题好比侦察案件,可以多采集利用分析各种信息,不要死磕。虽然最终发现问题很简单,并一开始就认定问题出在业务逻辑上,可几个人花了不少时间去查代码,都没能找到原因。(该问题被一些设计模式“隐藏”的较深)
<think>好的,我现在需要帮助用户解决如何优化DEA进程以减少内存占用问题。首先,我需要理解DEA进程是什么。根据用户提供的引用内容,可能是指某个特定的进程,比如在Android开发中的进程,或者是某种数据处理进程。不过,用户没有明确说明,可能需要假设是常见的应用进程,比如开发工具或后台服务。 接下来,查看用户提供的引用内容。引用[1]提到内存溢出分为内存真正不足和可用内存不足两种情况,并提到了内存泄漏的场景。引用[2]展示了一个监控脚本,用于跟踪进程的内存使用情况。引用[3]提到多核系统上的扩展性问题,需要优化每个CPU时间片的效率。引用[4]提到接口优化时加缓存的方法。 用户的问题是如何减少DEA进程的内存占用。根据这些引用,可能的解决方案包括分析内存使用情况、检测内存泄漏、优化代码和资源使用、调整JVM参数(如果是Java进程)、使用缓存等。 首先,我需要确定DEA进程的具体情况。比如,是否是Java应用,是否有内存泄漏,或者是否存在资源未释放的问题。根据引用[2]的监控脚本,可以持续监控进程的内存使用情况,识别内存增长的模式。如果有内存只增不减的情况,可能存在内存泄漏。 然后,根据引用[1]的内存泄漏场景,需要检查是否有未关闭的流、未释放的对象、静态集合类持有对象引用等问题。例如,在Java中,未关闭的数据库连接或文件流会导致内存泄漏。可以使用内存分析工具如MAT或Android Profiler来检测。 另外,引用[4]提到通过缓存优化接口响应时间,这可能也适用于减少内存占用,比如缓存频繁使用的数据,避免重复加载,从而减少内存消耗。 如果DEA进程是基于JVM的,调整堆大小(-Xmx、-Xms)可能有助于控制内存使用,但需要平衡性能和内存。引用[3]提到多核系统的扩展性问题,可能需要优化线程池配置,避免过多线程消耗内存。 还需要考虑代码层面的优化,比如使用更效的数据结构,及时释放无用对象,避免大对象或集合的频繁创建。例如,处理大文件时使用流处理而非一次性加载到内存。 综上所述,解决方案的步骤可能包括: 1. 监控内存使用,确定内存增长的原因。 2. 检测并修复内存泄漏。 3. 优化代码和资源管理。 4. 调整运行时参数(如JVM配置)。 5. 使用缓存和效的数据结构。 6. 减少不必要的后台进程或服务。 需要确保每一步都有对应的引用支持,比如引用[2]的监控方法,引用[1]的内存泄漏检测,引用[4]的缓存策略等。同时,可能需要指导用户使用工具进行具体分析,比如Android Profiler或MAT,或者编写脚本监控内存。</think>### 如何优化 DEA 进程以减少内存占用 #### 1. **分析内存使用情况** - **监控内存变化**:使用类似引用[2]的脚本定期记录进程内存占用(如`VmRSS`),观察内存是否持续增长或存在峰值。 示例脚本改进: ```bash # 持续监控指定进程的内存和CPU pid=$1 while true; do mem=$(grep VmRSS /proc/$pid/status | awk '{print $2 " kB"}') cpu=$(top -bn1 -p $pid | tail -1 | awk '{print $9 "%"}') echo "$(date +%H:%M:%S) | 内存: $mem, CPU: $cpu" >> dea_mem.log sleep 5 done ``` - **工具辅助分析**:使用`jmap`(Java进程)或`Valgrind`(C/C++)生成内存快照,或通过Android Profiler(移动端)定位内存热点[^1]。 #### 2. **检测并修复内存泄漏** - **常见泄漏场景**: - 未释放资源(如文件流、数据库连接) - 静态集合类长期持有对象引用 - 匿名内部类/异步任务未取消 - **排查方法**: 通过工具(如MAT)分析内存快照,查看对象引用链,识别冗余对象或未释放的组件。 #### 3. **优化代码与资源管理** - **释放无用对象**: 及时将集合(如`ArrayList`、`HashMap`)置为`null`或调用`clear()`,尤其在处理大数据时。 ```java // 示例:释放大对象 BigData data = loadData(); process(data); data = null; // 显式解除引用 ``` - **流式处理替代全加载**: 避免一次性将文件或网络响应全部加载到内存,改用分块读写。 ```java try (BufferedReader reader = new BufferedReader(new FileReader("large.txt"))) { String line; while ((line = reader.readLine()) != null) { processLine(line); // 逐行处理 } } ``` #### 4. **调整运行时参数** - **JVM 堆配置**(Java进程适用): 限制最大堆内存(`-Xmx`)并启用垃圾回收优化参数。 ```shell java -Xms128m -Xmx512m -XX:+UseG1GC -jar dea_app.jar ``` - **限制线程数**: 减少线程池大小或使用协程(如Kotlin协程),避免过多线程占用内存[^3]。 #### 5. **缓存与数据结构优化** - **缓存策略**: 对频繁访问但不变的数据使用内存缓存(如`LruCache`),避免重复计算或IO操作[^4]。 - **选择效数据结构**: 例如用`SparseArray`替代`HashMap`(Android场景),或使用原始类型集合(如`IntArray`)。 #### 6. **减少后台负载** - **关闭非核心服务**: 检查DEA进程是否运行了冗余的后台任务(如定时轮询、日志同步),按需启用或合并任务周期。 - **延迟加载**: 将模块初始化拆分为按需加载,减少启动时的内存压力。 --- ###
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值