Windbg分析高内存占用问题
问题简介最近产品发布大版本补丁更新一商超客户升级后反馈系统经常奔溃导致超市的收银系统无法正常收银现场排队付款的顾客更是抱怨声声。为了缓解现场的情况 客户都是手动回收IIS应用程序池才能解决。这样的后果是很严重的接到反馈第一时间想到的是加内存吧这样最快。但是客户从8G--16G--32G只是延长了每次奔溃的时间但是并没有解决系统卡顿的问题。到这里也基本猜测了问题所在了肯定是什么东西一直在吃内存且得不到释放。这种问题也就只能打Dump分析了。2. 打Dump远程客户应用服务器32G内存占用已经消耗了78%而现场已经反馈收银系统接近奔溃了要求先强制回收内存。反正也要奔溃了先打Dump再说吧。PS打Dump会挂起进程导致应用无法响应而打Dump的耗时也是根据当时进程的内存占用有关内存占用越大耗时越久。打开任务管理器选择对应的IIS进程右键创建转储文件Dump。结果Dump文件是生成的结果当分析的时候发现Windbg提示Dump无效。说明Dump文件创建的有问题。观察任务管理器发现内存占用一下就降下来了原来是之前的进程直接奔溃了重启了一个W3WP进程。既然直接从任务管理器无法创建就使用第三方工具收集Dump吧。经过Goggle找到一款很好用的Dump收集工具ProcDump是一个命令行应用其主要用途是监视应用程序的CPU或内存峰值并在峰值期间生成Dump。因为是高内存占用问题我们使用以下命令来抓取dumpPS可以使用进程名称也可以使用进程ID来指定要创建Dump的进程。当有多个相同名称的进程时必须使用进程ID来指定procdump w3wp -m 20480 -o D:\Dumps 当内存超过20G时抓取一个w3wp进程的MiniDump上面就是我踩得第一个坑因为默认抓取的是MiniDump很快就抓下来文件也很小正在我得意的时候Windbg加载Dump分析的时候发现包含的信息很少根本无法进行进一步的分析。调整创建Dump的命令添加-ma参数即可创建完整Dump。procdump w3wp -ma -m 20480 -o D:\Dumps 当内存超过20G时抓取一个w3wp进程的完整Dump结果再一次当内存占用到达20G占比80%的时候Dump再次创建失败提示Procdump Error writing dump file。再一次感觉到绝望。不过至少有错误提示Google一把果然存在天涯沦落人。Procdump Error writing dump file: 0x80070005 Error 0x80070005 (-2147024891): Access is denied。大致的意思是说当90S内Dump文件没有成功创建的话也就意外这w3wp进程被挂起了90sIIS检测到w3wp进程挂起超过90s没有响应就会终止进程重现创建一个新的进程。好嘛真是处处是坑。这个坑也让我开始真正停下来思考问题。罗马不是一日建成的内存也不是一下撑爆的。我干嘛死脑筋非要到内存占用超过80%才去打Dump呢呢呢焕然大悟如醍醐灌顶。procdump w3wp -ma -m 8000 -o D:\Dumps 当内存超过8000M时抓取一个w3wp进程的完整Dump并输出到D:\Dumps文件夹此时内存占用在40%左右这次Dump终于成功创建了。3..分析Dump分析Dump上WinDbg。如果对WinDbg不理解可以看我这篇WinDbg学习笔记。接下来就是一通命令乱敲我尽量解释清晰。0:000 !dumpheap -stat //检查当前所有托管类型的统计信息....00007ffdb9387a98 777101 69462436 System.Char[]00007ffdb938c988 588917 115563505 System.Byte[]00007ffdb9389220 1026406 119828936 System.Int32[]00007ffdb93516a8 663559 128819040 System.Collections.Generic.Dictionary2Entry[[System.String, mscorlib],[System.Object, mscorlib]][]00000218c6c30a80 6436865 197832116 Free00007ffdae9cc240 23171 273333144 System.Collections.Generic.HashSet1Slot[[System.String, mscorlib]][]00007ffdb9391f28 13885170 333244080 System.Boolean00007ffd5c24a068 14003455 560138200 Kingdee.BOS.JSON.JSONArray00007ffdb9386fc0 14373648 1393615400 System.Object[]00007ffdb9386948 76146065 4000287202 System.StringTotal 138435970 objects使用dumpheap -stat命令查看当前所有托管类型的统计信息。从输出的结果来看其中占用内存最多当属System.String类型接近4G的大小是不是很吃惊!。其次System.Object[]类型占有1.3G大小。Kingdee.BOS.JSON.JSONArray类型也大概占用了560M。我们首先来分析占用最多的System.String类型看看有什么发现。0:000 !dumpheap -mt 00007ffdb9386948 -min 200 //查看200byte以上的stringAddress MT Size...0000021bcbaf5158 00007ffdb9386948 11400000021d375d1038 00007ffdb9386948 1496980000021d375f5920 00007ffdb9386948 1496980000021d3765b138 00007ffdb9386948 1497060000021d37f739c8 00007ffdb9386948 2171200000021d37fa8a08 00007ffdb9386948 1901620000021d38047330 00007ffdb9386948 12246980000021d3829d348 00007ffdb9386948 12246980000021d386bd678 00007ffdb9386948 26109940000021d38bb8500 00007ffdb9386948 2610994Statistics:MT Count TotalSize Class Name00007ffdb9386948 10991 76632628 System.StringTotal 10991 objects从上面的输出可以发现单个System.String类型最大占用2M以上。超过200byte的字节的大小的System.String总大小也不过76M。所以我们也不必深究大的String对象。那我们索性挑一个小点的对象来看看存储的是什么字符串来满足一下我们的好奇心。0.000 !do 0000021bcbaf5158 //使用!do命令查看一个对象的内容Name: System.StringMethodTable: 00007ffdb9386948EEClass: 00007ffdb8c850e0Size: 1140(0x474) bytesFile: C:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dllString: 5b13710029d012False2052_T_BD_MATERIAL_MATERIAL.FAuxPropertyIdFBaseUnitIdFCategoryIDFChargeIDFCheckIncomingFDefaultVendorFErpClsIDFInvPtyIdFIsAffectPlanFIsAffectPlan1FIsBatchManageFIsComControlFIsEnableFIsEnable1FIsExpParToFlotFIsInventoryFIsPRFIsReturnMaterialFIsSourceControlFIsVmiBusinessFNameFNumberFPlanModeFPurchasePriceUnitIdFPurchaseUnitIdFPurPriceURNomFPurPriceURNumFPurURNomFPurURNumFReceiveAdvanceDaysFReceiveDelayDaysFReceiveMaxScaleFReceiveMinScaleFSalePriceUnitIdFSaleUnitIdFSpecificationFStockIdFStockPlaceIdFStoreUnitIDFTaxTypeFUseOrgId111193Fields:MT Field Offset Type VT Attr Value Name00007ffdb9389288 400026f 8 System.Int32 1 instance 557 m_stringLength00007ffdb9387b00 4000270 c System.Char 1 instance 35 m_firstChar00007ffdb9386948 4000274 90 System.String 0 shared static Empty Domain:Value 00000218c6c4d220:NotInit 0000021d52d81840:NotInit 似乎是基础资料字段信息。那接下来使用!gcroot命令查看其对应的GC根看看到底是什么对象持有其引用导致占用内存得不到释放。0:000 !gcroot 0000021bcbaf5158 //使用!gcroot 查看一个对象的gc根HandleTable:00000218c6ff15e8 (pinned handle)- 0000021cc75ebe68 System.Object[]- 0000021bc7629a10 Kingdee.BOS.Cache.KCacheManagerFactory- 0000021bc7629ab8 System.Collections.Generic.Dictionary2[[System.String, mscorlib],[Kingdee.BOS.Cache.AbstractKCacheManager, Kingdee.BOS]]- 0000021c4da6fa48 System.Collections.Generic.Dictionary2Entry[[System.String, mscorlib],[Kingdee.BOS.Cache.AbstractKCacheManager, Kingdee.BOS]][]- 00000218c83861b8 Kingdee.BOS.Cache.KCacheManager- 00000218c8386630 Kingdee.BOS.Cache.ECache.ECacheManager- 00000218c83866e8 System.Collections.Concurrent.ConcurrentDictionary2[[System.String, mscorlib],[System.Collections.Generic.HashSet1[[System.String, mscorlib]], System.Core]]- 0000021bcbae0c70 System.Collections.Concurrent.ConcurrentDictionary2Tables[[System.String, mscorlib],[System.Collections.Generic.HashSet1[[System.String, mscorlib]], System.Core]]- 0000021bcbad0128 System.Collections.Concurrent.ConcurrentDictionary2Node[[System.String, mscorlib],[System.Collections.Generic.HashSet1[[System.String, mscorlib]], System.Core]][]- 0000021bcbb34bf8 System.Collections.Concurrent.ConcurrentDictionary2Node[[System.String, mscorlib],[System.Collections.Generic.HashSet1[[System.String, mscorlib]], System.Core]]- 0000021bcbada790 System.Collections.Concurrent.ConcurrentDictionary2Node[[System.String, mscorlib],[System.Collections.Generic.HashSet1[[System.String, mscorlib]], System.Core]]- 0000021a49766460 System.Collections.Generic.HashSet1[[System.String, mscorlib]]- 00000219540976b0 System.Collections.Generic.HashSet1Slot[[System.String, mscorlib]][]- 0000021bcbaf5158 System.StringFound 1 unique roots (run !GCRoot -all to see all roots).从以上输出可以看出该String类型被一个Hashset所持有。从Cache关键字可以看出该String类型是被缓存所持有。分析到这里我们大致可以得出一个结论String类型占用4G内存绝大多数是由缓存所占用才导致String类型得不到释放。那我们是不是可以猜测内存占用持续走高是不是被缓存撑爆的呢。带着这个疑问我们来继续分析下Kingdee.BOS.JSON.JSONArray类型。0:000 !dumpheap -mt 00007ffd5c24a068 //输出托管堆上的所有JSONArray对象Address MT Size....0000021975972b48 00007ffd5c24a068 4000000218c933f060 00007ffd5c24a068 4000000218c7605990 00007ffd5c24a068 4000000218c7605af0 00007ffd5c24a068 4000000218c7605c50 00007ffd5c24a068 4000000218c7605e18 00007ffd5c24a068 4000000218c7605fa0 00007ffd5c24a068 4000000218c7606198 00007ffd5c24a068 4000000218c7606338 00007ffd5c24a068 4000000218c76064b0 00007ffd5c24a068 40User interrupt.从输出结果来看满屏都是40byte的JSONArray。只能使用CtrlBreak命令中止输出。但为了保险期间我们来验证下有没有100byte以上的JSONArray。0:000 !dumpheap -mt 00007ffd5c24a068 -min 100Address MT SizeStatistics:MT Count TotalSize Class NameTotal 0 objects这时我们可以大胆猜测所有的JSONArray对象都是40byte。从而可以得出另一个猜测占用560M内存的JSONArray都具有相似的对象结构。接下来我们来验证这个猜测。随机选择几个对象看看其内容具体是什么。0:000 !DumpObj /d 0000021975972b48 //查看第一个JSONArrayName: System.Object[]MethodTable: 00007ffdb9386fc0EEClass: 00007ffdb8d4aa00Size: 88(0x58) bytesArray: Rank 1, Number of elements 8, Type CLASS (Print Array)Fields:None从输出可以看出JSONArray实质是System.Object[]类型。对应的MethodTable: 00007ffdb9386fc0。如果你记性好的话我们应当还记得占用内存第二多的就是这个System.Object[]类型占用1.3G。翻到上面你可以发现其MethodTable和上面的统计信息是一致的。PS到这里我们是不是可以猜测System.Object[]占用的内存无法释放就是由于被JSONArray持有引用导致的呢既然是数组就使用!DumpArray命令来解开数组的面纱。0:000 !DumpArray /d 0000021975972b48Name: System.Object[]MethodTable: 00007ffdb9386fc0EEClass: 00007ffdb8d4aa00Size: 88(0x58) bytesArray: Rank 1, Number of elements 8, Type CLASSElement Methodtable: 00007ffdb9386f28[0] 0000021975972a08[1] 0000021975972a70[2] 0000021975972a40[3] 0000021ac75e87b8[4] 0000021975972b10[5] 0000021975972ba0[6] null[7] null0:000 !DumpObj /d 0000021975972a08Name: System.StringMethodTable: 00007ffdb9386948EEClass: 00007ffdb8c850e0Size: 54(0x36) bytesFile: C:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dllString: 555d8ca25a6261Fields:7MT Field Offset Type VT Attr Value Name00007ffdb9389288 400026f 8 System.Int32 1 instance 14 m_stringLength00007ffdb9387b00 4000270 c System.Char 1 instance 35 m_firstChar00007ffdb9386948 4000274 90 System.String 0 shared static Empty Domain:Value 00000218c6c4d220:NotInit 0000021d52d81840:NotInit 从以上输出可以看出其共有8个子项我们再随机挑几个子项看看是什么内容。0:000 !DumpObj /d 0000021975972a70Name: System.StringMethodTable: 00007ffdb9386948EEClass: 00007ffdb8c850e0Size: 42(0x2a) bytesFile: C:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dllString: FHTZDLBFields:MT Field Offset Type VT Attr Value Name00007ffdb9389288 400026f 8 System.Int32 1 instance 8 m_stringLength00007ffdb9387b00 4000270 c System.Char 1 instance 50 m_firstChar00007ffdb9386948 4000274 90 System.String 0 shared static Empty Domain:Value 00000218c6c4d220:NotInit 0000021d52d81840:NotInit 0:000 !DumpObj /d 0000021975972a40Name: System.StringMethodTable: 00007ffdb9386948EEClass: 00007ffdb8c850e0Size: 42(0x2a) bytesFile: C:\Windows\Microsoft.Net\assembly\GAC_64\mscorlib\v4.0_4.0.0.0__b77a5c561934e089\mscorlib.dllString: 发货通知单列表Fields:MT Field Offset Type VT Attr Value Name00007ffdb9389288 400026f 8 System.Int32 1 instance