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


情景

项目上线了一个接口,先灰度一台机器调查调用状况;
接口不断的调用,过了一段时刻,发现机器上的接口调用开端报OOM异常
当天便是上线deadline了,刺激。。

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

发现问题

第一步,运用jps指令获取n 4 s I 2出问题jvm进程C N / w e T ! A的进程ID

运用jpm Z 7s -l -m获取到当时jvm进程的pid,通过上述指令获取到了服务的进程号:427726 (此处假设为这个)

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

jps指令

jps(JVM Process Status Tool):显现指定系统内一切的HotSpot虚拟机进程
jps -l -m : 参数-l列出机器上一切jvm进程,-m显现出JVM启动时传递给main()的参数

第二步,Q { u 2 ^ i ] K运用jstat调查jvm状况,发现问题

因为是OOM异常,所以咱们首要重启机器调查了JVM的运行状况;

咱们运用jstat -gc pid time指令调查GC,发现GC在YGC后,GC掉的内存并不多,每次YGC后都有一部分内存未收回,导致在屡次YGC后收回不掉的内存被挪到堆的old区,old满了之后FGC发现也是收回不掉;
这儿基H Z ) } A %本能够确定是内存走漏的问题了,下面咱们有简略看了下机器的cpu、内存、磁盘状况

jstat指令:

jstat(JVM statisv 2 w , ; ~tics Monitoring)是用于监督虚拟机运行时状况信息的指令,它能够显现出虚Y – s + f :拟机进程中的类装载、内存、废x ` = D x + 4 ^物收集、JIT编译等运行数据。

jstat -gc pid time : -gc 监控jvm的gc信息,pid 监控的jvmq % ? R 7 H d进程id,time每个多少毫秒改写一次

jstat -gccause pid time : -gccause 监控gc信息; 3 6 % V _ /并显现前次gc原因,pid 监控的jvm进程id,time每个多少毫秒改写一次

jstat -class pid time: -class 监控jvm的类加载信息,pid 监控的jvm进程id,time每个多少毫秒改写一次

在这儿先简略说一下,堆的GC:

在GC开端的时分,目标只会存在于Eden区和名为“From”的SurviX r 0 s q P , bvor区,SurZ * Ovivor区“To”是空的。紧接着进行GC,Eden区中一切存活的u s g k T @ l E M目标都会被复制到“To”,而在“From”区中,仍存活的目标会根据他们的年纪值来决议去向。

年纪到达必定值(年纪阈值,能够通过-XX:Mb { XaxT9 c W 3 – EenuringThreshold来设置)的目H B w标会被移动到年老代中,没o q e ~ 8有到A : J ? & ,达阈值的目标会被复制到“To”区域。通过这次GC后,Eden区和From区现已被清空。这个时分,“From”和“To”会交流他们的角色,也便是新的“To”便是前次GC前的“From”,新^ j : ^ M } 的“From”便是前次GC前的“To”。不管怎样,都会保证名为To的Surviv` w k &or区域是空的,minor GC会一向重复这样的过_ l ( I + M H程。

第三步,调查机器状况,承认问题

运用top -p pid获取进程的cpu和内存运用率;检查RESc R V d j F , 和 %CPU %MEM三个指标:

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

在这儿先简略说一下,top指令展现的内容:

VIRT:vi, ` c 7 M Wrtual memory usage 虚拟内存
1、进程“需求的”虚拟内存巨细,包含进程运用的库、代码、数据等
2、假设进程请求100m的内存,但实{ p Q o %践只运用了10mF : 3 2 M N $,那么它会增P % R S N &长100m,而不是实践的运用量

RES:reside3 : ( G c # *nt memory usage 常驻内存
1、进程当时运用的内存巨细,但不n A 2 e { m l 包含swap out
2、包含其他进程的同享
3、假h R W a } w s z如请求100m的内存,实践运x W 7 k = f { *用10m,+ v 6 E ~ Q z l P它只增长10m,与VIRT相反
4、关于库占用内存的状况,它只统计加载的库文件所占内存巨细

SHR:shared memory 同享内存
1、除了本身进程的同享内存,Z _ 3 + &也包含其他进程的同享内存
2、虽然进程只运用了几个同享库的函数,但它包含了整个同享_ L I g q t |库的巨细
3、核算某个进程所占的物理内存巨细公式:RES – SH# T j y iR
4、swap out后,它将会降下来

DATA
1、数据占用的内存。假如top没有显现,按f键能够显现出来。
2、真实的该程序要求b P P _的数据空间,是真实在运行中要运用3 n | f Q 7的。

ps : 假如程序占用( 3 g m j 7 U实存比较多,说明程序请求内存多,] 0 / .实践运用的空间也多。
假如~ o f Y _ J c程序占用虚存比较多,说明程序请求来许多空间,可是没有运用。

发现机器的本身状况不存在问题, so毋庸置疑,发现问题了,典型的内存走漏Y | { w X。。

第四步,运用jmap获取jvm进程dump文件

咱们运用jmapa J b e ! h -dump:format=b,file=dump_file_name pid 指令,将当Q _ 8 k [ 1 U ^时机器的jg j 8vm的状况dT T Iump下来或缺的一份dump文件,用做下面的剖析

jmap指令:

jmap(JVM Memory Map)指令用于生成heap dump文件,还能够查询finalize执行队列、Java堆和永久代的详细信息,如当时运用率、当时运用的是哪种收集器等。
jmap -dump:format=b,file=dump_file_name^ y # w . pid : file=指定输出数据文件名, pid jvm进程号

接下来,回滚灰度的机器,开端解决问题=.=

解决问题

第一步,dump文件剖析

在这儿,咱们剖析dump文件,运用的Jprofiler软件,便是下面这个东东:

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

详细的* 3 5 + * z [ Q E运用办法,在这就不再赘述了,下面+ D b Y E将dump文件导入到Jprofiler中:
挑选Heap Walkq w . T A P V 1er 中的Current Object Se3 ) Q ?t,这儿面显现的是当时的类的占用资源,从占用空间从大到小排序;

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

从上图中,没有调查出什么问题,咱们点击Biggest Objects,检g 6 o 0 , H t ~查哪个目标的占用的内存高:

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

从上图中,咱们发现org.janusgraph.graphdb.database.StandardJanusGraph这个目标居然占用了高达724M的内存! 看来内存走漏八九不离十便是这个目标的问题了!
再点开看看 ,如下图,能够发现是一F ~ { ( vopenTransactions的类型为ConcurrentHashMap的数据结构:

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

第二步,源码– x Q N { x 8查找定位代码

这究竟是什么目标呢,去项目中查找一下,翻开idea-翻开项目-双击shift键-翻开大局类查找-输入StandardJanusGraph,如下图:

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

发现是咱们项目运用的b a o a G .图数据库janus [ 2graph的一个类,找到对应的数据结构:
类型界说:

private Set<StI Q ,a[ G @  yndardJanusGraphTx> openTransactions;

初始化为一个ConcurrentHashMaq / l I ~ w . [p:

openTransactions = Collections.newSetFromMap(new
Con2 C 3 , Z gcurrentHashMap<StandaH Z y 6 x k IrdJanusGraphTx, Boolean>(100,
0.75f, 1));

调查上述代码,咱们能够看到,里边的存储的StandardJanusGraphTx从字面意义上理解是janusgraph框架中的业务目标,下面往上追一下代码,看看什么时分会往这个MX [ m 7 / Z C D Tap中赋值:

// 找到执行openTransactions.add()的办法@ g @ i * m
public StandardJanusGraphTx new( q t ETransaction(final TransactionConfiguration configuration) {
if (!isOpen) ExceptionFactory.graphShutdown();
try {
StandardJanusGraphTx tx = new StandardJanusGraphTx(this, configuration);
tx.se! | A | _tBacke}  j b y  z 0 0ndTransaction(openBacken/ @ _ 6 g y ; ` kdTransaction(tx));
openTransactions.add(tx);  // 留意! 此处对上述的map目标进行了add
return tx;
} catch (BackendException e) {
throw new JanusGraphException("Could not sl P D x 9 = ! S ,tart new transaction", e);
}
}
// 上述发现,是一个newTransaction,创立, h F | u业务的一个办法,为保证起见,再往上跟找到调用上述办法的类:
public JanusG3 5 J b C vraphTo k / e ( . ; j )ransaction start() {
Transac, P M s z L HtionConfiguration immutable = new ImmutableTxCfg(isReadOnly, hasEnabledBatchLoading,
assignIDsImmediately, preloadedData, forceIndexUsage, verifyExternalVertexExS 6 Fistence,
verifyInternalVertexExistence, acquireLocks, ve! m ` ~ k F MrifyUniqueness,
propertyPrefetching, singleT. ( [ d D d lhreaded, threadBound, getTimestampProvider(), userCommitTime,
inR 7 h Z @ H ] l vdexCacheWeight, getVertexCacheSize(), getDirtyVertexSize(),
logIdentifier, restri{ T Q A q qctedPartitions, groupName,
defaultSchemaMaker, customOptions);
return graph.newTransaction(immutable);  // 留意!此处调用了上述的newTT a 2 4ransaction办法
}
// 接着找上x + ^ c v层调用,发现了最上层的办法
public JanusGraphTransaction newTransaction() {
return buildTransaction().start();  // 此处调用了上述的start办法
}

在咱们对图数据库中图数据操作的过程中,O 0 p _ Q P Z z采用的是手动创立业务的方式,在每次查Y = o a J D ?询图数据库之前,咱们都会调用类似于dataDao.begin()代码,
其中便是调用的public JanusGraphTransaction newTransaction()这个办法;

最终,咱们简略的看下源码能够发现,9 O g H l 3 )从上述内存走2 6 R G漏的map中去除数据的逻辑便是commit业务的接口,调用链如下:

    publ( I + h 2ic void closeTransaction(StaD 9 p g X H = /ndardJanusGraphTx tx) {
openTran] S n ^ 8 G v I Osactions.remove(tx); // 从map中o _ b /删除StandardJanusGraphTx目& [ g 0
}
privn $ -ate void releaseTransaP | ` t # actI e 1 u Y Eion() {
isOpe_ U W Y 9 I 5n = faL A Q o : 1lsez d l N 4 T;
graph.cl= + + {oseTransa2 0 g 3 ?ction(this); // 调用上述closeTr) B U Gansaction办法
vertexCache.close();
}
public synchronized void commit() {
Preconditions.checkArgumen( % D c _  & q nt(isOpen(), "The transaction has already been closed");
boolean succ* $ M z ^ : $ u Zess = false;
if (m } y $ x ] f /null !8 9 f l= config.gi e U - ? ;etGroupName()) {
MetricManager.INSTANCE.getCounter(con 4 : R G gfig.getGroupX h Z p | Z c + ~Name(), "tx", "coT p E W 0 S . Gmmit").inc();
}
try {
if (hasModifications()) {
graphh ^ { ^ m 1.commit(addedRelai t t W { ;tions.getAll(), del# ~ Q cetedRelae D E . 0 Z R rtions.values(), this);
} else {
txHandle.commit();  // 这个commit办法中开释业务也是调用releaseTransaction
}
success = true;
} catch (Excep i e + | O +tion e) {
try {
txHandle.rollback();
} catch (BackendException e1) {
throw new JanusGraphExc& 9 H J D ( F 9 reption("Could not rollback after a failed commit", e);
}
throw new JanusGraphException("Could not commit transaction due to exception duri{ [ yng persistencet N i ; x 5 O j", e);
} finally {
releaseTransaction();  // // 调用relek N _ ^ x u i #aseTX T } & F Z 4 9 |ransaction
if ({ 9 #nD ) x k r w 7ull != config.getGroupName() && !success) {
MetricManager.INSTANCE.getCounter(config.getGroupName(), "tx", "commp h V a G % @ 8 Iit.excG I ^ k 5 s Keptions").inc();
}
}
}

终于,咱们找到了内存走漏的根源所在:项目代码中存在调用了业F } { e X F 0 } ]begin可是没有commit的代码!

第三步,修正问题验证

解决问题: 找到内存走漏接口的代码,并发现了没有commit()的位置,try-catch-finally中添加上了commitk s 0 J W [ }()代码;

提交-部署-发布-灰度一台机器后调查内存走漏的现象消失,GC收回正常;

内存走漏问题解决,项目按期上线~

最终

我们,有6 0 & ? ; P没有遇到过内存走漏的状况,欢迎在谈论区说出你的故事=.=

写这篇文章消耗的时刻超出了我的预料,预计2个小时q H u g a B K f写完,成果花了一下午的时刻…~ f 4 s R 9 L _

原创不易,假如我们有所收获G * B +,期望我们能够点赞谈论支撑一下~

也欢迎我们重视我的6HU网和微信查找公众号[匠心Java]支撑一下作者,作者定期共享工作中的所见所得~

发表评论

提供最优质的资源集合

立即查看 了解详情