对象的生命周期、类的生命周期

对象的生命周期
参考: 《理解C#对象的生命周期》https://www.cnblogs.com/Jack47/archive/2012/11/14/2770748.html
 
在C#中,程序员无法直接在C#中删除一个托管对象,因为C#不提供这个功能,那么类的实例就需要通过CLR调用垃圾回收机制进行清除,回收内存。.NET垃圾回收器会压缩空的内存块来实现优化,为了辅助这一功能,托管堆会保存一个指针,它指向下一个对象将被分配的位置。
那么CLR是如何使用垃圾回收机制呢? 
首先,类实例化之后具体的对象会被分配到一块叫托管堆的内存区域上,将托管堆中的对象的引用地址返回给函数中的引用变量,引用变量保存在栈内,要使用对象中的方法只需要使用点符号操作就可以。 特别需要说一下的是,结构是数值类型,它直接分配在栈上(所以的数值类都是这样的,只有引用类型才会保存在托管堆上)。
实例化结束之后,垃圾回收器会在一个对象从代码库的任何部分都不可访问的时候,将它从堆中删除,例:
static void MakeCar()
{
Car mycar=new Car();
}
在例子中,Car的引用mycar直接在MakeCar中创建,并没有被传到该方法以外的作用域的,因此,在这个方法调用结束之后,这个对象就不会再被访问, 此时它就是垃圾回收器的回收目标,但是,要知道,一个对象在失去意义之后并不会立即被删除。
CLR调用垃圾回收器的标准是:在创建对象时,先判断对象所需要的内存的大小,在判断目前的托管堆是否有足够的内存保存它,当托管堆没有足够的内存时,CLR就会调用垃圾回收器进行垃圾回收,因此,在对象失去意义之后还需要等待CLR调用垃圾回收器时才能被删除。
那么CLR是如何判断对象所需的内存,以及堆的内存是否够用?
当C#编译器在遇到new关键字时,它会自动在方法的实现中加上一条CIL newobj 的指令,它会计算分配对象所需的总内存,检查堆的内存空间,如果内存足够,则调用类型的构造函数,最终将内存中的新变量的引用返回给调用者,它的地址是下一个对象指针的最后位置,若是内存不足,则CLR调用垃圾回收器释放内存。在回调地址之后,就将对象的指针指向下一个可用的地址。
那么垃圾回收器如何判断一个对象是否还在使用?
这就要介绍一个应用程序根,所谓的根,说白了就是存储堆上的对象的引用地址的存储位置,在回收过程中运行库会对对象进行判断,判断程序是否还可以访问它们,也就是说它们的根是否还存在,若是不存在,则标记为垃圾,进而被清除对象,CLR压缩内存,指针指向正确的位置。
但是若是每一次进行垃圾回收的时候都要对托管堆上的所以数据进行一次判定,这种方法就太过于耗时耗力了,于是就有了代,代的设计思路是:对象在堆上存活的时间越长,接下来它继续存在的可能性也就越大,即较旧的对象生存期长,较新的对象生存期短。例如,实现Main()的对象一直在内存中,直到程序结束。相反,最近才被放到堆中的对象(例如在一个函数范围里分配的对象)很可能很快就不可达。在堆上的每个对象属于以下的某一个代:
  • Generation 0: 标识一个最近分配的还没有被标记为回收的对象
  • Generation 1: 标识一个经历了一次垃圾回收而存活下来的对象(例如,他被标记为回收,但由于堆空间够用而没有被清除掉)
  • Generation 2:标识一个经历了不止一轮垃圾回收而存活下来的对象。
对象在堆上存在的时间越长就越可能被保留。所以就将堆上的对象共分为0-2的3代,垃圾回收器在运行的时候,首先会检测0代的对象,并且将那些不需要的对象释放,空出内存,若是空出的内存不够,则会往上面一级的1级代进行检测,以此类推,直到获取所需要的内存大小为止,而在这之后,0代上没被删除的对象就会被标记为1代,一代中未被删除的对象就标记为2代,但是2代还是二代,因为这是上限。在这里还得说一下,其实垃圾回收器使用的两个堆,我们所说的是小对象堆,还有一个大对象堆,他存储的是大于85k的对象,因为它的内容太大,对它进行修改的话,花费代价太大,所以在垃圾回收器极少会对它进行修改。
通过给堆中的对象赋予一个generation的值,新对象(比如局部变量)会被很快回收,而老一些的对象(如一个应用程序对象)不会被经常骚扰。
 
----------------------以下为扩展内容----------------------
 
以上是垃圾回收器自动对对上面的数据进行回收,这并不需要人为的进行操控,但是这是对于托管在托管堆上面的对象,若是有些数据不是托管的资源呢?.NET提供了一个System.GC的类类型,它可以通过编程使用一些静态成员与垃圾回收器进行交互,这种行为也叫作强制垃圾回收,它可以由我们自己决定什么时候释放某个对象的资源,而不用被动等待垃圾回收器运行,一般来说在不希望接下来的代码被垃圾回收器打断的运行时候(垃圾回收器的运行时间是不确定的),或者我需要一次性分配很多的对象的时候都会用到强制回收,强制回收使用GC.Collect()方法,在它的后面必须要调用GC.WaitForPendingFinalize(), 另外,使用Finalize()构建可终结对象,当应用程序的应用程序域从内存中卸载的话,CLR就会自动调用它的生命周期中所创建的每一个可终结对象的终结器进行强制回收,重写Finalize()无法像普通的类一样,它需要类似C++的析构语法,此外终结器还需在名称之前加~,他不接受访问修饰符,不接受参数,不支持重载,但是使用这种方式的话,需要两次的来及回收才能真正的释放该资源,并且由于是额外的处理,所以速度回变得非常慢。所以就可以考虑构建一个可处置对象,构建可处置对象需要实现IDisposable,这个方法不止可以释放一个对象的非托管资源,而且还可以对任何它包含的可处置对象调用Dipose(),使用这种方法可以有自己调用释放内存,若是忘记调用,也会有垃圾回收器进行释放,所以这种方式在我看来会更安全好用一些。在C#类中还为实现了IDisposable的接口提供了一类语法:using,使用这种语法的好处在于,可以由Dispose()调用扩展try/catch结构,例如:using(class c=new class()){}在编译之后,与class c=new class();try{}catch(){};是等同的。
在这一个章节内,我觉得还有一个非常使用的泛型类Lazy<>,但凡被这个类所定义的数据在代码库实际使用它之前是不会被创建的,这样就可以使一些不常使用的大数据在实例化对象的时候不同时被创建,进而占用内存空间。
-------------------------------------文章二---------------------------------------
类的生命周期
参考:《详解java类的生命周期》: https://blog.csdn.net/zhengzhb/article/details/7517213
首先来了解一下jvm(java虚拟机)中的几个比较重要的内存区域,这几个区域在java类的生命周期中扮演着比较重要的角色:
 
方法区:在java的虚拟机中有一块专门用来存放已经加载的类信息、常量、静态变量以及方法代码的内存区域,叫做方法区。
常量池:常量池是方法区的一部分,主要用来存放常量和类中的符号引用等信息。
堆区:用于存放类的对象实例。
栈区:也叫java虚拟机栈,是由一个一个的栈帧组成的后进先出的栈式结构,栈桢中存放方法运行时产生的局部变量、方法出口等信息。当调用一个方法时,虚拟机栈中就会创建一个栈帧存放这些数据,当方法调用完成时,栈帧消失,如果方法中调用了其他方法,则继续在栈顶创建新的栈桢。
  当我们编写一个java的源文件后,经过编译会生成一个后缀名为class的文件,这种文件叫做字节码文件,只有这种字节码文件才能够在java虚拟机中运行,java类的生命周期就是指一个class文件从加载到卸载的全过程。
 
        一个java类的完整的生命周期会经历加载、连接、初始化、使用、和卸载五个阶段,当然也有在加载或者连接之后没有被初始化就直接被使用的情况,如图所示:
 
下面我们就依次来说一说这五个阶段。
 
加载
在java中,我们经常会接触到一个词——类加载,它和这里的加载并不是一回事,通常我们说类加载包含了类的生命周期中加载、连接、初始化三个阶段。在加载阶段,java虚拟机会做什么工作呢?其实很简单,就是找到需要加载的类并把类的信息加载到jvm的方法区中,然后在堆区中实例化一个java.lang.Class对象,作为方法区中这个类的信息的入口。
类的加载方式比较灵活,我们最常用的加载方式有两种,一种是根据类的全路径名找到相应的class文件,然后从class文件中读取文件内容;另一种是从jar文件中读取。
 
       对于加载的时机,各个虚拟机的做法并不一样,但是有一个原则,就是当jvm“预期”到一个类将要被使用时,就会在使用它之前对这个类进行加载。比如说,在一段代码中出现了一个类的名字,jvm在执行这段代码之前并不能确定这个类是否会被使用到,于是,有些jvm会在执行前就加载这个类,而有些则在真正需要用的时候才会去加载它,这取决于具体的jvm实现。我们常用的hotspot虚拟机是采用的后者,就是说当真正用到一个类的时候才对它进行加载。
 
       加载阶段是类的生命周期中的第一个阶段,加载阶段之后,是连接阶段。有一点需要注意,就是有时连接阶段并不会等加载阶段完全完成之后才开始,而是交叉进行,可能一个类只加载了一部分之后,连接阶段就已经开始了。但是这两个阶段总的开始时间和完成时间总是固定的:加载阶段总是在连接阶段之前开始,连接阶段总是在加载阶段完成之后完成。
 
连接
       连接阶段比较复杂,一般会跟加载阶段和初始化阶段交叉进行,这个阶段的主要任务就是做一些加载后的验证工作以及一些初始化前的准备工作,可以细分为三个步骤:验证、准备和解析。
 
验证:当一个类被加载之后,必须要验证一下这个类是否合法,比如这个类是不是符合字节码的格式、变量与方法是不是有重复、数据类型是不是有效、继承与实现是否合乎标准等等。总之,这个阶段的目的就是保证加载的类是能够被jvm所运行。
准备:准备阶段的工作就是为类的静态变量分配内存并设为jvm默认的初值,对于非静态的变量,则不会为它们分配内存。有一点需要注意,这时候,静态变量的初值为jvm默认的初值,而不是我们在程序中设定的初值。jvm默认的初值是这样的:
基本类型(int、long、short、char、byte、boolean、float、double)的默认值为0。
引用类型的默认值为null。
常量的默认值为我们程序中设定的值,比如我们在程序中定义final static int a = 100,则准备阶段中a的初值就是100。
解析:这一阶段的任务就是把常量池中的符号引用转换为直接引用。那么什么是符号引用,什么又是直接引用呢?我们来举个例子:我们要找一个人,我们现有的信息是这个人的身份证号是1234567890。只有这个信息我们显然找不到这个人,但是通过公安局的身份系统,我们输入1234567890这个号之后,就会得到它的全部信息:比如安徽省黄山市余暇村18号张三,通过这个信息我们就能找到这个人了。这里,123456790就好比是一个符号引用,而安徽省黄山市余暇村18号张三就是直接引用。在内存中也是一样,比如我们要在内存中找一个类里面的一个叫做show的方法,显然是找不到。但是在解析阶段,jvm就会把show这个名字转换为指向方法区的的一块内存地址,比如c17164,通过c17164就可以找到show这个方法具体分配在内存的哪一个区域了。这里show就是符号引用,而c17164就是直接引用。在解析阶段,jvm会将所有的类或接口名、字段名、方法名转换为具体的内存地址。
        连接阶段完成之后会根据使用的情况(直接引用还是被动引用)来选择是否对类进行初始化。
 
初始化
       如果一个类被直接引用,就会触发类的初始化。在java中,直接引用的情况有:
 
  • 通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法。
  • 通过反射方式执行以上三种行为。
  • 初始化子类的时候,会触发父类的初始化。
  • 作为程序入口直接运行时(也就是直接调用main方法)。
除了以上四种情况,其他使用类的方式叫做被动引用,而被动引用不会触发类的初始化。
 
类的初始化过程是这样的:按照顺序自上而下运行类中的变量赋值语句和静态语句,如果有父类,则首先按照顺序运行父类中的变量赋值语句和静态语句。
  在类的初始化阶段,只会初始化与类相关的静态赋值语句和静态语句,也就是有static关键字修饰的信息,而没有static修饰的赋值语句和执行语句在实例化对象的时候才会运行。
 

使用

类的使用包括主动引用和被动引用,主动引用在初始化的章节中已经说过了,下面我们主要来说一下被动引用:

  • 引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。

  • 定义类数组,不会引起类的初始化。

  • 引用类的常量,不会引起类的初始化。

被动引用的示例代码:
class InitClass{
    static {
        System.out.println("初始化InitClass");
    }
    public static String a = null;
    public final static String b = "b";
    public static void method(){}
}
 
class SubInitClass extends InitClass{
    static {
        System.out.println("初始化SubInitClass");
    }
}
 
public class Test4 {
    public static void main(String[] args) throws Exception{
    //    String a = SubInitClass.a;// 引用父类的静态字段,只会引起父类初始化,而不会引起子类的初始化
    //    String b = InitClass.b;// 使用类的常量不会引起类的初始化
        SubInitClass[] sc = new SubInitClass[10];// 定义类数组不会引起类的初始化
    }
}
 最后总结一下使用阶段:使用阶段包括主动引用和被动引用,主动饮用会引起类的初始化,而被动引用不会引起类的初始化。
 
卸载
       关于类的卸载,笔者在单例模式讨论篇:单例模式与垃圾回收一文中有过描述,在类使用完之后,如果满足下面的情况,类就会被卸载:
 
  • 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
  • 加载该类的ClassLoader已经被回收。
  • 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。
如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。
 
总结
做java的朋友对于对象的生命周期可能都比较熟悉,对象基本上都是在jvm的堆区中创建,在创建对象之前,会触发类加载(加载、连接、初始化),当类初始化完成后,根据类信息在堆区中实例化类对象,初始化非静态变量、非静态代码以及默认构造方法,当对象使用完之后会在合适的时候被jvm垃圾收
集器回收。读完本文后我们知道,对象的生命周期只是类的生命周期中使用阶段的主动引用的一种情况(即实例化类对象)。而类的整个生命周期则要比对象的生命周期长的多。
 
 
posted @ 2020-05-27 23:11  gaoyang'Blog  阅读(781)  评论(0)    收藏  举报