1. 首页 > 电脑教程 > Dump分析SpeechSynthesizer存在内存泄漏

Dump分析SpeechSynthesizer存在内存泄漏

.net framework 3.0带了个System.Speech.dll,装个语音包,然后就可以实现文字朗读等功能。最近在使用的时候,发现随着程序的运行,程序占用的内存一直在增长,直到程序崩溃。用WinDbg抓了个Dump,然后看了下,里面一堆没有释放的SPVTEXTFRAG、AudioDeviceOut+InItem、WAVEHDR、WaveHeader对象,于是写了一小段来测试:1: //happyhippy.cnblogs.com2: using System.Speech.Synthesis;3: using (SpeechSynthesizer ss = new SpeechSynthesizer())4: {5: for (int i = 0; i < int.MaxValue; i++)6: {7: ss.Speak(i.ToString());8: }9: }跑了几个小时,然后抓了个内存,开始干活:1. 看下内存中有哪些对象1: 0:000> .load clr20/sos.dll2: 0:000> !dumpheap -stat3: PDB symbol for mscorwks.dll not loaded4: total 275193 objects5: Statistics:6: MT Count TotalSize Class Name7: 70441ff8 1 12 System.RuntimeTypeHandle8: 7043fc7c 1 12 System.__Filters9: //。。。。。。。。。。。。中间省略了一堆。。。。。。。。。。。10: 704431a8 18 1008 System.Collections.Hashtable11: 7042c938 65 1560 System.Security.Policy.StrongNameMembershipCondition12: 70441cd4 81 1620 System.RuntimeType13: 70441784 16 1820 System.Char[]14: 7042c6e4 232 6496 System.Security.SecurityElement15: 70442b84 278 6672 System.Collections.ArrayList16: 704435c4 17 75056 System.Byte[]17: 5610529c 4297 85940 System.Speech.Internal.AlphabetConverter+PhoneMapData+ConversionUnit18: 704432a4 18 181896 System.Collections.Hashtable+bucket[]19: 70414324 376 319064 System.Object[]20: 70440b54 13185 396696 System.String21: 5610ab38 7538 693496 System.Speech.Synthesis.TtsEngine.SPVTEXTFRAG22: 56105c50 63264 1012224 System.Speech.Internal.Synthesis.AudioDeviceOut+InItem23: 560f2694 65652 2626080 System.Speech.Internal.Synthesis.WAVEHDR24: 56105b6c 63264 3289728 System.Speech.Internal.Synthesis.WaveHeader25: 002be948 56465 172964660 Free26: Total 275193 objects27: Fragmented blocks larger than 0.5 MB:28: Addr Size Followed by29: 1228c6d8 0.5MB 12311d7c System.Speech.Synthesis.TtsEngine.SPVTEXTFRAG30: 123293e0 0.6MB 123baa64 System.Speech.Synthesis.TtsEngine.SPVTEXTFRAG31: 124be36c 1.9MB 1269d898 System.Speech.Synthesis.TtsEngine.SPVTEXTFRAG32: 126a39f0 1.3MB 127e6644 System.Speech.Synthesis.TtsEngine.SPVTEXTFRAG列表中的第25行,172M的Free状态的内存。最后4行(29~32)的内存碎片,用DumpObj看了下,都是Free状态。2. 看看终结队列中有哪些对象1: 0:000> !FinalizeQueue -detail2: SyncBlocks to be cleaned up: 03: MTA Interfaces to be released: 04: STA Interfaces to be released: 05: ----------------------------------6: generation 0 has 2973 finalizable objects (068fdec4->06900d38)7: generation 1 has 11 finalizable objects (068fde98->068fdec4)8: generation 2 has 60304 finalizable objects (068c3058->068fde98)9: Ready for finalization 0 objects (06900d38->06900d38)10: Statistics:11: MT Count TotalSize Class Name12: 7043a304 1 16 System.WeakReference13: 7002d1c8 1 20 System.ComponentModel.AsyncOperation14: 5610a9c0 1 20 System.Speech.Synthesis.SpeechSynthesizer15: 5610576c 1 20 System.Speech.Internal.ObjectTokens.RegistryDataKey16: 5610581c 1 24 System.Speech.Internal.ObjectTokens.ObjectToken17: 56105ae8 1 56 System.Speech.Internal.Synthesis.AudioDeviceOut18: 70427b90 4 80 Microsoft.Win32.SafeHandles.SafeWaitHandle19: 561092e0 1 176 System.Speech.Internal.Synthesis.VoiceSynthesis20: 7043b370 9 180 Microsoft.Win32.SafeHandles.SafeRegistryHandle21: 70441128 4 224 System.Threading.Thread22: 56105b6c 63264 3289728 System.Speech.Internal.Synthesis.WaveHeader23: Total 63288 objects我靠,内存中3289728个WaveHeader全部挂在终结队列中。一个可能的原因就是:WaveHeader对象中实现了析构函数,并且程序使用完WaveHeader对象后,没有手动释放(Dispose),而是等着GC来自动回收。3. 用Reflector看WaveHeader的源代码1: internal sealed class WaveHeader : IDisposable2: {3: // Fields4: internal int _dwBufferLength;5: private GCHandle _gcHandle = new GCHandle();6: private GCHandle _gcHandleWaveHdr = new GCHandle();7: private WAVEHDR _waveHdr = new WAVEHDR();8: internal const int WAVE_FORMAT_PCM = 1;9: internal const int WHDR_BEGINLOOP = 4;10: internal const int WHDR_DONE = 1;11: internal const int WHDR_ENDLOOP = 8;12: internal const int WHDR_INQUEUE = 0x10;13: internal const int WHDR_PREPARED = 2;14: 15: // Methods16: internal WaveHeader(byte[] buffer)17: {18: this._dwBufferLength = buffer.Length;19: this._gcHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned);//申请内存20: }21: 22: public void Dispose()23: {24: this.Dispose(true);25: GC.SuppressFinalize(this);26: }27: 28: private void Dispose(bool disposing)29: {30: if (disposing)31: {32: this.ReleaseData();33: if (this._gcHandleWaveHdr.IsAllocated)34: {35: this._gcHandleWaveHdr.Free();36: }37: }38: }39: 40: ~WaveHeader()41: {42: this.Dispose(false);43: }44: 45: internal void ReleaseData()46: {47: if (this._gcHandle.IsAllocated)48: {49: this._gcHandle.Free();50: }51: }52: 53: // Properties54: internal int SizeHDR55: {56: get57: {58: return Marshal.SizeOf(this._waveHdr);59: }60: }61: 62: internal GCHandle WAVEHDR63: {64: get65: {66: if (!this._gcHandleWaveHdr.IsAllocated)67: {68: this._waveHdr.lpData = this._gcHandle.AddrOfPinnedObject();69: this._waveHdr.dwBufferLength = (uint) this._dwBufferLength;70: this._waveHdr.dwBytesRecorded = 0;71: this._waveHdr.dwUser = 0;72: this._waveHdr.dwFlags = 0;73: this._waveHdr.dwLoops = 0;74: this._waveHdr.lpNext = IntPtr.Zero;75: this._gcHandleWaveHdr = GCHandle.Alloc(this._waveHdr, GCHandleType.Pinned);//申请内存76: }77: return this._gcHandleWaveHdr;78: }79: }80: }如我们所料,的确定义了一个析构函数(~WaveHeader()),WaveHealder实现了Dispose模式。4. 进一步分析,看看WaveHeader都申请了啥资源,为啥要实现Dispose模式WaveHeader的构造函数中,申请了一片Pinned状态的内存this._gcHandle = GCHandle.Alloc(buffer, GCHandleType.Pinned),并且其WAVEHDR属性的实现中,也通过延迟加载的方式申请了一片Pinned状态的内存this._gcHandleWaveHdr = GCHandle.Alloc(this._waveHdr, GCHandleType.Pinned)。MSDN对GCHander的阐述如下:(FROM MSDN)GCHandle 类与 GCHandleType 枚举结合使用以创建对应于任何托管对象的句柄。此句柄可为以下四种类型之一:Weak、WeakTrackResurrection、Normal 或 Pinned。分配了句柄以后,在非托管客户端保留唯一的引用时,可以使用它防止垃圾回收器回收托管对象。如果没有这样的句柄,则在该对象代表非托管客户端完成工作以前,有可能被垃圾回收器回收。 用 GCHandle 创建一个固定对象,该对象返回��个内存地址,并防止垃圾回收器在内存中移动该对象。 超出范围时,您必须通过调用Free方法显式释放它;否则,可能会发生内存泄漏。当您释放固定的句柄时,如果没有对关联对象的其他引用,则关联对象将解除固定,并可以被当成垃圾回收。也就是说,通过GCHandler.Alloc申请的内存,必须通过调用GCHandler.Free方法来手动释放。表面上看来,WaveHeader的实现中,的确有手动释放这些申请的内存。5. WaveHeader在哪儿使用SpeechSynthesizer封装了一对东西,有点儿绕,我顺着Speak方法找了N久,没有找到使用WaveHeader的地方。只好换另一个思路:在上面的第1步中,我们可以看到,WaveHeader的MT Address为56105b6c,于是:0:000> !dumpheap -MT 56105b6c//。。。省却6万多行。。。 127fabdc 56105b6c 52 12800a64 56105b6c 52 //找到一个还活着的对象total 63264 objectsStatistics:MT Count TotalSize Class Name56105b6c 63264 3289728 System.Speech.Internal.Synthesis.WaveHeaderTotal 63264 objects然后找到一个还活着的对象(地址为12800a64),看看它被谁引用了:0:000> !gcroot 12800a64Note: Roots found on stacks may be false positives. Run "!help gcroot" formore info.Scan Thread 0 OSTHread 3668ESP:1df238:Root:01f67e1c(System.Speech.Internal.Synthesis.VoiceSynthesis)->01f6841c(System.Speech.Internal.Synthesis.AudioDeviceOut)->01f68570(System.Collections.Generic.List`1[[System.Speech.Internal.Synthesis.AudioDeviceOut+InItem, System.Speech]])->02f91c10(System.Object[])->020e4088(System.Speech.Internal.Synthesis.AudioDeviceOut+InItem)->12800a64(System.Speech.Internal.Synthesis.WaveHeader)Scan Thread 2 OSTHread 2c5cScan Thread 3 OSTHread 1160Scan Thread 4 OSTHread 211cScan Thread 6 OSTHread 22d4Scan Thread 7 OSTHread 2fccScan Thread 8 OSTHread 377c这里,我们终于可以看到路径了: SpeechSynthesizer->VoiceSynthesis->AudioDeviceOut-> List -> AudioDeviceOut + InItem(内部类)-> WaveHeader终于找到目标了:AudioDeviceOut。

6. 怎么使用WaveHeaderAudioDeviceOut的代码页有点长,这里只截出部分使用WaveHeader的代码:1: internal override void Play(byte[] buffer)2: {3: if (this._deviceOpen)4: {5: int length = buffer.Length;6: this._bytesWritten += length;7: WaveHeader waveHeader = new WaveHeader(buffer);//上面第3节提到,这里通过GCHandler.Alloc申请了内存8: GCHandle wAVEHDR = waveHeader.WAVEHDR;//上面第3节提到,这里通过GCHandler.Alloc申请了内存9: MMSYSERR errorCode = SafeNativeMethods.waveOutPrepareHeader(this._hwo, wAVEHDR.AddrOfPinnedObject(), waveHeader.SizeHDR);10: if (errorCode != MMSYSERR.NOERROR)11: {12: throw new AudioException(errorCode);13: }14: lock (this._noWriteOutLock)15: {16: if (!base._aborted)17: {18: lock (this._queueIn)19: {20: InItem item = new InItem(waveHeader);//InItem封装了一下WaveHeader,也没啥21: this._queueIn.Add(item); //将Item加入到List中22: this._evt.Reset();23: }24: errorCode = SafeNativeMethods.waveOutWrite(this._hwo, wAVEHDR.AddrOfPinnedObject(), waveHeader.SizeHDR);//P/Invoke向设备输出数据25: if (errorCode != MMSYSERR.NOERROR)26: {27: lock (this._queueIn)28: {29: this._queueIn.RemoveAt(this._queueIn.Count - 1);//如果出错了,从List删除一个Item30: throw new AudioException(errorCode);31: }32: }33: }34: }35: }36: }在这里,Play方法了面,终于实例化了WaveHeader和WaveHDR(GCHandler);但是实例化的WaveHeader又被InItem封装了一下,加入到List _queueIn中。Play方法里面创建了WaveHeader,但是却没有看到释放WaveHeader的代码。那我们再来看下,其他地方有没有释放WaveHeader的代码:既然InItem/WaveHeader被加入到_queueIn中了,程序应该还要使用WaveHeader;那么当InItem从该_queueIn中移出的时候,应该就会释放WaveHeader了吧。顺着这个思路继续往下看。。。很不幸,就在Play方法里面:当判断播放出错的时候,会把InItem从_queueIn中移出(上面第29行),但是却没有释放InItem/WaveHeader。另一个将InItem从_queueIn中移出的地方是CallBackProc,AudioDeviceOut在构造函数中初始化了一个委托:this._delegate = new SafeNativeMethods.WaveOutProc(this.CallBackProc); 从字面上理解,应该是输出声音完毕后,调用这个回调函数,来释放内存:1: private void CallBackProc(IntPtr hwo, MM_MSG uMsg, IntPtr dwInstance, IntPtr dwParam1, IntPtr dwParam2)2: {3: if (uMsg == MM_MSG.MM_WOM_DONE)4: {5: lock (this._queueIn)6: {7: InItem item = this._queueIn[0];8: item.ReleaseData();//释放InItem/WaveHeader9: this._queueIn.RemoveAt(0); //移除InItem10: this._queueOut.Add(item);11: while (this._queueIn.Count > 0)12: {13: item = this._queueIn[0];14: if (item._waveHeader != null)15: {16: break;17: }18: if ((this._asyncDispatch != null) && !base._aborted)19: {20: this._asyncDispatch.Post(item._userData);21: }22: this._queueIn.RemoveAt(0);//移除InItem,但是却没有释放InItem/WaveHeader23: }24: }25: if (this._queueIn.Count == 0)26: {27: this._evt.Set();28: }29: }30: }代码中,可以看到,当uMsg == MM_MSG.MM_WOM_DONE,才会执行释放操作。MM_MSG枚举有三个可选值:MM_WOM_CLOSE = 0x3bc, MM_WOM_DONE = 0x3bd, MM_WOM_OPEN = 0x3bb,暂时不太清楚是回调函数的uMsg参数 否可能会传入其他值;如果传入了其他值,显然InItem/WaveHeader有没有被释放。即使uMsg == MM_MSG.MM_WOM_DONE,我们来看下释放的代码,上面第8~9行,从_queueIn中移除InItem/WaveHeader的时候,的确执行了ReleaseData()来释放内存,但是看该方法的源代码:1: //happyhippy.cnblogs.com2: //InItem3: internal void ReleaseData()4: {5: if (this._waveHeader != null)6: {7: this._waveHeader.ReleaseData();8: }9: }10: //WaveHeader11: internal void ReleaseData()12: {13: if (this._gcHandle.IsAllocated)14: {15: this._gcHandle.Free();16: }17: }18: 里面调用WaveHeader.ReleaseData()来释放内存,不是调用Dispose来释放内存。上面提到,Play方法中,第8行GCHandle wAVEHDR = waveHeader.WAVEHDR申请了内存,但是ReleaseData()方法并没有释放该内存。应该调用Dispose来释放内存,而不是调用ReleaseData()。然后在回过头来看CallBackProc方法,从源代码中可以看到,其只释放了_queueIn中第一个InItem/WaveHeader,后续的InItem/WaveHeader从_queueIn中移除的时候,并没有释放内存。GCHandler.Alloc申请的内存,必须通过调用Free方法显式释放它;否则,可能会发生内存泄漏。7. 解决办法无解。那封装的一大坨,都是Internal。

声明:希维路由器教程网提供的内容,仅供网友学习交流,如有侵权请与我们联系删除,谢谢。ihuangque@qq.com
本文地址:https://www.ctrlcv.com.cn/diannao/169323482810686.html