javawindbg gcroott 可以是thread么

浅谈并小结java内存泄漏
浅谈并小结java内存泄漏
首先什么是内存泄漏,简单点说就是用完了忘了回收,而其他对象等资源想用却没法用的一种“站着茅坑不拉屎”的浪费资源的情况。在C/C++中,多数泄漏的场景就是程序离开某一运行域时,如在某个方法体中new出的对象或者malloc出的结构体等,并且只有该方法体中的局部变量指向这块内存区域时,在该方法返回时,存在栈中的局部变量指针随着栈帧被一起销毁,那么就没有任何指针可以指向该内存区域了,那么这块内存区域便是泄漏了。
而java的内存泄漏呢?众所周知,java的内存回收是由gc管理的。gc运行的是可达性算法,jvm中的gc线程将java对象从一个特定的对象gcroot开始,看成一个树,遍历树以判断对象的可达性,和C++那种静态的指针计数拥有本质区别,不存在无用对象间相互引用造成的困扰。也就是说当java对象真正确实没有任何一个来自存活对象的(强)引用(弱引用等除外)的时候(如果如果引用来自不可达对象,那么该对象仍然是不可达的),java对象才可能会被回收。也就是说,这里的java内存泄漏和传统的C/C++是不尽相同的。
下图是java的可达性算法:
二.什么是java的内存泄漏
个人理解的java内存泄漏,其实可以描述为:对象遗忘或者说是找不到引用。因为在java中如果对象还在heap中存活的话(死亡对象gc回收之前除外),那么必然在整个环境中有来自存活对象的引用指向这个对象,简而言之,就是“此人还活着必有其存在的意义”。
那么问题来了,那个引用在哪呢?我们开发者根本在代码里看不见啊?别急,这个就是java内存泄漏的关键所在。很多时候这些看不见的引用藏在了某框架中,某静态变量中,或者某还在运行的子线程中。。。。
下面举几个例子说不定你就明白了。
三.常见场景
1.HashMap,有时候,我们一不小心把可变hashcode的对象作为HashMap的key,当你一不小心改变了对象的hashcode的时候,key就无法取出对应的value,此value对于你便不可达了。但是value的引用还是存在与HashMap框架内部的数组和链表结构中。而你又无可奈何,内存便”泄漏“了。
2.Android中常见的Context泄漏(Activity代表)。在Android中,给Context对象引用要小心处理,究其根源是因为Android应用层框架帮我们管理了Activity(Context)对象的生命周期,这类差不多可以代表我们现在常用的IOC框架,框架管理对象的生灭,开发者只有应用权。如果你不小心将Context对象塞进了哪个静态变量中,或者引用到了哪个长周期的子线程中等,在Activity(Context)结束时,框架层跑完所有销毁程序,移除了它对Context的引用,这时框架层就认为这个Context已经死亡了。而框架却没有想到在他外部还有其他引用没有释放。这样就造成了泄漏。
3.静态变量,这个不用过多阐述,很好理解。
4.资源对象用完没有释放,如数据库,流这些资源类,如Cusor,File.Socket等。原因是因为这些与本地平台交互的接口涉及JNI,可能有native层的指针引用了java层的对象,没有释放的话,引用是一直存在的。
5.非静态的内部类,尤见于Activity内部的Handler和Runnable等,和上一个阐述的关联起来。非静态内部类包含一个对外部类的强引用,而且这个引用是编译器自己加的,开发者在源码中看不见,故比较隐蔽。
如果你反编译一个在Activity中的内部类你会发现:
# instance fields
.field final synthetic this$0:Lcom/gy/just/VoltageMonitor/View/Activity/YunWeiA
.field final synthetic val$sec:I
# direct methods
.method constructor &init&(Lcom/gy/just/VoltageMonitor/View/Activity/YunWeiAI)V
.param p1, &this$0&
# Lcom/gy/just/VoltageMonitor/View/Activity/YunWeiA编译器自动给内部类加了一个叫“this$0”的外部类变量,并且在构造函数中赋值。那么内部类中就持有有了一个不可见的外部类强引用。像Runnable,Handler这类的内部类如果子线程没有结束的话,那么外部类对象的泄漏就容易发生了。
那么,我们来做一个有趣的小实验,我们如果把内部类中外部类的引用置空的话,会发生什么有趣的现象?
public class B extends A{
public void test(){
new Thread(new Demo()).start();
public void dosth() {
// TODO Auto-generated method stub
System.out.println(&doB&);
class Demo implements Runnable{
public Demo(){
Field field = getClass().getDeclaredField(&this$0&);
field.setAccessible(true);
field.set(this, null);
} catch (NoSuchFieldException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (SecurityException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalArgumentException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (IllegalAccessException e) {
// TODO Auto-generated catch block
e.printStackTrace();
public void run() {
// TODO Auto-generated method stub
运行异常如下
public abstract interface java.util.List&E&
Exception in thread &Thread-0& java.lang.NullPointerException
at test.B$Demo.run(B.java:46)
at java.lang.Thread.run(Thread.java:745)
怎么样?内部类Demo已经无法调用外部类的dosth方法了,抛出空指针异常。
四.总结,严格意义上说java是没有真正的内存泄漏的,这些泄漏和我们看不见的框架内部息息相关,所以养成良好的编程习惯,理解理解常用框架的结构原理,就能避免这些错误。最后,如果大家觉得小弟理解的还不错的,不妨给个赞,道友们觉得理解上有错误的也欢迎指正,毕竟这是我的个人看法。
我的热门文章
即使是一小步也想与你分享内存泄漏(1)
android_性能优化(9)
Android 提高流畅度(3)
/blog/1779651
/blog/1779651
/blog/1779651
博客分类:&
在网上看到一篇不错的文章,记录下来备忘。
要理解java对象的生命周期,我们需要要明白两个问题,
&1、java是怎么分配内存的 ,2、java是怎么回收内存的。
喜欢java的人,往往因为它的内存自动管理机制,不喜欢java的人,往往也是因为它的内存自动管理。我属于前者,这几年的coding经验让我认识到,要写好java程序,理解java的内存管理机制是多么的重要。任何语言,内存管理无外乎分配和回收,在C中我们可以用malloc动态申请内存,调用free释放申请的内存;在C++中,我们可以用new操作符在堆中动态申请内存,编写析构函数调用delete释放申请的内存;那么在java中究竟是内存怎样管理的呢?要弄清这个问题,我们首先要了解java内存的分配机制,在java虚拟机规范里,JVM被分为7个内存区域,但是规范这毕竟只是规范,就像我们编写的接口一样,虽然最终行为一致,但是个人的实现可能千差万别,各个厂商的JVM实现也不尽相同,在这里,我们只针对sun的Hotspot虚拟机讨论,该虚拟机也是目前应用最广泛的虚拟机。
& 虚拟器规范中的7个内存区域分别是三个线程私有的和四个线程共享的内存区,线程私有的内存区域与线程具有相同的生命周期,它们分别是: 指令计数器、 线程栈和本地线程栈,四个共享区是所有线程共享的,在JVM启动时就会分配,分别是:方法区、 常量池、直接内存区和堆(即我们通常所说的JVM的内存分为堆和栈中的堆,后者就是前面的线程栈)。接下来我们逐一了解这几个内存区域。
& & 1 指令计数器。我们都知道java的多线程是通过JVM切换时间片运行的,因此每个线程在某个时刻可能在运行也可能被挂起,那么当线程挂起之后,JVM再次调度它时怎么知道该线程要运行那条字节码指令呢?这就需要一个与该线程相关的内存区域记录该线程下一条指令,而指令计数器就是实现这种功能的内存区域。有多少线程在编译时是不确定的,因此该区域也没有办法在编译时分配,只能在创建线程时分配,所以说该区域是线程私有的,该区域只是指令的计数,占用的空间非常少,所以虚拟机规范中没有为该区域规定OutofMemoryError。
& &2 线程栈。先让我看以下一段代码:
Java代码&&
class&Test{&&
&&&&public&static&void&main(String[]&args)&{&&
&&&&&&&&Thread&th&=&new&Thread();&&
&&&&&&&&th.start();&&
& 在运行以上代码时,JVM将分配一块栈空间给线程th,用于保存方法内的局部变量,方法的入口和出口等,这些局部变量包括基本类型和对象引用类型,这里可能有人会问,java的对象引用不是分配在堆上吗?有这样疑惑的人,可能是没有理解java中引用和对象之间的区别,当我们写出以下代码时:
Java代码&&
public&Object&test(){&&
&&&&Object&obj&=&new&Object();&&
&&&&return&&&
& 其中的Object obj就是我们所说的引用类型,这样的声明本身是要占用4个字节,而这4个字节在这里就是在栈空间里分配的,准确的说是在线程栈中为test方法分配的栈帧中分配的,当方法退出时,将会随栈帧的弹出而自动销毁,而new Object()则是在堆中分配的,由GC在适当的时间收回其占用的空间。每个栈空间的默认大小为0.5M,在1.7里调整为1M,每调用一次方法就会压入一个栈帧,如果压入的栈帧深度过大,即方法调用层次过深,就会抛出StackOverFlow,,SOF最常见的场景就是递归中,当递归没办法退出时,就会抛此异常,Hotspot提供了参数设置改区域的大小,使用-Xss:xxK,就可以修改默认大小。
3 本地线程栈.顾名思义,该区域主要是给调用本地方法的线程分配的,该区域和线程栈的最大区别就是,在该线程的申请的内存不受GC管理,需要调用者自己管理,JDK中的Math类的大部分方法都是本地方法,一个值得注意的问题是,在执行本地方法时,并不是运行字节码,所以之前所说的指令计数器是没法记录下一条字节码指令的,当执行本地方法时,指令计数器置为undefined。 接下来是四个线程共享区。 1 方法区。这块区域是用来存放JVM装载的class的类信息,包括:类的方法、静态变量、类型信息(接口/父类),我们使用反射技术时,所需的信息就是从这里获取的。
2 常量池。当我们编写如下的代码时:
Java代码&&
class&Test1{&&
&&&&&private&final&int&size=50;&&
& 这个程序中size因为用final修饰,不能再修改它的值,所以就成为常量,而这常量将会存放在常量区,这些常量在编译时就知道占用空间的大小,但并不是说明该区域编译就固定了,运行期也可以修改常量池的大小,典型的场景是在使用String时,你可以调用String的 intern(),JVM会判断当前所创建的String对象是否在常量池中,若有,则从常量区取,否则把该字符放入常量池并返回,这时就会修改常量池的大小,比如JDK中java.io.ObjectStreamField的一段代码:
Java代码&&
ObjectStreamField(Field&field,&boolean&unshared,&boolean&showType)&{&&
&&&&this.field&=&&&
&&&&this.unshared&=&&&
&&&&name&=&field.getName();&&
&&&&Class&ftype&=&field.getType();&&
&&&&type&=&(showType&||&ftype.isPrimitive())&?&ftype&:&Object.class;&&
&&&&signature&=&ObjectStreamClass.getClassSignature(ftype).intern();&&
& 这段代码将获取的类的签名放入常量池。HotSpot中并没有单独为该区域分配,而是合并到方法区中。 3 直接内存区。直接内存区并不是JVM可管理的内存区。在JDK1.4中提供的NIO中,实现了高效的R/W操作,这种高效的R/W操作就是通过管道机制实现的,而管道机制实际上使用了本地内存,这样就避免了从本地源文件复制JVM内存,再从JVM复制到目标文件的过程,直接从源文件复制到目标文件,JVM通过DirectByteBuffer操作直接内存。 4 堆。主角总是最后出场,堆绝对是JVM中的一等公民,绝对的主角,我们通常所说的GC主要就是在这块区域中进行的,所有的java对象都在这里分配,这也是JVM中最大的内存区域,被所有线程共享,成千上万的对象在这里创建,也在这里被销毁。
java内存分配到这就算是一个完结了,接下来我们将讨论java内存的回收机制, 内存回收主要包含以下几个方面理解: 第一,局部变量占用内存的回收,所谓局部变量,就是指在方法内创建的变量,其中变量又分为基本类型和引用类型。如下代码:
Java代码&&
public&void&test(){&&
&&&&int&x=1;&&
&&&&char&y='a';&&
&&&&long&z=10L;&&
& 变量x y z即为局部变量,占用的空间将在test()所在的线程栈中分配,test()执行完了后会自动从栈中弹出,释放其占用的内存,再来看一段代码:
Java代码&&
public&void&test2(){&&
&&&&Date&d&=&new&Date();&&
&&&&System.out.println(&Now&is&&+d);&&
& 我们都知道上述代码会创建两个对象,一个是Date d另一个是new Date。Date d叫做声明了一个date类型的引用,引用就是一种类型,和int x一样,它表明了这种类型要占用多少空间,在java中引用类型和int类型一样占用4字节的空间,如果只声明引用而不赋值,这4个字节将指向JVM中地址为0的空间,表示未初始化,对它的任何操作都会引发空指针异常。 如果进行赋值如d = new Date()那么这个d就保存了new Date()这个对象的地址,通过之前的内存分配策略,我知道new Date()是在jvm的heap中分配的,其占用的空间的回收我们将在后面着重分析,这里我们要知道的是这个Date
d所占用的空间是在test2()所在的线程栈分配的,方法执行完后同样会被弹出栈,释放其占用的空间。 第二.非局部变量的内存回收,在上面的代码中new Date()就和C++里的new创建的对象一样,是在heap中分配,其占用的空间不会随着方法的结束而自动释放需要一定的机制去删除,在C++中必须由程序员在适当时候delete掉,在java中这部分内存是由GC自动回收的,但是要进行内存回收必须解决两问题:那些对象需要回收、怎么回收。判定那些对象需要回收,我们熟知的有以下方法: 一,引用计数法,这应是绝大数的的java
程序员听说的方法了,也是很多书上甚至很多老师讲的方法,该方法是这样描述的,为每个对象维护一个引用计数器,当有引用时就加1,引用解除时就减1,那些长时间引用为0的对象就判定为回收对象,理论上这样的判定是最准确的,判定的效率也高,但是却有一个致命的缺陷,请看以下代码:
Java代码&&
package&&&
import&java.util.ArrayL&&
import&java.util.L&&
public&class&Test&{&&
&&&&private&byte[]&&&
&&&&private&List&&&
&&&&public&Test()&{&&
&&&&&&&&this.buffer&=&new&byte[4&*&1024&*&1024];&&
&&&&&&&&this.ls&=&new&ArrayList();&&
&&&&private&List&getList()&{&&
&&&&&&&&return&&&
&&&&public&static&void&main(String[]&args)&{&&
&&&&&&&&Test&t1&=&new&Test();&&
&&&&&&&&Test&t2&=&new&Test();&&
&&&&&&&&t1.getList().add(t2);&&
&&&&&&&&t2.getList().add(t1);&&
&&&&&&&&t1&=&t2&=&null;&&
&&&&&&&&Test&t3&=&new&Test();&&
&&&&&&&&System.out.println(t3);&&
&我们用以下参数运行:-Xmx10M -Xms10M M 将jvm的大小设置为10M,不允许扩展,按引用计数法,t1和t2相互引用,他们的引用计数都不可能为0,那么他们将永远不会回收,在我们的环境中JVM共10M,t1 t2占用8m,那么剩下的2M,是不足以创建t3的,理论上应该抛出OOM。但是,程序正常运行了,这说明JVM应该是回收了t1和t2的我们加上-XX:+PrintGCDetails运行,将打印GC的回收日记:
[GC [DefNew: 252K-&64K(960K), 0.0030166 secs][Tenured: 8265K-&137K(9216K), 0.0109869 secs] 8444K-&137K(10176K), [Perm : 2051K-&K)], 0.0140892 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]&
[GC [DefNew: 252K-&64K(960K), 0.0030166 secs][Tenured: 8265K-&137K(9216K), 0.0109869 secs] 8444K-&137K(10176K), [Perm : 2051K-&K)], 0.0140892 secs] [Times: user=0.01 sys=0.00, real=0.02 secs]
com.mail.czp.Test@2ce908
def new generation
total 960K, used 27K [0x029eaeae0000)
eden space 896K,
3% used [0x029ee6c40, 0x02ac0000)
from space 64K,
0% used [0x02adadae0000)
space 64K,
0% used [0x02acacad0000)
tenured generation
total 9216K, used 4233K [0x02aeee0000)
the space 9216K,
45% used [0x02aef0f0e0000)
compacting perm gen
total 12288K, used 2077K [0x033efee0000)
the space 12288K,
16% used [0x033ee74d8, 0x035efe0000)
No shared spaces configured.
从打印的日志我们可以看出,GC照常回收了t1 t2,这就从侧面证明jvm不是采用这种策略判定对象是否可以回收的。
二,根搜索算法,这是当前的大部分虚拟机采用的判定策略,GC线程运行时,它会以一些特定的引用作为起点称为GCRoot,从这些起点开始搜索,把所用与这些起点相关联的对象标记,形成几条链路,扫描完时,那些没有与任何链路想连接的对象就会判定为可回收对象。具体那些引用作为起点呢,一种是类级别的引用:静态变量引用、常量引用,另一种是方法内的引用,如之前的test()方法中的Date d对new Date()的引用,在我们的测试代码中,在创建t3时,jvm发现当前的空间不足以创建对象,会出发一次GC,虽然t1和t2相互引用,但是执行t1=t2=null后,他们不和上面的3个根引用中的任何一个相连接,所以GC会判定他们是可回收对象,并在随后将其回收,从而为t3的创建创造空间,当进行回收后发现空间还是不够时,就会抛出OOM。
接下来我们就该讨论GC 是怎么回收的了,目前版本的Hotspot虚拟机采用分代回收算法,它把heap分为新生代和老年代两块区域,如下图:
&默认的配置中老年代占90% 新生代占10%,其中新生代又被分为一个eden区和两个survivor区,每次使用eden和其中的一个survivor区,一般对象都在eden和其中的一个survivor区分配,但是那些占用空间较大的对象,就会直接在老年代分配,比如我们在进行文件操作时设置的缓冲区,如byte[] buffer = new byte[],这样的对象如果在新生代分配将会导致新生代的内存不足而频繁的gc,GC运行时首先会进行会在新生代进行,会把那些标记还在引用的对象复制到另一块survivor空间中,然后把整个eden区和另一个survivor区里所有的对象进行清除,但也并不是立即清除,如果这些对象重写了finalize方法,那么GC会把这些对象先复制到一个队列里,以一个低级别的线程去触发finalize方法,然后回收该对象,而那些没有覆写finalize方法的对象,将会直接被回收。在复制存活对象到另一个survivor空间的过程中可能会出现空间不足的情况,在这种情况下GC回直接把这些存活对象复制到老年代中,如果老年代的空间也不够时,将会触发一次Full
GC,Full gc会回收老年代中那些没有和任何GC Root相连的对象,如果Full GC后发现内存还是不足,将会出现OutofMemoryError。
Hotspot虚拟机下java对象内存的分配和回收到此就算完结了
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:16728次
排名:千里之外
转载:216篇
评论:57条
(62)(40)(59)(59)扫描二维码,关注牛客网
下载牛客APP,随时随地刷题
浙ICP备号-2
扫一扫,把题目装进口袋1467人阅读
不常用的C#系列(7)
Thread与ThreadPool使用的时候在内存里对象是如何分布的呢?&今天我们就从内存堆的角度分析下两者。&先上小白鼠代码:
static void Main(string[] args)
for (int i = 0; i & 30; i++)
Thread t = new Thread(new ThreadStart(ThreadProc));
t.Name = "Overred_" +
t.Start();
Console.Read();
static void ThreadProc()
for (int i = 0; i & 10; i++)
Console.WriteLine("{0}
Value:{1}",Thread.CurrentThread.Name,i);
catch (Exception ex)
Console.WriteLine(ex.Message);
以上代码非常简单,就是循环启动30个线程去执行同一个方法ThreadProc(),然后打印出结果。
现在提出问题1:当Main里的30个线程都把ThreadProc()方法执行完毕后,这些Threads是自动消亡还是被GC回收,还是变成DeadThread?
好,拿出我们的看家工具windbg,来debug一把。
首先启动我们的程序,然后打开windbg,然后F6,Attach我们的exe
1,加载mscorwks(.net 2.0或者以上)
0:003&&.loadby&sos&mscorwks&
2,查看该程序的线程情况
0:003&&!Threads***&ERROR:&Symbol&file&could&not&be&found.&&Defaulted&to&export&symbols&for&C:/Windows/Microsoft.NET/Framework/v2.0.50727/mscorwks.dll&-&PDB&symbol&for&mscorwks.dll&not&loadedThreadCount:&32UnstartedThread:&0BackgroundThread:&1PendingThread:&0DeadThread:&30Hosted&Runtime:&no&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&&PreEmptive&&&GC&Alloc&&&&&&&&&&&Lock&&&&&&&ID&OSID&ThreadOBJ&&&&State&&&&&GC&&&&&&&Context&&&&&&&Domain&&&Count&APT&Exception&&&0&&&&1&25e4&&&&&&&a020&Enabled&&013f878c:013f9fe8&&&&&&1&MTA&&&2&&&&2&24b8&00526f20&&&&&&b220&Enabled&&18&&&&&0&MTA&(Finalizer)XXXX&&&&3&&&&0&&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&&4&&&&0&&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&&5&&&&0&&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&&6&&&&0&&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&&7&&&&0&00534fd8&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&&8&&&&0&&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&&9&&&&0&&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&&a&&&&0&0053bfc0&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&&b&&&&0&0053eba8&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&&c&&&&0&&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&&d&&&&0&00543b38&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&&e&&&&0&&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&&f&&&&0&00544ec8&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&10&&&&0&&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&11&&&&0&00545ee0&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&12&&&&0&&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&13&&&&0&00546a88&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&14&&&&0&00546e50&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&15&&&&0&&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&16&&&&0&&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&17&&&&0&&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&18&&&&0&00547d70&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&19&&&&0&&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&1a&&&&0&&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&1b&&&&0&&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&1c&&&&0&00548c90&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&1d&&&&0&&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&1e&&&&0&&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&1f&&&&0&&&&&&&9820&Enabled&&18&&&&&0&UknXXXX&&&20&&&&0&00549bb0&&&&&&9820&Enabled&&18&&&&&0&Ukn
看红色加粗部分,我们总共有32个线程,而DeadThread为30个(其他2个为程序自身所有,其中一个BackgroundThread),先告诉你这30个死线程正式我们循环创建的线程,可以回答我提的第一个问题拉,没错,他们统统死拉,而且不会醒来,还占地方(不是永远占地方,待会我们用GC手动让它们消亡)。
3,然后我们继续看看内存堆上它们这些坏家伙如何分布:
0:003&&!DumpHeap&-type&System.Threading&-stattotal&155&objectsStatistics:&&&&&&MT&&&&Count&&&&TotalSize&Class&Name&&&&&&&&1&&&&&&&&&&&32&System.Threading.ContextCallback790fe284&&&&&&&&2&&&&&&&&&&144&System.Threading.ThreadAbortException79124b74&&&&&&&30&&&&&&&&&&600&System.Threading.ThreadHelper79104de8&&&&&&&31&&&&&&&&&1116&System.Threading.ExecutionContext790fe704&&&&&&&31&&&&&&&&&1736&System.Threading.Thread&&&&&&&60&&&&&&&&&1920&System.Threading.ThreadStartTotal&155&objects
红色部分,31个Thread,对应着31个Context,每个线程在windows底层都是一个内核对象和一个栈空间,内核对象存放一些线程的统计信息,比如计数器以及一个上下文,就是我上次执行到那里等。而栈空间则是用来存放线程参数等。
4,我们来具体看下这些Thread们的MethodTable
0:003&&!DumpHeap&-MT&790fe704&&Address&&&&&&&MT&&&&&Size013c4&&&&&&&56&&&&&013c178c&790fe704&&&&&&&56&&&&&013c235c&790fe704&&&&&&&56&&&&&013c4&&&&&&&56&&&&&013c258c&790fe704&&&&&&&56&&&&&013c26a4&790fe704&&&&&&&56&&&&&013c27bc&790fe704&&&&&&&56&&&&&013c28d4&790fe704&&&&&&&56&&&&&013c29ec&790fe704&&&&&&&56&&&&&013c2b04&790fe704&&&&&&&56&&&&&013c2c1c&790fe704&&&&&&&56&&&&&013c2d34&790fe704&&&&&&&56&&&&&013c2e54&790fe704&&&&&&&56&&&&&013c2f74&790fe704&&&&&&&56&&&&&013c4&&&&&&&56&&&&&013c31b4&790fe704&&&&&&&56&&&&&013c32d4&790fe704&&&&&&&56&&&&&013c33f4&790fe704&&&&&&&56&&&&&013c4&&&&&&&56&&&&&013c4&&&&&&&56&&&&&013c4&&&&&&&56&&&&&013c4&&&&&&&56&&&&&013c4&&&&&&&56&&&&&013c3ab4&790fe704&&&&&&&56&&&&&013c3bd4&790fe704&&&&&&&56&&&&&013c3cf4&790fe704&&&&&&&56&&&&&013c3e14&790fe704&&&&&&&56&&&&&013c3f34&790fe704&&&&&&&56&&&&&013f4&&&&&&&56&&&&&013f81a4&790fe704&&&&&&&56&&&&&013f82c4&790fe704&&&&&&&56&&&&&total&31&objectsStatistics:&&&&&&MT&&&&Count&&&&TotalSize&Class&Name790fe704&&&&&&&31&&&&&&&&&1736&System.Threading.ThreadTotal&31&objects
5,随便拿一个线程的Address来看看到底是谁占着我们的Thread而不让我们的GC回收掉
0:003&&!GCRoot&013c3bd4
Note:&Roots&found&on&stacks&may&be&false&positives.&Run&"!help&gcroot"&for
more&info.
Scan&Thread&0&OSTHread&25e4
Scan&Thread&2&OSTHread&24b8
DOMAIN():HANDLE(WeakSh):241298:Root:013c3bd4(System.Threading.Thread)
结果另我们很失望,他自己就是根,并没被其他任何对象所引用,什么情况下会出现此情况呢?我们先来看看对象在内存中分布的几种方式,我们只需在windbg里执行如下命令则知:
0:003&&!Help&gcroot-------------------------------------------------------------------------------!GCRoot&[-nostacks]&&Object&address&!GCRoot&looks&for&references&(or&roots)&to&an&object.&These&can&exist&in&fourplaces:&&&1.&On&the&stack&&&2.&Within&a&GC&Handle&&&3.&In&an&object&ready&for&finalization&&&4.&As&a&member&of&an&object&found&in&1,&2&or&3&above.First,&all&stacks&will&be&searched&for&roots,&then&handle&tables,&and&finallythe&freachable&queue&of&the&finalizer.&Some&caution&about&the&stack&roots:&!GCRoot&doesn't&attempt&to&determine&if&a&stack&root&it&encountered&is&valid&or&is&old&(discarded)&data.&You&would&have&to&use&!CLRStack&and&!U&to&disassemble&the&frame&that&the&local&or&argument&value&belongs&to&in&order&to&determine&if&it&is&still&in&use.Because&people&often&want&to&restrict&the&search&to&gc&handles&and&freachableobjects,&there&is&a&-nostacks&option.
windbg已经很清楚的告诉我们,&一个对象可以&1,在栈上&2,在一个GCHandle里(可以执行!GCHandles命令查看)&3,在FinalizeQueue里&4,是一个对象的成员&难道对象就必定在以上的&四行&之中吗?答案是不一定,还有个Gchandleleaks,就是你在内存里看不到这个Handle,它已经leak。(这种也算在GCHandle里吧)。&回头我们接着说他自己没被其他任何对象所引用,自己就是个根,但是GC却不搭理它,为何?那就是他在GCHandle里,
0:003&&!GCHandlesGC&Handle&Statistics:Strong&Handles:&14Pinned&Handles:&4Async&Pinned&Handles:&0Ref&Count&Handles:&0Weak&Long&Handles:&0Weak&Short&Handles:&31Other&Handles:&0Statistics:&&&&&&MT&&&&Count&&&&TotalSize&Class&Name790fd0f0&&&&&&&&1&&&&&&&&&&&12&System.Object790fcc48&&&&&&&&1&&&&&&&&&&&24&System.Reflection.Assembly790feba4&&&&&&&&1&&&&&&&&&&&28&System.SharedStatics790fe17c&&&&&&&&1&&&&&&&&&&&72&System.ExecutionEngineException790fe0e0&&&&&&&&1&&&&&&&&&&&72&System.StackOverflowException790fe044&&&&&&&&1&&&&&&&&&&&72&System.OutOfMemoryException790fed00&&&&&&&&1&&&&&&&&&&100&System.AppDomain79100a18&&&&&&&&4&&&&&&&&&&144&System.Security.PermissionSet790fe284&&&&&&&&2&&&&&&&&&&144&System.Threading.ThreadAbortException790fe704&&&&&&&32&&&&&&&&&1792&System.Threading.Thread&&&&&&&&4&&&&&&&&&8736&System.Object[]Total&49&objects
而且在FinalizeQueue里也有它的踪影:
0:003&&!FinalizeQueueSyncBlocks&to&be&cleaned&up:&0MTA&Interfaces&to&be&released:&0STA&Interfaces&to&be&released:&0----------------------------------generation&0&has&35&finalizable&objects&(5266e4)generation&1&has&0&finalizable&objects&(526658)generation&2&has&0&finalizable&objects&(526658)Ready&for&finalization&0&objects&(-&)Statistics:&&&&&&MT&&&&Count&&&&TotalSize&Class&Name&&&&&&&&1&&&&&&&&&&&20&Microsoft.Win32.SafeHandles.SafeFileMappingHandle&&&&&&&&1&&&&&&&&&&&20&Microsoft.Win32.SafeHandles.SafeViewOfFileHandle&&&&&&&&2&&&&&&&&&&&40&Microsoft.Win32.SafeHandles.SafeFileHandle790fe704&&&&&&&31&&&&&&&&&1736&System.Threading.ThreadTotal&35&objects
下面就来解释下什么才可以在FinalizeQueue里出现呢?答案就是有身份的人,很有身份的人,享受特殊待遇的哦!&啥身份,就是自身实现拉析构函数。
啥待遇,就是GC两次才有可能把他们部分清理掉!为啥部分,是我们不知道windows到底何时去把所有的清理掉(赖皮阿)&具体原理大家可以看.net框架去,我这里不多说。
说到此,也就找到我们当初30个彪形大汉为啥赖着不走的原因拉,是在0代的第一次GC时候,他们被放进FinalizeQueue,等着第二次GC他们部分才会从内存堆上消亡。&为证明我们的观点,我们可以修改程序为 :
static void Main(string[] args)
for (int i = 0; i & 30; i++)
Thread t = new Thread(new ThreadStart(ThreadProc));
t.Name = "Overred_" +
t.Start();
GC.Collect();
GC.Collect();
Console.Read();
首先声明一点就是当我们调用一次GC.Collect();时,并不是执行一次垃圾收集,只是告诉系统我要强制进行垃圾收集,系统听到这个命令后乖不乖那就不一定拉。&当我们用Reflector查看mscorlib对Thread实现的使用也会发现他实现拉析构:
this.InternalFinalize();
来个虎头蛇尾吧,当我们把小白鼠程序使用ThreadPool修改为:
static void Main(string[] args)
for (int i = 0; i & 30; i++)
ThreadPool.QueueUserWorkItem(new WaitCallback(ThreadProc));
Console.Read();
void ThreadProc(object o)
for (int i = 0; i & 10; i++)
Console.WriteLine(" Value:{0}",i);
catch (Exception ex)
Console.WriteLine(ex.Message);
再用windbg查看线程时则为:
0:006&&!Threads***&ERROR:&Symbol&file&could&not&be&found.&&Defaulted&to&export&symbols&for&C:/Windows/Microsoft.NET/Framework/v2.0.50727/mscorwks.dll&-&PDB&symbol&for&mscorwks.dll&not&loadedThreadCount:&4UnstartedThread:&0BackgroundThread:&3PendingThread:&0DeadThread:&0
而FinalizeQueue则为:
0:006&&!FinalizeQueueSyncBlocks&to&be&cleaned&up:&0MTA&Interfaces&to&be&released:&0STA&Interfaces&to&be&released:&0----------------------------------generation&0&has&7&finalizable&objects&(266674)generation&1&has&0&finalizable&objects&(266658)generation&2&has&0&finalizable&objects&(266658)Ready&for&finalization&0&objects&(266674)Statistics:&&&&&&MT&&&&Count&&&&TotalSize&Class&Name&&&&&&&&1&&&&&&&&&&&20&Microsoft.Win32.SafeHandles.SafeFileMappingHandle&&&&&&&&1&&&&&&&&&&&20&Microsoft.Win32.SafeHandles.SafeViewOfFileHandle&&&&&&&&2&&&&&&&&&&&40&Microsoft.Win32.SafeHandles.SafeFileHandle790fe704&&&&&&&&3&&&&&&&&&&168&System.Threading.ThreadTotal&7&objects
那现在又出现问题拉,既然ThreadPool这么好,那我们为啥还使用Thread呢?这个问题就是ThreadPool有个GetMaxThreads,可以通过GetMaxThreads(out int workerThreads, out int completionPortThreads);方法获取到,如果线程池满拉,则会死锁更严重!另:ThreadPool都为后台线程。究竟使用那个,根据情况而定,理解拉内在的东西,一切表象就简单拉。OK,到此吧。。。
希望本文能对你有所帮助,谢谢!&
参考知识库
* 以上用户言论只代表其个人观点,不代表CSDN网站的观点或立场
访问:41011次
排名:千里之外
转载:14篇
(1)(1)(1)(5)(1)(1)(9)(2)

我要回帖

更多关于 gcroot是什么 的文章

 

随机推荐