一 基礎知識小程序
在分析以前,先上一張圖:數組
從上面能夠看到,這個w3wp進程佔用了376M內存,啓動了54個線程。安全
在使用windbg查看以前,看到的進程含有 *32 字樣,意思是在64位機器上已32位方式運行w3wp進程。這個能夠經過查看IIS Application Pool 的高級選項進行設置:服務器
好了,接下打開Windbg看看這個w3wp進程佔用了376M內存,啓動的54個線程。併發
1. 加載 WinDbg SOS 擴展命令dom
.load C:\Windows\Microsoft.NET\Framework\v2.0.50727\sos.dllide
2. !dumpheap -stat測試
!DumpHeap 將遍歷 GC 堆對對象進行分析。ui
MT Count TotalSize Class Name
78eb9834 1 12 System.ServiceModel.ServiceHostingEnvironment+HostingManager+ExtensionHelperthis
0118c800 101 14824 Free
...
63ce0004 19841 1111096 System.Reflection.RuntimeMethodInfo
63ce2ee4 11080 2061036 System.Int32[]
63ce0d48 34628 2242596 System.String
63ce37b8 20012 3264884 System.Byte[]
63cb4518 157645 4940676 System.Object[]
Total 524310 objects
能夠看到,w3wp上總共有524310個對象, 共佔用了這些內存。
咱們能夠將上述上述列出的這些對象歸爲2類:
1). 有根對象(在應用程序中對這些對象存在引用)
2). 自從上次垃圾回收以後新建立或無跟對象
要注意的是Free這項:
0118c800 101 14824 Free
這項通常都是GC not yet Compacted的空間或一些堆上分配的禁止GC compacted釘釦對象.
第一欄 : 類型的方法列表 MT(method type for the type)
第二欄:堆上的對象數量
第三欄:全部同類對象的總大小
3. !dumpheap -mt 63ce0d48
查看 63ce0d48 單元的有哪些對象。
4. !do 103b3360
看看103b3360地址的string包含哪些內容
可見,103b3360地址的字符串value="System.Web.UI.PageRequestManager:AsyncPostBackError", 佔120bytes. 這個字符串對象包含3個字段,它們的偏移量分別是4,8,12。
5. dd 103b3360
看看103b3360的值
從左往右第一列是地址,而第二列開始則是地址上的數據。
6. !dumpheap -type String -min 100
看看堆上全部大於100字節的字符串。 注意:假如 -min 85000(大於85000字節的字符串或對象將存儲在大對象堆上).
二. NET內存泄露分析案例
1 基礎認識
.net世界裏,GC是負責垃圾回收的,但GC僅僅是回收哪些不可及的對象(無根對象),對於有應用的有根對象,GC對此無能爲力。
.net一些內存泄漏的根本緣由:
一些避免內存泄漏的建議:
對這些作基本瞭解後,咱們將步入正題。
2. 案例分析
先上測試代碼:
1 public class LeakTest 2 { 3 private static string leakString; 4 5 public LeakTest() 6 { 7 for (int i = 0; i < 1000; i++) 8 { 9 leakString += "LEAK"; 10 } 11 } 12 13 public string GetRamdonString() 14 { 15 System.Random random = new System.Random(); 16 17 string str = string.Empty; 18 for (int i = 0; i < 25; i++) 19 { 20 str += str + random.Next(100); 21 } 22 return str; 23 } 24 25 public void NoDispose() 26 { 27 string str = GetRamdonString(); 28 29 ZipFile zip = new ZipFile(); 30 zip.AddEntry("a.txt", str); 31 zip.AddEntry("b.txt", str); 32 zip.Save("test.rar"); 33 //zip.Dispose(); 34 } 35 } 36 37 class Program 38 { 39 static void Main(string[] args) 40 { 41 LeakTest leakTest = new LeakTest(); 42 leakTest.NoDispose(); 43 Console.ReadLine(); 44 } 45 }
須要說明的是:
這裏程序裏面定義了一個Static 字符串,及使用了Ionic.Zip 這個Zip壓縮包,僅僅是爲了模擬內存堆積現象,沒有調用zip.Dispose()方法,事實上Ionic.Zip並不會形成內存泄露。
正式開始了:
啊哈,好極了。 運行程序,好傢伙,果真很耗費內存! 這麼個小程序,吃了287M,並啓動了12個線程.
0:005> .load C:\Windows\Microsoft.NET\Framework64\v2.0.50727\sos.dll
0:005> .load C:\Symbols\sosex_64\sosex.dll
0:005> !dumpheap -stat
1 0:012> !dumpheap -stat 2 PDB symbol for mscorwks.dll not loaded 3 total 12840 objects 4 Statistics: 5 MT Count TotalSize Class Name 6 000007ff001d2248 1 24 System.Collections.Generic.Dictionary`2+ValueCollection[[System.String, mscorlib],[Ionic.Zip.ZipEntry, Ionic.Zip.Reduced]] 7 000007ff000534f0 1 24 ZipTest.LeakTest 8 000007fee951e8e8 1 24 System.IO.TextReader+NullTextReader 9 000007fee94f8198 1 24 System.Security.Cryptography.RNGCryptoServiceProvider
11 ...
43 000007ff001d9130 1041 66624 Ionic.Zlib.DeflateManager+CompressFunc 44 000007fee94d2d40 1023 73656 System.Threading.ExecutionContext 45 000007fee951e038 3075 1387592 System.UInt32[] 46 000007fee951ca10 3179 2450704 System.Int16[] 47 0000000000207800 261 67034512 Free 48 000007fee94d7d90 514 134251544 System.String 49 000007fee94dfdd0 102 138593344 System.Byte[] 50 Total 12840 objects
果真,咱們看到了裏面有2類大對象,分別佔用了134M和138M . 好傢伙!
0:005> !dumpheap -mt
1 0:012> !dumpheap -mt 000007fee94dfdd0 2 Address MT Size 3... 24 00000000026f11f0 000007fee94dfdd0 65560 25 0000000002701288 000007fee94dfdd0 65560 26 00000000027112a0 000007fee94dfdd0 65592 27 0000000002722b50 000007fee94dfdd0 65560 28 0000000002752b98 000007fee94dfdd0 65560 29 ... 47 000000000290ab98 000007fee94dfdd0 65560 48 000000000293abe0 000007fee94dfdd0 65560 49 ... 64 0000000002ac1378 000007fee94dfdd0 65560 65 0000000002ad1410 000007fee94dfdd0 65560 66... 103 00000000165a71e0 000007fee94dfdd0 67108888 104 0000000027c11000 000007fee94dfdd0 67108888 105 total 102 objects 106 Statistics: 107 MT Count TotalSize Class Name 108 000007fee94dfdd0 102 138593344 System.Byte[] 109 Total 102 objects
果真,有那麼多65592和65560啊 啊
隨便找一個看一下:
0:005> !do 0000000002ba4790
1 0:012> !do 0000000002ba4790 2 Name: System.Byte[] 3 MethodTable: 000007fee94dfdd0 4 EEClass: 000007fee90e26b0 5 Size: 65590(0x10036) bytes 6 Array: Rank 1, Number of elements 65566, Type Byte 7 Element Type: System.Byte 8 Fields: 9 None
哦。這是個一維的數組,有65566字節,推測應該好像是short(int16)長度。
繼續,
!gcroot 0000000002b42dd0
0:012> !gcroot 0000000002b42dd0 Note: Roots found on stacks may be false positives. Run "!help gcroot" for more info. Scan Thread 0 OSTHread 1d3c RSP:18ef58:Root:00000000025c5b88(Ionic.Zip.ZipFile)-> 00000000025d2578(Ionic.Zlib.ParallelDeflateOutputStream)-> 00000000025dc528(System.Collections.Generic.List`1[[Ionic.Zlib.WorkItem, Ionic.Zip.Reduced]])-> 000000000294ac38(System.Object[])-> 0000000002b32d78(Ionic.Zlib.WorkItem)-> 0000000002b42dd0(System.Byte[]) ... Scan Thread 10 OSTHread 3718
這裏有點看頭了! 看其跟對象 Ionic.Zip.ZipFile 這個對象佔着沒銷燬的內存呢!
RSP:18ef58:Root:00000000025c5b88(Ionic.Zip.ZipFile)->
00000000025d2578(Ionic.Zlib.ParallelDeflateOutputStream)->
00000000025dc528(System.Collections.Generic.List`1[[Ionic.Zlib.WorkItem, Ionic.Zip.Reduced]])->
000000000294ac38(System.Object[])->
0000000002b32d78(Ionic.Zlib.WorkItem)->
0000000002b42dd0(System.Byte[])
換一個看看:
0:012> !gcroot 00000000029bc730 Note: Roots found on stacks may be false positives. Run "!help gcroot" for more info. Scan Thread 0 OSTHread 1d3c RSP:18ef58:Root:00000000025c5b88(Ionic.Zip.ZipFile)-> 00000000025d2578(Ionic.Zlib.ParallelDeflateOutputStream)-> 00000000025dc528(System.Collections.Generic.List`1[[Ionic.Zlib.WorkItem, Ionic.Zip.Reduced]])-> 000000000294ac38(System.Object[])-> 00000000029ac6d8(Ionic.Zlib.WorkItem)-> 00000000029bc730(System.Byte[]) ... Scan Thread 10 OSTHread 3718
查看下其代齡:
0:012> !gcgen 00000000029bc730
GEN 1
看到了,這個byte[]在1代。
到此爲止,還記得有個靜態字符串吧
private static string leakString;
咱們回頭再去看看,
!dumpheap -type String -min 1000
0:012> !dumpheap -type String -min 1000 Address MT Size 00000000025c26e0 000007fee94d7d90 8032 00000000025cca30 000007fee94d7d90 1176 00000000025cd308 000007fee94d7d90 1600 000000001ae81000 000007fee94d7d90 134215704 total 4 objects Statistics: MT Count TotalSize Class Name 000007fee94d7d90 4 134226512 System.String Total 4 objects
Next,
0:012> !do 00000000025c26e0
0:012> !do 00000000025c26e0 Name: System.String MethodTable: 000007fee94d7d90 EEClass: 000007fee90de560 Size: 8026(0x1f5a) bytes (C:\Windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll) String: LEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKL....
EAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAK Fields: MT Field Offset Type VT Attr Value Name 000007fee94df000 4000096 8 System.Int32 1 instance 4001 m_arrayLength 000007fee94df000 4000097 c System.Int32 1 instance 4000 m_stringLength 000007fee94d97d8 4000098 10 System.Char 1 instance 4c m_firstChar 000007fee94d7d90 4000099 20 System.String 0 shared static Empty >> Domain:Value 000000000062b1d0:00000000025c1308 << 000007fee94d9688 400009a 28 System.Char[] 0 shared static WhitespaceChars >> Domain:Value 000000000062b1d0:00000000025c1a90 <<
再看下這個對象:
!dumpobj 00000000025c26e0
0:012> !dumpobj 00000000025c26e0 Name: System.String MethodTable: 000007fee94d7d90 EEClass: 000007fee90de560 Size: 8026(0x1f5a) bytes (C:\Windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll)
(C:\Windows\assembly\GAC_64\mscorlib\2.0.0.0__b77a5c561934e089\mscorlib.dll) String: LEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKL....
EAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAKLEAK Fields:
MT Field Offset Type VT Attr Value Name 000007fee94df000 4000096 8 System.Int32 1 instance 4001 m_arrayLength 000007fee94df000 4000097 c System.Int32 1 instance 4000 m_stringLength 000007fee94d97d8 4000098 10 System.Char 1 instance 4c m_firstChar 000007fee94d7d90 4000099 20 System.String 0 shared static Empty >> Domain:Value 000000000062b1d0:00000000025c1308 << 000007fee94d9688 400009a 28 System.Char[] 0 shared static WhitespaceChars >> Domain:Value 000000000062b1d0:00000000025c1a90 <<
顯示結果同樣,String:LEAKLEAKLEAKLEAKLEAK......,字符串長度4000,和咱們的測試代碼吻合:
1 public LeakTest() 2 { 3 for (int i = 0; i < 1000; i++) 4 { 5 leakString += "LEAK"; 6 } 7 }
到此,內存查看分析演示的差很少了!
這裏咱們演示的是個小得不能再小的程序,且存在前提預期。 假如在實際項目環境中,由於引用的DLL多,生成的對象繁雜,實際診斷問題根源就複雜得多,這就須要比較紮實的基本功。
三. 死鎖排查
1. 基礎
仍是用上面的Console App例子,運行這個程序,啓動了13個線程。咱們先看一下這13個線程:
!runaway
0:012> !runaway User Mode Time Thread Time 0:5588 0 days 0:00:05.085 7:4954 0 days 0:00:01.903 3:4ddc 0 days 0:00:01.825 8:5af4 0 days 0:00:01.809 9:4740 0 days 0:00:01.747 10:6c38 0 days 0:00:01.731 4:6a94 0 days 0:00:01.700 5:43ec 0 days 0:00:01.622 6:8fdc 0 days 0:00:01.606 12:1e64 0 days 0:00:00.000 11:6a4 0 days 0:00:00.000 2:64b4 0 days 0:00:00.000 1:69e4 0 days 0:00:00.000
恩。13個線程,沒錯。 這裏還能夠看到每一個線程的執行時間。 其中 0 線程佔用時間最多。咱們去看下堆棧調用:
~0s
!ClrStack -a
0:012> ~0s ntdll!ZwRequestWaitReplyPort+0xa: 00000000`77b714da c3 ret 0:000> !ClrStack -a OS Thread Id: 0x5588 (0) *** WARNING: Unable to verify checksum for C:\Windows\assembly\NativeImages_v2.0.50727_64\mscorlib\c3beeeb6432f004b419859ea007087f1\mscorlib.ni.dll Child-SP RetAddr Call Site 00000000001de670 000007fee9b02c79 DomainNeutralILStubClass.IL_STUB(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte*, Int32, Int32 ByRef, IntPtr) PARAMETERS: <no data> <no data> <no data> <no data> <no data> 00000000001de790 000007fee9b02d92 System.IO.__ConsoleStream.ReadFileNative(Microsoft.Win32.SafeHandles.SafeFileHandle, Byte[], Int32, Int32, Int32, Int32 ByRef) PARAMETERS: hFile = <no data> bytes = <no data> offset = <no data> count = <no data> mustBeZero = <no data> errorCode = 0x00000000001de820 LOCALS: <no data> 0x00000000001de7c0 = 0x0000000000000000 <no data> <no data> 00000000001de7f0 000007fee93f08da System.IO.__ConsoleStream.Read(Byte[], Int32, Int32) PARAMETERS: this = <no data> buffer = <no data> offset = <no data> count = <no data> LOCALS: 0x00000000001de820 = 0x0000000000000000 <no data> 00000000001de850 000007fee9412a8a System.IO.StreamReader.ReadBuffer() PARAMETERS: this = <no data> LOCALS: <no data> 00000000001de8a0 000007fee9b0622f System.IO.StreamReader.ReadLine() PARAMETERS: this = <no data> LOCALS: <no data> <no data> <no data> <no data> 00000000001de8f0 000007ff00190188 System.IO.TextReader+SyncTextReader.ReadLine() PARAMETERS: this = 0x00000000030387b0 00000000001de950 000007feea23c6a2 ZipTest.Program.Main(System.String[]) PARAMETERS: args = 0x00000000027e2680 LOCALS: 0x00000000001de970 = 0x00000000027e26a0
瞧準了,這是個主線程,他在等待Console.ReadLine(). 因此佔用了這麼長時間。
再在看一下這13個線程裏,哪些是託管堆線程:
!threads
0:012> !threads ThreadCount: 10 UnstartedThread: 0 BackgroundThread: 9 PendingThread: 0 DeadThread: 0 Hosted Runtime: no PreEmptive Lock ID OSID ThreadOBJ State GC GC Alloc Context Domain Count APT Exception 0 1 5588 00000000009d4510 a020 Enabled 00000000030387d0:000000000303a510 00000000009cb1d0 1 MTA 2 2 64b4 00000000009dc4d0 b220 Enabled 0000000000000000:0000000000000000 00000000009cb1d0 0 MTA (Finalizer) 3 3 4ddc 0000000000a1a010 180b220 Enabled 0000000002fe1e28:0000000002fe2450 00000000009cb1d0 0 MTA (Threadpool Worker) 4 4 6a94 0000000000a1d590 180b220 Enabled 0000000002fe73c8:0000000002fe8450 00000000009cb1d0 0 MTA (Threadpool Worker) 5 5 43ec 0000000000a7bbd0 180b220 Enabled 0000000002fec968:0000000002fee450 00000000009cb1d0 0 MTA (Threadpool Worker) 6 6 8fdc 0000000000a892b0 180b220 Enabled 0000000002ff0968:0000000002ff2450 00000000009cb1d0 0 MTA (Threadpool Worker) 7 7 4954 0000000000aa3270 180b220 Enabled 0000000002fee968:0000000002ff0450 00000000009cb1d0 0 MTA (Threadpool Worker) 8 8 5af4 0000000000a97eb0 180b220 Enabled 0000000002fe8968:0000000002fea450 00000000009cb1d0 0 MTA (Threadpool Worker) 9 9 4740 0000000000a99400 180b220 Enabled 0000000002fe0358:0000000002fe0450 00000000009cb1d0 0 MTA (Threadpool Worker) 10 a 6c38 0000000000a9f3a0 180b220 Enabled 0000000002fe3e28:0000000002fe4450 00000000009cb1d0 0 MTA (Threadpool Worker)
在託管堆上啓動的線程有10個。這10個線程分別是什麼,繼續看:
0號MTA: 程序主線程
MTA (Finalizer):這個是Finalizer線程,該線程負責垃圾對象回收。
MTA (Threadpool Worker):這些是ThreadPool建立的線程,這裏是Ionic.Zlib.WorkItem產生的工做線程。
另外,CLR根據須要還會開啓其餘一些線程,如:
併發的GC線程 ,服務器GC線程 ,調試器幫助線程 ,AppDomain卸載線程 等.
看一下同步塊狀況,有麼有死鎖?
!syncblk
!dlk
0:003> !dlk Examining SyncBlocks... Scanning for ReaderWriterLock instances... Scanning for holders of ReaderWriterLock locks... Scanning for ReaderWriterLockSlim instances... Scanning for holders of ReaderWriterLockSlim locks... Examining CriticalSections... No deadlocks detected.
顯示該程序沒有鎖相關資源,實際確實沒有。
2 死鎖
Lock:lock 關鍵字將語句塊標記爲臨界區,方法是獲取給定對象的互斥鎖,執行語句,而後釋放該鎖。 下面的示例包含一個 lock 語句。
lock 關鍵字可確保當一個線程位於代碼的臨界區時,另外一個線程不會進入該臨界區。 若是其餘線程嘗試進入鎖定的代碼,則它將一直等待(即被阻止),直到該對象被釋放。
一般,應避免鎖定 public 類型,不然實例將超出代碼的控制範圍。 常見的結構 lock (this)、lock (typeof (MyType)) 和 lock ("myLock") 違反此準則:
若是實例能夠被公共訪問,將出現 lock (this) 問題。
若是 MyType 能夠被公共訪問,將出現 lock (typeof (MyType)) 問題。
因爲進程中使用同一字符串的任何其餘代碼都將共享同一個鎖,因此出現 lock("myLock") 問題。
最佳作法是定義 private 對象來鎖定, 或 private static 對象變量來保護全部實例所共有的數據。
3 案例分析
這個案例很簡單,上菜:
1 public class Consumer1 2 { 3 private string connString; 4 public Consumer1(string str) 5 { 6 this.connString = str; 7 } 8 } 9 10 public class Consumer2 11 { 12 private string connString; 13 public Consumer2(string str) 14 { 15 this.connString = str; 16 } 17 } 18 19 class Program 20 { 21 private static Consumer1 consumer1; 22 private static Consumer2 consumer2; 23 24 static void Main(string[] args) 25 { 26 consumer1 = new Consumer1("Conn1"); 27 consumer2 = new Consumer2("Conn2"); 28 29 Thread thread = new Thread(Proc); 30 thread.Start(); 31 32 lock (consumer2) 33 { 34 Console.WriteLine("Proc->Lock consumer2"); 35 Thread.Sleep(1000); 36 lock (consumer1) 37 { 38 Console.WriteLine("Proc->Lock consumer2->Lock consumer1 "); 39 } 40 } 41 42 } 43 44 private static void Proc() 45 { 46 lock (consumer1) 47 { 48 Console.WriteLine("Proc->Lock consumer1"); 49 Thread.Sleep(1000); 50 lock (consumer2) 51 { 52 Console.WriteLine("Proc->Lock consumer1->Lock consumer2 "); 53 } 54 } 55 56 } 57 }
運行程序,便進入死鎖。
ok,上 windbg.
.load C:\Symbols\sosex_64\sosex.dll
0:000> !dlk
1 0:000> !dlk 2 Examining SyncBlocks... 3 Scanning for ReaderWriterLock instances... 4 Scanning for holders of ReaderWriterLock locks... 5 Scanning for ReaderWriterLockSlim instances... 6 Scanning for holders of ReaderWriterLockSlim locks... 7 Examining CriticalSections... 8 Scanning for threads waiting on SyncBlocks... 9 *** WARNING: Unable to verify checksum for D:\Test\PInvoke\CPP\Test\bin\Debug\Test.exe 10 Scanning for threads waiting on ReaderWriterLock locks... 11 Scanning for threads waiting on ReaderWriterLocksSlim locks... 12 Scanning for threads waiting on CriticalSections... 13 *DEADLOCK DETECTED* 14 CLR thread 0x3 holds the lock on SyncBlock 0000000000c94690 OBJ:00000000027736b8[Test.Consumer1] 15 ...and is waiting for the lock on SyncBlock 0000000000c946d8 OBJ:00000000027736d0[Test.Consumer2] 16 CLR thread 0x1 holds the lock on SyncBlock 0000000000c946d8 OBJ:00000000027736d0[Test.Consumer2] 17 ...and is waiting for the lock on SyncBlock 0000000000c94690 OBJ:00000000027736b8[Test.Consumer1] 18 CLR Thread 0x3 is waiting at Test.Program.Proc()(+0x31 IL,+0x98 Native) [D:\Test\PInvoke\CPP\Test\Program.cs @ 60,17] 19 CLR Thread 0x1 is waiting at Test.Program.Main(System.String[])(+0x68 IL,+0x196 Native) [D:\Test\PInvoke\CPP\Test\Program.cs @ 46,17] 20 21 22 1 deadlock detected.
只需敲一個命令,死鎖就檢測到了。 注意下面這些地址:
0000000000c94690 OBJ:00000000027736b8
0000000000c946d8 OBJ:00000000027736d0
0000000000c946d8 OBJ:00000000027736d0
0000000000c94690 OBJ:00000000027736b8
!mdt 00000000027736b8 看下,把這四個地址都看下:
1 0:000> !mdt 00000000027736b8 2 00000000027736b8 (Test.Consumer1) 3 connString:00000000027735b8 (System.String) Length=5, String="Conn1" 4 0:000> !mdt 00000000027736d0 5 00000000027736d0 (Test.Consumer2) 6 connString:00000000027735e0 (System.String) Length=5, String="Conn2" 7 0:000> !mdt 00000000027736d0 8 00000000027736d0 (Test.Consumer2) 9 connString:00000000027735e0 (System.String) Length=5, String="Conn2" 10 0:000> !mdt 00000000027736b8 11 00000000027736b8 (Test.Consumer1) 12 connString:00000000027735b8 (System.String) Length=5, String="Conn1"
明瞭了。
對比下上面那個程序,瞧一瞧,是否是這幾個對像,連對象裏的字符串值都盡收眼底!