情景 项目上线了一个接口,先灰度一台机器观察调用情况; 接口不断的调用,过了一段时间,发现机器上的接口调用开始报OOM异常 ! 当天就是上线deadline了,刺激。。 发现问题 第一步,使用j…

                                                                                                                                                                                    ##### 情景 

项目上线了一个接口,先灰度一台机器观察调用情况; 接口不断的调用,过了一段时间,发现机器上的接口调用开始报 ```java
OOM异常

##### 发现问题 
###### 第一步,使用 ```java 
jps
``` 命令获取出问题jvm进程的进程ID 
使用 ```java 
jps -l -m
``` 获取到当前jvm进程的pid,通过上述命令获取到了服务的进程号:427726 (此处假设为这个) ![Test](https://img-blog.csdnimg.cn/20200416192229765.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5fX19MWVk=,size_16,color_FFFFFF,t_70  '事务处理不当,线上接口又双叒内存泄漏了!(附图解问���全过程)') jps命令 
 
###### 第二步,使用 ```java 
jstat
``` 观察jvm状态,发现问题 
 
我们使用 ```java 
jstat -gc pid time
``` 命令观察GC,发现GC在YGC后,GC掉的内存并不多,每次YGC后都有一部分内存未回收,导致在多次YGC后回收不掉的内存被挪到堆的old区,old满了之后FGC发现也是回收不掉; 这里基本可以确定是内存泄漏的问题了,下面我们有简单看了下机器的cpu、内存、磁盘状态 
jstat命令: 
 
在这里先简单说一下,堆的GC: 
 
 
###### 第三步,观察机器状态,确认问题 
使用 ```java 
top -p pid
``` 获取进程的cpu和内存使用率;查看RES 和 %CPU %MEM三个指标: ![Test](https://img-blog.csdnimg.cn/20200416192229765.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5fX19MWVk=,size_16,color_FFFFFF,t_70  '事务处理不当,线上接口又双叒内存泄漏了!(附图解问���全过程)') 
在这里先简单说一下,top命令展示的内容: 
 
 
 
 
 
发现机器的自身状态不存在问题, so毋庸置疑,发现问题了,典型的内存泄漏。。 
###### 第四步,使用jmap获取jvm进程dump文件 
我们使用 ```java 
jmap -dump:format=b,file=dump_file_name pid
```  命令,将当前机器的jvm的状态dump下来或缺的一份dump文件,用做下面的分析 
jmap命令: 
 
接下来,回滚灰度的机器,开始解决问题=.= 
##### 解决问题 
###### 第一步,dump文件分析 
在这里,我们分析dump文件,使用的 ```java 
Jprofiler
``` 软件,就是下面这个东东: ![Test](https://img-blog.csdnimg.cn/20200416192229765.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5fX19MWVk=,size_16,color_FFFFFF,t_70  '事务处理不当,线上接口又双叒内存泄漏了!(附图解问���全过程)') 
具体的使用方法,在这就不再赘述了,下面将dump文件导入到 ```java 
Jprofiler
``` 中: 选择 ```java 
Heap Walker
```  中的 ```java 
Current Object Set
``` ,这里面显示的是当前的类的占用资源,从占用空间从大到小排序; ![Test](https://img-blog.csdnimg.cn/20200416192229765.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5fX19MWVk=,size_16,color_FFFFFF,t_70  '事务处理不当,线上接口又双叒内存泄漏了!(附图解问���全过程)') 从上图中,没有观察出什么问题,我们点击 ```java 
Biggest Objects
``` ,查看哪个对象的占用的内存高: ![Test](https://img-blog.csdnimg.cn/20200416192229765.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5fX19MWVk=,size_16,color_FFFFFF,t_70  '事务处理不当,线上接口又双叒内存泄漏了!(附图解问���全过程)') 从上图中,我们发现 ```java 
org.janusgraph.graphdb.database.StandardJanusGraph
``` 这个对象居然占用了高达724M的内存! 看来内存泄漏八九不离十就是这个对象的问题了! 再点开看看 ,如下图,可以发现是一个 ```java 
openTransactions
``` 的类型为 ```java 
ConcurrentHashMap
``` 的数据结构: ![Test](https://img-blog.csdnimg.cn/20200416192229765.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5fX19MWVk=,size_16,color_FFFFFF,t_70  '事务处理不当,线上接口又双叒内存泄漏了!(附图解问���全过程)') 
###### 第二步,源码查找定位代码 
这到底是什么对象呢,去项目中查找一下,打开idea-打开项目-双击shift键-打开全局类查找-输入 ```java 
StandardJanusGraph
``` ,如下图: ![Test](https://img-blog.csdnimg.cn/20200416192229765.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5fX19MWVk=,size_16,color_FFFFFF,t_70  '事务处理不当,线上接口又双叒内存泄漏了!(附图解问���全过程)') 发现是我们项目使用的图数据库 ```java 
janusgraph
``` 的一个类,找到对应的数据结构: 类型定义: 
 ```java 
private Set<StandardJanusGraphTx> openTransactions;

初始化为一个ConcurrentHashMap:

 openTransactions = Collections.newSetFromMap(new 
ConcurrentHashMap<StandardJanusGraphTx, Boolean>(100, 
0.75f, 1));

观察上述代码,我们可以看到,里面的存储的 ```java
StandardJanusGraphTx

 ```java 
// 找到执行openTransactions.add()的方法
  public StandardJanusGraphTx newTransaction(final TransactionConfiguration configuration) {
      if (!isOpen) ExceptionFactory.graphShutdown();
      try {
          StandardJanusGraphTx tx = new StandardJanusGraphTx(this, configuration);
          tx.setBackendTransaction(openBackendTransaction(tx));
          openTransactions.add(tx);  // 注意! 此处对上述的map对象进行了add
          return tx;
      } catch (BackendException e) {
          throw new JanusGraphException("Could not start new transaction", e);
      }
  }
 // 上述发现,是一个newTransaction,创建事务的一个方法,为确保起见,再往上跟找到调用上述方法的类:
 public JanusGraphTransaction start() {
      TransactionConfiguration immutable = new ImmutableTxCfg(isReadOnly, hasEnabledBatchLoading,
              assignIDsImmediately, preloadedData, forceIndexUsage, verifyExternalVertexExistence,
              verifyInternalVertexExistence, acquireLocks, verifyUniqueness,
              propertyPrefetching, singleThreaded, threadBound, getTimestampProvider(), userCommitTime,
              indexCacheWeight, getVertexCacheSize(), getDirtyVertexSize(),
              logIdentifier, restrictedPartitions, groupName,
              defaultSchemaMaker, customOptions);
      return graph.newTransaction(immutable);  // 注意!此处调用了上述的newTransaction方法
  }
 // 接着找上层调用,发现了最上层的方法
  public JanusGraphTransaction newTransaction() {
      return buildTransaction().start();  // 此处调用了上述的start方法
  } 

在我们对图数据库中图数据操作的过程中,采用的是手动创建事务的方式,在每次查询图数据库之前,我们都会调用类似于 java dataDao.begin() 代码, 其中就是调用的 ```java
public JanusGraphTransaction newTransaction()

最后,我们简单的看下源码可以发现,从上述内存泄漏的map中去除数据的逻辑就是 ```java 
commit
``` 事务的接口,调用链如下: 
 ```java 
    public void closeTransaction(StandardJanusGraphTx tx) {
      openTransactions.remove(tx); // 从map中删除StandardJanusGraphTx对象
  }
  
  private void releaseTransaction() {
      isOpen = false;
      graph.closeTransaction(this); // 调用上述closeTransaction方法
      vertexCache.close();
  }
  
 public synchronized void commit() {
      Preconditions.checkArgument(isOpen(), "The transaction has already been closed");
      boolean success = false;
      if (null != config.getGroupName()) {
          MetricManager.INSTANCE.getCounter(config.getGroupName(), "tx", "commit").inc();
      }
      try {
          if (hasModifications()) {
              graph.commit(addedRelations.getAll(), deletedRelations.values(), this);
          } else {
              txHandle.commit();  // 这个commit方法中释放事务也是调用releaseTransaction
          }
          success = true;
      } catch (Exception e) {
          try {
              txHandle.rollback();
          } catch (BackendException e1) {
              throw new JanusGraphException("Could not rollback after a failed commit", e);
          }
          throw new JanusGraphException("Could not commit transaction due to exception during persistence", e);
      } finally {
          releaseTransaction();  // // 调用releaseTransaction
          if (null != config.getGroupName() && !success) {
              MetricManager.INSTANCE.getCounter(config.getGroupName(), "tx", "commit.exceptions").inc();
          }
      }
  }
 

终于,我们找到了内存泄漏的根源所在:项目代码中存在调用了事务 java begin 但是没有 ```java
commit

###### 第三步,修复问题验证 
解决问题: 找到内存泄漏接口的代码,并发现了没有commit()的位置,try-catch-finally中添加上了commit()代码; 
提交-部署-发布-灰度一台机器后观察内存泄漏的现象消失,GC回收正常; 
内存泄漏问题解决,项目如期上线~ 
##### 最后 
大家,有没有遇到过内存泄漏的情况,欢迎在评论区说出你的故事=.= 
写这篇文章耗费的时间超出了我的预料,预计2个小时写完,结果花了一下午的时间... 
 
也欢迎大家关注我的 ```java 
oschina
``` 和微信搜索公众号[ ```java 
匠心Java
``` ]支持一下作者,作者定期分享工作中的所见所得~ ![Test](https://img-blog.csdnimg.cn/20200416192229765.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L0NTRE5fX19MWVk=,size_16,color_FFFFFF,t_70  '事务处理不当,线上接口又双叒内存泄漏了!(附图解问���全过程)')
                                      

本文标题: 事务处理不当,线上接口又双叒内存泄漏了!(附图解问���全过程)

本文作者: OSChina

发布时间: 2021年04月15日 09:17

最后更新: 2025年04月03日 11:07

原始链接: https://haoxiang.eu.org/a5721e3d/

版权声明: 本文著作权归作者所有,均采用CC BY-NC-SA 4.0许可协议,转载请注明出处!

× 喜欢就赞赏一下呗!
打赏二维码