精华内容
下载资源
问答
  • Java 对象生命周期和类生命周期

    万次阅读 2015-04-21 16:57:30
    Java 对象生命周期 在JVM运行空间中,对象的整个生命周期大致可以分为7个阶段:创建阶段(Creation)、应用阶段(Using)、不可视阶段(Invisible)、不可到达阶段(Unreachable)、可收集阶段(Collected...

    Java 对象生命周期


    在JVM运行空间中,对象的整个生命周期大致可以分为7个阶段:创建阶段(Creation)、应用阶段(Using)、不可视阶段(Invisible)、不可到达阶段(Unreachable)、可收集阶段(Collected)、终结阶段(Finalized)与释放阶段(Free)。上面的这7个阶段,构成了 JVM中对象的完整的生命周期。下面分别介绍对象在处于这7个阶段时的不同情形。 

    创建阶段

    在对象创建阶段,系统要通过下面的步骤,完成对象的创建过程:

    (1)为对象分配存储空间。

    (2)开始构造对象。

    (3)递归调用其超类的构造方法。

    (4)进行对象实例初始化与变量初始化。

    (5)执行构造方法体。

    上面的5个步骤中的第3步就是指递归地调用该类所扩展的所有父类的构造方法,一个Java类(除Object类外)至少有一个父类(Object),这个规则既是强制的,也是隐式的。你可能已经注意到在创建一个Java类的时候,并没有显式地声明扩展(extends)一个Object父类。实际上,在 Java程序设计中,任何一个Java类都直接或间接的是Object类的子类。例如下面的代码:

    public class A { 
         … 
    } 
    这个声明等同于下面的声明: 
    public class A extends java.lang.Object { 
         … 
    } 

    上面讲解了对象处于创建阶段时,系统所做的一些处理工作,其中有些过程与应用的性能密切相关,因此在创建对象时,我们应该遵循一些基本的规则,以提高应用的性能。

    下面是在创建对象时的几个关键应用规则:

    (1)避免在循环体中创建对象,即使该对象占用内存空间不大。

    (2)尽量及时使对象符合垃圾回收标准。

    (3)不要采用过深的继承层次。

    (4)访问本地变量优于访问类中的变量。

    关于规则(1)避免在循环体中创建对象,即使该对象占用内存空间不大,需要提示一下,这种情况在我们的实际应用中经常遇到,而且我们很容易犯类似的错误,例如下面的代码:

    … … 
    for (int i = 0; i < 10000; ++i) { 
        Object obj = new Object(); 
        System.out.println("obj= "+ obj); 
    } 
    … … 

    上面代码的书写方式相信对你来说不会陌生,也许在以前的应用开发中你也这样做过,尤其是在枚举一个Vector对象中的对象元素的操作中经常会这样书写,但这却违反了上述规则(1),因为这样会浪费较大的内存空间,正确的方法如下所示:

    … … 
    Object obj = null; 
    for (int i = 0; i < 10000; ++i) { 
        obj = new Object(); 
        System.out.println("obj= "+ obj); 
    } 
    … … 

    采用上面的第二种编写方式,仅在内存中保存一份对该对象的引用,而不像上面的第一种编写方式中代码会在内存中产生大量的对象应用,浪费大量的内存空间,而且增大了系统做垃圾回收的负荷。因此在循环体中声明创建对象的编写方式应该尽量避免。

    另外,不要对一个对象进行多次初始化,这同样会带来较大的内存开销,降低系统性能,如:

    public class A { 
        private Hashtable table = new Hashtable (); 
        public A() { 
            // 将Hashtable对象table初始化了两次 
            table = new Hashtable(); 
        } 
    } 

    正确的方式为:

    public class B { 
         private Hashtable table = new Hashtable (); 
         public B() { 
         } 
    } 

    不要小看这个差别,它却使应用软件的性能相差甚远,如图2-5所示。

     

     

    图2-5  初始化对象多次所带来的性能差别

    看来在程序设计中也应该遵从“勿以恶小而为之”的古训,否则我们开发出来的应用也是低效的应用,有时应用软件中的一个极小的失误,就会大幅度地降低整个系统的性能。因此,我们在日常的应用开发中,应该认真对待每一行代码,采用最优化的编写方式,不要忽视细节,不要忽视潜在的问题。

    应用阶段

    当对象的创建阶段结束之后,该对象通常就会进入对象的应用阶段。这个阶段是对象得以表现自身能力的阶段。也就是说对象的应用阶段是对象整个生命周期中证明自身“存在价值”的时期。在对象的应用阶段,对象具备下列特征:

    ◆系统至少维护着对象的一个强引用(Strong Reference);

    ◆所有对该对象的引用全部是强引用(除非我们显式地使用了:软引用(Soft Reference)、弱引用(Weak Reference)或虚引用(Phantom Reference))。

    上面提到了几种不同的引用类型。可能一些读者对这几种引用的概念还不是很清楚,下面分别对之加以介绍。在讲解这几种不同类型的引用之前,我们必须先了解一下Java中对象引用的结构层次。

    Java对象引用的结构层次示意如图2-6所示。

     

    图2-6  对象引用的结构层次示意

    由图2-6我们不难看出,上面所提到的几种引用的层次关系,其中强引用处于顶端,而虚引用则处于底端。下面分别予以介绍。

    1.强引用

    强引用(Strong Reference)是指JVM内存管理器从根引用集合(Root Set)出发遍寻堆中所有到达对象的路径。当到达某对象的任意路径都不含有引用对象时,对这个对象的引用就被称为强引用。

    2.软引用

    软引用(Soft Reference)的主要特点是具有较强的引用功能。只有当内存不够的时候,才回收这类内存,因此在内存足够的时候,它们通常不被回收。另外,这些引用对象还能保证在Java抛出OutOfMemory 异常之前,被设置为null。它可以用于实现一些常用资源的缓存,实现Cache的功能,保证最大限度的使用内存而不引起OutOfMemory。再者,软可到达对象的所有软引用都要保证在虚拟机抛出OutOfMemoryError之前已经被清除。否则,清除软引用的时间或者清除不同对象的一组此类引用的顺序将不受任何约束。然而,虚拟机实现不鼓励清除最近访问或使用过的软引用。下面是软引用的实现代码:

    … … 
    import java.lang.ref.SoftReference; 
    … 
    A a = new A(); 
    … 
    // 使用 a 
    … 
    // 使用完了a,将它设置为soft 引用类型,并且释放强引用; 
    SoftReference sr = new SoftReference(a); 
    a = null; 
    … 
    // 下次使用时 
    if (sr!=null) { 
         a = sr.get(); 
    } 
    else{ 
         // GC由于内存资源不足,可能系统已回收了a的软引用, 
         // 因此需要重新装载。 
         a = new A(); 
         sr=new SoftReference(a); 
    } 
    … … 

    软引用技术的引进,使Java应用可以更好地管理内存,稳定系统,防止系统内存溢出,避免系统崩溃(crash)。因此在处理一些占用内存较大而且声明周期较长,但使用并不频繁的对象时应尽量应用该技术。正像上面的代码一样,我们可以在对象被回收之后重新创建(这里是指那些没有保留运行过程中状态的对象),提高应用对内存的使用效率,提高系统稳定性。但事物总是带有两面性的,有利亦有弊。在某些时候对软引用的使用会降低应用的运行效率与性能,例如:应用软引用的对象的初始化过程较为耗时,或者对象的状态在程序的运行过程中发生了变化,都会给重新创建对象与初始化对象带来不同程度的麻烦,有些时候我们要权衡利弊择时应用。

    3.弱引用

    弱引用(Weak Reference)对象与Soft引用对象的最大不同就在于:GC在进行回收时,需要通过算法检查是否回收Soft引用对象,而对于Weak引用对象, GC总是进行回收。因此Weak引用对象会更容易、更快被GC回收。虽然,GC在运行时一定回收Weak引用对象,但是复杂关系的Weak对象群常常需要好几次GC的运行才能完成。Weak引用对象常常用于Map数据结构中,引用占用内存空间较大的对象,一旦该对象的强引用为null时,对这个对象引用就不存在了,GC能够快速地回收该对象空间。与软引用类似我们也可以给出相应的应用代码:

    … … 
    import java.lang.ref.WeakReference; 
    … 
    A a = new A(); 
    … 
    // 使用 a 
    … 
    // 使用完了a,将它设置为weak 引用类型,并且释放强引用; 
    WeakReference wr = new WeakReference (a); 
    a = null; 
    … 
    // 下次使用时 
    if (wr!=null) { 
        a = wr.get(); 
    } 
    else{ 
        a = new A(); 
        wr = new WeakReference (a); 
    } 
    … … 

    弱引用技术主要适用于实现无法防止其键(或值)被回收的规范化映射。另外,弱引用分为“短弱引用(Short Week Reference)”和“长弱引用(Long Week Reference)”,其区别是长弱引用在对象的Finalize方法被GC调用后依然追踪对象。基于安全考虑,不推荐使用长弱引用。因此建议使用下面的方式创建对象的弱引用。

    … … 
    WeakReference wr = new WeakReference(obj); 
    或 
    WeakReference wr = new WeakReference(obj, false); 
    … … 

    4.虚引用

    虚引用(Phantom Reference)的用途较少,主要用于辅助finalize函数的使用。Phantom对象指一些执行完了finalize函数,并且为不可达对象,但是还没有被GC回收的对象。这种对象可以辅助finalize进行一些后期的回收工作,我们通过覆盖Reference的clear()方法,增强资源回收机制的灵活性。虚引用主要适用于以某种比 java 终结机制更灵活的方式调度 pre-mortem 清除操作。

    &注意  在实际程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。

    不可视阶段

    在一个对象经历了应用阶段之后,那么该对象便处于不可视阶段,说明我们在其他区域的代码中已经不可以再引用它,其强引用已经消失,例如,本地变量超出了其可视范围,如下所示。

    … … 
    public void process () { 
        try { 
             Object obj = new Object(); 
             obj.doSomething(); 
         } catch (Exception e) { 
         e.printStackTrace(); 
         } 
         while (isLoop) { // ... loops forever 
          // 这个区域对于obj对象来说已经是不可视的了 
             // 因此下面的代码在编译时会引发错误 
             obj.doSomething();  
         } 
    } 
    … … 

    如果一个对象已使用完,而且在其可视区域不再使用,此时应该主动将其设置为空(null)。可以在上面的代码行obj.doSomething();下添加代码行obj = null;,这样一行代码强制将obj对象置为空值。这样做的意义是,可以帮助JVM及时地发现这个垃圾对象,并且可以及时地回收该对象所占用的系统资源。

    不可到达阶段

    处于不可到达阶段的对象,在虚拟机所管理的对象引用根集合中再也找不到直接或间接的强引用,这些对象通常是指所有线程栈中的临时变量,所有已装载的类的静态变量或者对本地代码接口(JNI)的引用。这些对象都是要被垃圾回收器回收的预备对象,但此时该对象并不能被垃圾回收器直接回收。其实所有垃圾回收算法所面临的问题是相同的——找出由分配器分配的,但是用户程序不可到达的内存块。

    可收集阶段、终结阶段与释放阶段

    对象生命周期的最后一个阶段是可收集阶段、终结阶段与释放阶段。当对象处于这个阶段的时候,可能处于下面三种情况:

    (1)垃圾回收器发现该对象已经不可到达。

    (2)finalize方法已经被执行。

    (3)对象空间已被重用。

    当对象处于上面的三种情况时,该对象就处于可收集阶段、终结阶段与释放阶段了。虚拟机就可以直接将该对象回收了。





    Java 类生命周期


            最近有位细心的朋友在阅读笔者的文章时,对java类的生命周期问题有一些疑惑,笔者打开百度搜了一下相关的问题,看到网上的资料很少有把这个问题讲明白的,主要是因为目前国内java方面的教材大多只是告诉你“怎样做”,但至于“为什么这样做”却不多说,所以造成大家在基础和原理方面的知识比较匮乏,所以笔者今天就斗胆来讲一下这个问题,权当抛砖引玉,希望对在这个问题上有疑惑的朋友有所帮助,文中有说的不对的地方,也希望各路高手前来指正。

            首先来了解一下jvm(java虚拟机)中的几个比较重要的内存区域,这几个区域在java类的生命周期中扮演着比较重要的角色:

    • 方法区:在java的虚拟机中有一块专门用来存放已经加载的类信息、常量、静态变量以及方法代码的内存区域,叫做方法区。
    • 常量池:常量池是方法区的一部分,主要用来存放常量和类中的符号引用等信息。
    • 堆区:用于存放类的对象实例。
    • 栈区:也叫java虚拟机栈,是由一个一个的栈帧组成的后进先出的栈式结构,栈桢中存放方法运行时产生的局部变量、方法出口等信息。当调用一个方法时,虚拟机栈中就会创建一个栈帧存放这些数据,当方法调用完成时,栈帧消失,如果方法中调用了其他方法,则继续在栈顶创建新的栈桢。

            除了以上四个内存区域之外,jvm中的运行时内存区域还包括本地方法栈程序计数器,这两个区域与java类的生命周期关系不是很大,在这里就不说了,感兴趣的朋友可以自己百度一下。



    类的生命周期

            当我们编写一个java的源文件后,经过编译会生成一个后缀名为class的文件,这种文件叫做字节码文件,只有这种字节码文件才能够在java虚拟机中运行,java类的生命周期就是指一个class文件从加载到卸载的全过程。

            一个java类的完整的生命周期会经历加载、连接、初始化、使用、和卸载五个阶段,当然也有在加载或者连接之后没有被初始化就直接被使用的情况,如图所示:

     

     

    下面我们就依次来说一说这五个阶段。

     

    加载

           在java中,我们经常会接触到一个词——类加载,它和这里的加载并不是一回事,通常我们说类加载指的是类的生命周期中加载、连接、初始化三个阶段。在加载阶段,java虚拟机会做什么工作呢?其实很简单,就是找到需要加载的类并把类的信息加载到jvm的方法区中,然后在堆区中实例化一个java.lang.Class对象,作为方法区中这个类的信息的入口。

           类的加载方式比较灵活,我们最常用的加载方式有两种,一种是根据类的全路径名找到相应的class文件,然后从class文件中读取文件内容;另一种是从jar文件中读取。另外,还有下面几种方式也比较常用:

    • 从网络中获取:比如10年前十分流行的Applet。
    • 根据一定的规则实时生成,比如设计模式中的动态代理模式,就是根据相应的类自动生成它的代理类。
    • 从非class文件中获取,其实这与直接从class文件中获取的方式本质上是一样的,这些非class文件在jvm中运行之前会被转换为可被jvm所识别的字节码文件。

           对于加载的时机,各个虚拟机的做法并不一样,但是有一个原则,就是当jvm“预期”到一个类将要被使用时,就会在使用它之前对这个类进行加载。比如说,在一段代码中出现了一个类的名字,jvm在执行这段代码之前并不能确定这个类是否会被使用到,于是,有些jvm会在执行前就加载这个类,而有些则在真正需要用的时候才会去加载它,这取决于具体的jvm实现。我们常用的hotspot虚拟机是采用的后者,就是说当真正用到一个类的时候才对它进行加载。

           加载阶段是类的生命周期中的第一个阶段,加载阶段之后,是连接阶段。有一点需要注意,就是有时连接阶段并不会等加载阶段完全完成之后才开始,而是交叉进行,可能一个类只加载了一部分之后,连接阶段就已经开始了。但是这两个阶段总的开始时间和完成时间总是固定的:加载阶段总是在连接阶段之前开始,连接阶段总是在加载阶段完成之后完成。

     

    连接

           连接阶段比较复杂,一般会跟加载阶段和初始化阶段交叉进行,这个阶段的主要任务就是做一些加载后的验证工作以及一些初始化前的准备工作,可以细分为三个步骤:验证、准备和解析。

    1. 验证:当一个类被加载之后,必须要验证一下这个类是否合法,比如这个类是不是符合字节码的格式、变量与方法是不是有重复、数据类型是不是有效、继承与实现是否合乎标准等等。总之,这个阶段的目的就是保证加载的类是能够被jvm所运行。
    2. 准备:准备阶段的工作就是为类的静态变量分配内存并设为jvm默认的初值,对于非静态的变量,则不会为它们分配内存。有一点需要注意,这时候,静态变量的初值为jvm默认的初值,而不是我们在程序中设定的初值。jvm默认的初值是这样的:
      • 基本类型(int、long、short、char、byte、boolean、float、double)的默认值为0。
      • 引用类型的默认值为null。
      • 常量的默认值为我们程序中设定的值,比如我们在程序中定义final static int a = 100,则准备阶段中a的初值就是100。
    3.  解析:这一阶段的任务就是把常量池中的符号引用转换为直接引用。那么什么是符号引用,什么又是直接引用呢?我们来举个例子:我们要找一个人,我们现有的信息是这个人的身份证号是1234567890。只有这个信息我们显然找不到这个人,但是通过公安局的身份系统,我们输入1234567890这个号之后,就会得到它的全部信息:比如安徽省黄山市余暇村18号张三,通过这个信息我们就能找到这个人了。这里,123456790就好比是一个符号引用,而安徽省黄山市余暇村18号张三就是直接引用。在内存中也是一样,比如我们要在内存中找一个类里面的一个叫做show的方法,显然是找不到。但是在解析阶段,jvm就会把show这个名字转换为指向方法区的的一块内存地址,比如c17164,通过c17164就可以找到show这个方法具体分配在内存的哪一个区域了。这里show就是符号引用,而c17164就是直接引用。在解析阶段,jvm会将所有的类或接口名、字段名、方法名转换为具体的内存地址。

            连接阶段完成之后会根据使用的情况(直接引用还是被动引用)来选择是否对类进行初始化。

     

    初始化

           如果一个类被直接引用,就会触发类的初始化。在java中,直接引用的情况有:

    • 通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法。
    • 通过反射方式执行以上三种行为。
    • 初始化子类的时候,会触发父类的初始化。
    • 作为程序入口直接运行时(也就是直接调用main方法)。

            除了以上四种情况,其他使用类的方式叫做被动引用,而被动引用不会触发类的初始化。请看主动引用的示例代码:

    [java]  view plain copy
    1. import java.lang.reflect.Field;  
    2. import java.lang.reflect.Method;  
    3.   
    4. class InitClass{  
    5.     static {  
    6.         System.out.println("初始化InitClass");  
    7.     }  
    8.     public static String a = null;  
    9.     public static void method(){}  
    10. }  
    11.   
    12. class SubInitClass extends InitClass{}  
    13.   
    14. public class Test1 {  
    15.   
    16.     /** 
    17.      * 主动引用引起类的初始化的第四种情况就是运行Test1的main方法时 
    18.      * 导致Test1初始化,这一点很好理解,就不特别演示了。 
    19.      * 本代码演示了前三种情况,以下代码都会引起InitClass的初始化, 
    20.      * 但由于初始化只会进行一次,运行时请将注解去掉,依次运行查看结果。 
    21.      * @param args 
    22.      * @throws Exception 
    23.      */  
    24.     public static void main(String[] args) throws Exception{  
    25.     //  主动引用引起类的初始化一: new对象、读取或设置类的静态变量、调用类的静态方法。  
    26.     //  new InitClass();  
    27.     //  InitClass.a = "";  
    28.     //  String a = InitClass.a;  
    29.     //  InitClass.method();  
    30.           
    31.     //  主动引用引起类的初始化二:通过反射实例化对象、读取或设置类的静态变量、调用类的静态方法。  
    32.     //  Class cls = InitClass.class;  
    33.     //  cls.newInstance();  
    34.           
    35.     //  Field f = cls.getDeclaredField("a");  
    36.     //  f.get(null);  
    37.     //  f.set(null, "s");  
    38.       
    39.     //  Method md = cls.getDeclaredMethod("method");  
    40.     //  md.invoke(null, null);  
    41.               
    42.     //  主动引用引起类的初始化三:实例化子类,引起父类初始化。  
    43.     //  new SubInitClass();  
    44.   
    45.     }  
    46. }  

            上面的程序演示了主动引用触发类的初始化的四种情况。

     

            类的初始化过程是这样的:按照顺序自上而下运行类中的变量赋值语句和静态语句,如果有父类,则首先按照顺序运行父类中的变量赋值语句和静态语句。先看一个例子,首先建两个类用来显示赋值操作:

    [java]  view plain copy
    1. public class Field1{  
    2.     public Field1(){  
    3.         System.out.println("Field1构造方法");  
    4.     }  
    5. }  
    6. public class Field2{  
    7.     public Field2(){  
    8.         System.out.println("Field2构造方法");  
    9.     }  
    10. }  

    下面是演示初始化顺序的代码:

    [java]  view plain copy
    1. class InitClass2{  
    2.     static{  
    3.         System.out.println("运行父类静态代码");  
    4.     }  
    5.     public static Field1 f1 = new Field1();  
    6.     public static Field1 f2;   
    7. }  
    8.   
    9. class SubInitClass2 extends InitClass2{  
    10.     static{  
    11.         System.out.println("运行子类静态代码");  
    12.     }  
    13.     public static Field2 f2 = new Field2();  
    14. }  
    15.   
    16. public class Test2 {  
    17.     public static void main(String[] args) throws ClassNotFoundException{  
    18.         new SubInitClass2();  
    19.     }  
    20. }  

            上面的代码中,初始化的顺序是:第03行,第05行,第11行,第13行。第04行是声明操作,没有赋值,所以不会被运行。而下面的代码:

    [java]  view plain copy
    1. class InitClass2{  
    2.     public static Field1 f1 = new Field1();  
    3.     public static Field1 f2;  
    4.     static{  
    5.         System.out.println("运行父类静态代码");  
    6.     }  
    7. }  
    8.   
    9. class SubInitClass2 extends InitClass2{  
    10.     public static Field2 f2 = new Field2();  
    11.     static{  
    12.         System.out.println("运行子类静态代码");  
    13.     }  
    14. }  
    15.   
    16. public class Test2 {  
    17.     public static void main(String[] args) throws ClassNotFoundException{  
    18.         new SubInitClass2();  
    19.     }  
    20. }  

            初始化顺序为:第02行、第05行、第10行、第12行,各位可以运行程序查看结果。

           在类的初始化阶段,只会初始化与类相关的静态赋值语句和静态语句,也就是有static关键字修饰的信息,而没有static修饰的赋值语句和执行语句在实例化对象的时候才会运行。

     

    使用

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

    • 引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
    • 定义类数组,不会引起类的初始化。
    • 引用类的常量,不会引起类的初始化。

    被动引用的示例代码:

    [java]  view plain copy
    1. class InitClass{  
    2.     static {  
    3.         System.out.println("初始化InitClass");  
    4.     }  
    5.     public static String a = null;  
    6.     public final static String b = "b";  
    7.     public static void method(){}  
    8. }  
    9.   
    10. class SubInitClass extends InitClass{  
    11.     static {  
    12.         System.out.println("初始化SubInitClass");  
    13.     }  
    14. }  
    15.   
    16. public class Test4 {  
    17.   
    18.     public static void main(String[] args) throws Exception{  
    19.     //  String a = SubInitClass.a;// 引用父类的静态字段,只会引起父类初始化,而不会引起子类的初始化  
    20.     //  String b = InitClass.b;// 使用类的常量不会引起类的初始化  
    21.         SubInitClass[] sc = new SubInitClass[10];// 定义类数组不会引起类的初始化  
    22.     }  
    23. }  


            最后总结一下使用阶段:使用阶段包括主动引用和被动引用,主动饮用会引起类的初始化,而被动引用不会引起类的初始化。

            当使用阶段完成之后,java类就进入了卸载阶段。

     

    卸载

           关于类的卸载,笔者在单例模式讨论篇:单例模式与垃圾回收一文中有过描述,在类使用完之后,如果满足下面的情况,类就会被卸载:

    • 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
    • 加载该类的ClassLoader已经被回收。
    • 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

            如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。

     

    总结

            做java的朋友对于对象的生命周期可能都比较熟悉,对象基本上都是在jvm的堆区中创建,在创建对象之前,会触发类加载(加载、连接、初始化),当类初始化完成后,根据类信息在堆区中实例化类对象,初始化非静态变量、非静态代码以及默认构造方法,当对象使用完之后会在合适的时候被jvm垃圾收集器回收。读完本文后我们知道,对象的生命周期只是类的生命周期中使用阶段的主动引用的一种情况(即实例化类对象)。而类的整个生命周期则要比对象的生命周期长的多。


    展开全文
  • Java虚拟机之对象生命周期

    千次阅读 2013-01-26 15:51:55
    要在JVM的堆中创建某类型的实例化对象,必须先完成该类型的加载、连接和初始...下面主要介绍对象生命周期中的这三个阶段。 1. 类实例化  Java程序中,类可以被显式或者隐式地实例化。  显式实例化的4种途径:

           要在JVM的堆中创建某类型的实例化对象,必须先完成该类型的加载、连接和初始化。因此,对象的生命周期只是类生命周期中的使用阶段,而类的生命周期要比对象的生命周期要长。对象的生命周期开始于类的实例化,中间经历使用阶段,结束于对象的垃圾收集。下面主要介绍对象生命周期中的这三个阶段。

    1. 类实例化

        Java程序中,类可以被显式或者隐式地实例化。

        显式实例化的4种途径:

    • 使用new操作符
    • 使用反射,调用java.lang.Class或java.lang.reflect.Constructor的newInstance()方法
    • 使用现有对象的clone()方法
    • 使用反序列化手段,调用java.io.ObjectInputStream的readObject()方法

        隐式实例化的方法:

    • Java的每个命令行参数都会创建相应的String对象,并把它们组织成String数组作为一个参数传递给程序入口main(String args[])方法
    • JVM装载的每个类型,都会隐式地创建描述该类型的Class实例
    • 字符串常量对应一个String对象
    • 字符串操作符“+”的运算结果如果不是编译时常量,会创建一个String对象

        无论采用哪种方式实例化对象,JVM创建对象的步骤如下:

        1) 在堆中为对象分配内存

        2) 实例变量初始化为默认值

        3) 对象初始化,给实例变量赋正确的初始值

     

           Java编译器在编译每个类时都会为该类至少生成一个实例初始化方法——即“<init>()”方法。此方法与源代码中的每个构造方法相对应。如果类没有明确地声明任何构造方法,编译器则为该类生成一个默认的无参构造方法,这个默认的构造器仅仅调用父类的无参构造器,与此同时也会生成一个与默认构造方法对应的“ <init>()”方法。

          <init>()方法内可能包括的代码内容大概为:调用另一个<init>()方法;对实例变量初始化;与其对应的构造方法内的代码。
          如果构造方法是明确地从调用同一个类中的另一个构造方法(一个this()调用)开始,那它对应的<init>()方法体内包括的内容为:一个同类的<init>()方法的调用;对应用构造方法内的所有字节码。
          如果构造方法不是通过调用自身类的其它构造方法开始,并且该对象不是Object对象,那<init>()方法内则包括的内容为:一个对父类<init>()方法的调用;对实例变量初始化方法的字节码;最后是对应构造子的方法体字节码。

          如果构造方法不是通过调用自身类的其它构造方法开始,并且这个类是Object,那么它的<init>()方法则不包括对父类<init>()方法的调用。

          如果构造方法通过明确地调用超类的构造方法(一个super()调用)开始,那它对应的<init>()方法会调用对应的超类的<init>()方法。

          如果构造方法没有明确地从this()或者super()调用开始,对应的<init>()方法默认会调用超类的无参数<init>()方法。

    2. 对象使用

          Java中是通过引用来使用对象。在JDK 1.2以前的版本中,若一个对象不被任何变量引用,那么程序就无法再使用这个对象。也就是说,只有对象处于可触及状态,程序才能使用它。从JDK 1.2版本开始,把对象的引用分为4种级别,从而使程序能更加灵活地控制对象的生命周期。这4种级别由高到低依次为:强引用、软引用、弱引用和虚引用。其中,强引用是应用最普遍的引用。

          java.lang.ref.Reference类的三个直接子类:SoftReference、WeakReference和PhantomReference,分别对应着可触及状态的三个比较弱的形式:软引用、弱引用和虚引用。强引用与较弱形式的引用的最基本区别是:强引用禁止引用目标被垃圾收集器回收,而较弱形式的引用不禁止。创建软引用、弱引用和虚引用,只需要简单地把强引用传递到对应的引用对象的构造方法中。下面介绍一下4种级别的引用的特点。

    • 强引用

         如果一个对象具有强引用,垃圾收集器绝不会回收它。当内存空间不足,JVM宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会考随意回收具有强引用的对象来解决内存不足的问题。

    • 软引用

         如果一个对象具有软引用,当内存空间足够时,垃圾收集器不会回收它;当内存不足时,就会回收这些对象的内存。只要垃圾收集器没有回收它,该对象就可以被程序使用。因此,软引用可用来实现内存敏感的高速缓存。

    • 弱引用

         如果一个对象具有弱引用,当垃圾收集器发现只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾收集器是一个优先级很低的线程,因此不一定会很快发现只具有弱引用的对象。

    • 虚引用

         虚引用不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾收集器回收。虚引用主要用来跟踪对象被垃圾回收的活动。

    3. 垃圾收集

          程序可以显式或者隐式地为对象分配内存,但是不能明确地释放内存。当一个对象不再被程序所引用时,JVM会通过垃圾收集器回收该对象的内存空间。

          如果类声明一个返回值为void的finalize()方法,垃圾收集器会在释放这个类的实例对象所占内存之前执行这个方法一次。由于finalize()方法是一个普通的Java方法,它可以直接被程序所调用。这样的直接调用不会影响垃圾收集器的自动调用过程。垃圾收集器最多只会调用一次对象的finalize()方法。如果finalize()方法执行后,对象重新被引用,随后再次变得不被引用,垃圾收集器不会第二次调用finalize()方法。

         finalize()方法具有如下特点:

    • 垃圾收集器何时执行该方法是不确定的
    • finalize()方法可能使对象复活
    • 垃圾收集器自动调用finalize()方法抛出的任何异常都将被忽略

         在垃圾收集器看来,堆中的对象都处于三种状态之一:

    • 可触及:可以从根节点开始通过追踪“触及”到该对象;
    • 可复活:从根节点开始的追踪图中不可触及,但是可能在垃圾收集器执行某些终结方法时触及;
    • 不可触及:从根节点开始的追踪图中不可触及,同时不可能通过任何终结方法复活。垃圾收集器只回收该状态下的对象。

        垃圾收集器的具体回收策略,可以看之前的博客:

    Java虚拟机之垃圾回收 

    展开全文
  • 这是为了确保正确检查所有代码。 分叉该项目,创建一个功能分支,然后向我们发送拉取请求。 为了确保一致的代码库,您应确保代码遵循PSR-2编码标准。 它是什么? 工作流旨在为企业使用提供轻量级,高度灵活和可...
  • java类和对象生命周期

    万次阅读 多人点赞 2018-07-09 06:55:00
    Java 对象生命周期在JVM运行空间中,对象的整个生命周期大致可以分为7个阶段:创建阶段(Creation)、应用阶段(Using)、不可视阶段(Invisible)、不可到达阶段(Unreachable)、可收集阶段(Collected)、终结...

    Java 对象生命周期

    在JVM运行空间中,对象的整个生命周期大致可以分为7个阶段:创建阶段(Creation)、应用阶段(Using)、不可视阶段(Invisible)、不可到达阶段(Unreachable)、可收集阶段(Collected)、终结阶段(Finalized)与释放阶段(Free)。上面的这7个阶段,构成了 JVM中对象的完整的生命周期。下面分别介绍对象在处于这7个阶段时的不同情形。 

    创建阶段

    在对象创建阶段,系统要通过下面的步骤,完成对象的创建过程:

    (1)为对象分配存储空间。

    (2)开始构造对象。

    (3)递归调用其超类的构造方法。

    (4)进行对象实例初始化与变量初始化。

    (5)执行构造方法体。

    上面的5个步骤中的第3步就是指递归地调用该类所扩展的所有父类的构造方法,一个Java类(除Object类外)至少有一个父类(Object),这个规则既是强制的,也是隐式的。你可能已经注意到在创建一个Java类的时候,并没有显式地声明扩展(extends)一个Object父类。实际上,在 Java程序设计中,任何一个Java类都直接或间接的是Object类的子类。例如下面的代码:

    public class A { 
         … 
    } 
    这个声明等同于下面的声明: 
    public class A extends java.lang.Object { 
         … 
    } 

    上面讲解了对象处于创建阶段时,系统所做的一些处理工作,其中有些过程与应用的性能密切相关,因此在创建对象时,我们应该遵循一些基本的规则,以提高应用的性能。

    下面是在创建对象时的几个关键应用规则:

    (1)避免在循环体中创建对象,即使该对象占用内存空间不大。

    (2)尽量及时使对象符合垃圾回收标准。

    (3)不要采用过深的继承层次。

    (4)访问本地变量优于访问类中的变量。

    关于规则(1)避免在循环体中创建对象,即使该对象占用内存空间不大,需要提示一下,这种情况在我们的实际应用中经常遇到,而且我们很容易犯类似的错误,例如下面的代码:

    … … 
    for (int i = 0; i < 10000; ++i) { 
        Object obj = new Object(); 
        System.out.println("obj= "+ obj); 
    } 
    … … 

    上面代码的书写方式相信对你来说不会陌生,也许在以前的应用开发中你也这样做过,尤其是在枚举一个Vector对象中的对象元素的操作中经常会这样书写,但这却违反了上述规则(1),因为这样会浪费较大的内存空间,正确的方法如下所示:

    … … 
    Object obj = null; 
    for (int i = 0; i < 10000; ++i) { 
        obj = new Object(); 
        System.out.println("obj= "+ obj); 
    } 
    … … 

    采用上面的第二种编写方式,仅在内存中保存一份对该对象的引用,而不像上面的第一种编写方式中代码会在内存中产生大量的对象应用,浪费大量的内存空间,而且增大了系统做垃圾回收的负荷。因此在循环体中声明创建对象的编写方式应该尽量避免。

    另外,不要对一个对象进行多次初始化,这同样会带来较大的内存开销,降低系统性能,如:

    public class A { 
        private Hashtable table = new Hashtable (); 
        public A() { 
            // 将Hashtable对象table初始化了两次 
            table = new Hashtable(); 
        } 
    } 

    正确的方式为:

    public class B { 
         private Hashtable table = new Hashtable (); 
         public B() { 
         } 
    } 

    不要小看这个差别,它却使应用软件的性能相差甚远,如图2-5所示。

     

     

    图2-5  初始化对象多次所带来的性能差别

    看来在程序设计中也应该遵从“勿以恶小而为之”的古训,否则我们开发出来的应用也是低效的应用,有时应用软件中的一个极小的失误,就会大幅度地降低整个系统的性能。因此,我们在日常的应用开发中,应该认真对待每一行代码,采用最优化的编写方式,不要忽视细节,不要忽视潜在的问题。

    应用阶段

    当对象的创建阶段结束之后,该对象通常就会进入对象的应用阶段。这个阶段是对象得以表现自身能力的阶段。也就是说对象的应用阶段是对象整个生命周期中证明自身“存在价值”的时期。在对象的应用阶段,对象具备下列特征:

    ◆系统至少维护着对象的一个强引用(Strong Reference);

    ◆所有对该对象的引用全部是强引用(除非我们显式地使用了:软引用(Soft Reference)、弱引用(Weak Reference)或虚引用(Phantom Reference))。

    上面提到了几种不同的引用类型。可能一些读者对这几种引用的概念还不是很清楚,下面分别对之加以介绍。在讲解这几种不同类型的引用之前,我们必须先了解一下Java中对象引用的结构层次。

    Java对象引用的结构层次示意如图2-6所示。

     

    图2-6  对象引用的结构层次示意

    由图2-6我们不难看出,上面所提到的几种引用的层次关系,其中强引用处于顶端,而虚引用则处于底端。下面分别予以介绍。

    1.强引用

    强引用(Strong Reference)是指JVM内存管理器从根引用集合(Root Set)出发遍寻堆中所有到达对象的路径。当到达某对象的任意路径都不含有引用对象时,对这个对象的引用就被称为强引用。

    2.软引用

    软引用(Soft Reference)的主要特点是具有较强的引用功能。只有当内存不够的时候,才回收这类内存,因此在内存足够的时候,它们通常不被回收。另外,这些引用对象还能保证在Java抛出OutOfMemory 异常之前,被设置为null。它可以用于实现一些常用资源的缓存,实现Cache的功能,保证最大限度的使用内存而不引起OutOfMemory。再者,软可到达对象的所有软引用都要保证在虚拟机抛出OutOfMemoryError之前已经被清除。否则,清除软引用的时间或者清除不同对象的一组此类引用的顺序将不受任何约束。然而,虚拟机实现不鼓励清除最近访问或使用过的软引用。下面是软引用的实现代码:

    … … 
    import java.lang.ref.SoftReference; 
    … 
    A a = new A(); 
    … 
    // 使用 a 
    … 
    // 使用完了a,将它设置为soft 引用类型,并且释放强引用; 
    SoftReference sr = new SoftReference(a); 
    a = null; 
    … 
    // 下次使用时 
    if (sr!=null) { 
         a = sr.get(); 
    } 
    else{ 
         // GC由于内存资源不足,可能系统已回收了a的软引用, 
         // 因此需要重新装载。 
         a = new A(); 
         sr=new SoftReference(a); 
    } 
    … … 

    软引用技术的引进,使Java应用可以更好地管理内存,稳定系统,防止系统内存溢出,避免系统崩溃(crash)。因此在处理一些占用内存较大而且声明周期较长,但使用并不频繁的对象时应尽量应用该技术。正像上面的代码一样,我们可以在对象被回收之后重新创建(这里是指那些没有保留运行过程中状态的对象),提高应用对内存的使用效率,提高系统稳定性。但事物总是带有两面性的,有利亦有弊。在某些时候对软引用的使用会降低应用的运行效率与性能,例如:应用软引用的对象的初始化过程较为耗时,或者对象的状态在程序的运行过程中发生了变化,都会给重新创建对象与初始化对象带来不同程度的麻烦,有些时候我们要权衡利弊择时应用。

    3.弱引用

    弱引用(Weak Reference)对象与Soft引用对象的最大不同就在于:GC在进行回收时,需要通过算法检查是否回收Soft引用对象,而对于Weak引用对象, GC总是进行回收。因此Weak引用对象会更容易、更快被GC回收。虽然,GC在运行时一定回收Weak引用对象,但是复杂关系的Weak对象群常常需要好几次GC的运行才能完成。Weak引用对象常常用于Map数据结构中,引用占用内存空间较大的对象,一旦该对象的强引用为null时,对这个对象引用就不存在了,GC能够快速地回收该对象空间。与软引用类似我们也可以给出相应的应用代码:

    … … 
    import java.lang.ref.WeakReference; 
    … 
    A a = new A(); 
    … 
    // 使用 a 
    … 
    // 使用完了a,将它设置为weak 引用类型,并且释放强引用; 
    WeakReference wr = new WeakReference (a); 
    a = null; 
    … 
    // 下次使用时 
    if (wr!=null) { 
        a = wr.get(); 
    } 
    else{ 
        a = new A(); 
        wr = new WeakReference (a); 
    } 
    … … 

    弱引用技术主要适用于实现无法防止其键(或值)被回收的规范化映射。另外,弱引用分为“短弱引用(Short Week Reference)”和“长弱引用(Long Week Reference)”,其区别是长弱引用在对象的Finalize方法被GC调用后依然追踪对象。基于安全考虑,不推荐使用长弱引用。因此建议使用下面的方式创建对象的弱引用。

    … … 
    WeakReference wr = new WeakReference(obj); 
    或 
    WeakReference wr = new WeakReference(obj, false); 
    … … 

    4.虚引用

    虚引用(Phantom Reference)的用途较少,主要用于辅助finalize函数的使用。Phantom对象指一些执行完了finalize函数,并且为不可达对象,但是还没有被GC回收的对象。这种对象可以辅助finalize进行一些后期的回收工作,我们通过覆盖Reference的clear()方法,增强资源回收机制的灵活性。虚引用主要适用于以某种比 java 终结机制更灵活的方式调度 pre-mortem 清除操作。

    &注意  在实际程序设计中一般很少使用弱引用与虚引用,使用软引用的情况较多,这是因为软引用可以加速JVM对垃圾内存的回收速度,可以维护系统的运行安全,防止内存溢出(OutOfMemory)等问题的产生。

    不可视阶段

    在一个对象经历了应用阶段之后,那么该对象便处于不可视阶段,说明我们在其他区域的代码中已经不可以再引用它,其强引用已经消失,例如,本地变量超出了其可视范围,如下所示。

    … … 
    public void process () { 
        try { 
             Object obj = new Object(); 
             obj.doSomething(); 
         } catch (Exception e) { 
         e.printStackTrace(); 
         } 
         while (isLoop) { // ... loops forever 
          // 这个区域对于obj对象来说已经是不可视的了 
             // 因此下面的代码在编译时会引发错误 
             obj.doSomething();  
         } 
    } 
    … … 

    如果一个对象已使用完,而且在其可视区域不再使用,此时应该主动将其设置为空(null)。可以在上面的代码行obj.doSomething();下添加代码行obj = null;,这样一行代码强制将obj对象置为空值。这样做的意义是,可以帮助JVM及时地发现这个垃圾对象,并且可以及时地回收该对象所占用的系统资源。

    不可到达阶段

    处于不可到达阶段的对象,在虚拟机所管理的对象引用根集合中再也找不到直接或间接的强引用,这些对象通常是指所有线程栈中的临时变量,所有已装载的类的静态变量或者对本地代码接口(JNI)的引用。这些对象都是要被垃圾回收器回收的预备对象,但此时该对象并不能被垃圾回收器直接回收。其实所有垃圾回收算法所面临的问题是相同的——找出由分配器分配的,但是用户程序不可到达的内存块。

    可收集阶段、终结阶段与释放阶段

    对象生命周期的最后一个阶段是可收集阶段、终结阶段与释放阶段。当对象处于这个阶段的时候,可能处于下面三种情况:

    (1)垃圾回收器发现该对象已经不可到达。

    (2)finalize方法已经被执行。

    (3)对象空间已被重用。

    当对象处于上面的三种情况时,该对象就处于可收集阶段、终结阶段与释放阶段了。虚拟机就可以直接将该对象回收了。




    Java 类生命周期

     一.类的生命周期

    java类的生命周期就是指一个class文件从加载到卸载的全过程。

    类的完整生命周期包括7个部分:加载——验证——准备——解析——初始化——使用——卸载,如下图所示


      其中,验证——准备——解析  称为连接阶段,除了解析外,其他阶段是顺序发生的,而解析可以与这些阶段交叉进行,因为Java支持动态绑定(晚期绑定),需要运行时才能确定具体类型;在使用阶段实例化对象。

    二:类的初始化触发

         类的加载机制没有明确的触发条件,但是有5种情况下必须对类进行初始化,那么 加载——验证——准备 就必须在此之前完成了。

       1:通过new关键字实例化对象、读取或设置类的静态变量、调用类的静态方法。

       2:通过反射方式执行以上三种行为。

       3:初始化子类的时候,会触发父类的初始化。

       4:虚拟机启动时,初始化一个执行主类;(作为程序入口直接运行时(也就是直接调用main方法)。

       5、使用jdk1.7的动态语言支持时,如果一个java.lang.invoke.MethodHandle实例最后的解析结果REF_getStatic、REF_putStatic、RE_invokeStatic的方法句柄,并且这个方法句柄对应的类没有进行初始化,则需要先触发其初始化。

       注意,有且只有五种情况必须对类进行初始化,这五种情况被称为“主动引用”,除了这五种情况,所有其他的类引用方式都不会触发类初始化,被称为“被动引用”。

    请看主动引用的示例代码:


    [java]  view plain  copy
    1. import java.lang.reflect.Field;  
    2. import java.lang.reflect.Method;  
    3.   
    4. class InitClass{  
    5.     static {  
    6.         System.out.println("初始化InitClass");  
    7.     }  
    8.     public static String a = null;  
    9.     public static void method(){}  
    10. }  
    11.   
    12. class SubInitClass extends InitClass{}  
    13.   
    14. public class Test1 {  
    15.   
    16.     /** 
    17.      * 主动引用引起类的初始化的第四种情况就是运行Test1的main方法时 
    18.      * 导致Test1初始化,这一点很好理解,就不特别演示了。 
    19.      * 本代码演示了前三种情况,以下代码都会引起InitClass的初始化, 
    20.      * 但由于初始化只会进行一次,运行时请将注解去掉,依次运行查看结果。 
    21.      * @param args 
    22.      * @throws Exception 
    23.      */  
    24.     public static void main(String[] args) throws Exception{  
    25.     //  主动引用引起类的初始化一: new对象、读取或设置类的静态变量、调用类的静态方法。  
    26.     //  new InitClass();  
    27.     //  InitClass.a = "";  
    28.     //  String a = InitClass.a;  
    29.     //  InitClass.method();  
    30.           
    31.     //  主动引用引起类的初始化二:通过反射实例化对象、读取或设置类的静态变量、调用类的静态方法。  
    32.     //  Class cls = InitClass.class;  
    33.     //  cls.newInstance();  
    34.           
    35.     //  Field f = cls.getDeclaredField("a");  
    36.     //  f.get(null);  
    37.     //  f.set(null, "s");  
    38.       
    39.     //  Method md = cls.getDeclaredMethod("method");  
    40.     //  md.invoke(null, null);  
    41.               
    42.     //  主动引用引起类的初始化三:实例化子类,引起父类初始化。  
    43.     //  new SubInitClass();  
    44.   
    45.     }  
    46. }  

    请看被动引用的示例代码:

    三.类的加载过程

    从用户角度来说,类(对象)的生命周期只需笼统理解为“加载——使用——卸载”即可,无需太过深入。所以,这里的类加载过程就是我们说的 加载——验证——准备——解析——初始化  这五个使用前的阶段。

    1:加载

           加载阶段,虚拟机需要完成三件事:通过类名字获取类的二进制字节流——将字节流的内容转存到方法区——在内存中生成一个Class对象作为该类方法区数据的访问入口。

     其中,第一步:通过类名获取类的二进制字节流是通过类加载器来完成的。其加载过程使用“双亲委派模型”:

    类加载器的层次结构为:


    启动类加载器:加载系统环境变量下JAVA_HOME/lib目录下的类库。

           扩展类加载器:加载JAVA_HOME/lib/ext目录下的类库。

           应用程序类加载器(系统类加载器):加载用户类路径Class_Path指定的类库。(我们可以在使用第三方插件时,把jar包添加到ClassPath后就是使用了这个加载器)

           自定义加载器:如果需要自定义加载时的规则(比如:指定类的字节流来源、动态加载时性能优化等),可以自己实现类加载器。

           双亲委派模型是指:当一个类加载器收到类加载请求时,不会直接加载这个类,而是把这个加载请求委派给自己父加载器去完成。如果父加载器无法加载时,子加载器才会去尝试加载。

           采用双亲委派模型的原因:避免同一个类被多个类加载器重复加载。

    2:验证

    当一个类被加载之后,必须要验证一下这个类是否合法,比如这个类是不是符合字节码的格式、变量与方法是不是有重复、数据类型是不是有效、继承与实现是否合乎标准等等。总之,这个阶段的目的就是保证加载的类是能够被jvm所运行。

    3:准备

           为类变量(静态变量)在方法区分配内存,并设置零值。注意:这里是类变量,不是实例变量,实例变量是对象分配到堆内存时根据运行时动态生成的。

    4:解析

           把常量池中的符号引用解析为直接引用:根据符号引用所作的描述,在内存中找到符合描述的目标并把目标指针指针返回。

     5:初始化

         类的初始化过程是这样的:按照顺序自上而下运行类中的变量赋值语句和静态语句,如果有父类,则首先按照顺序运行父类中的变量赋值语句和静态语句。先看一个例子,首先建两个类用来显示赋值操作:

    [java]  view plain  copy
    1. public class Field1{  
    2.     public Field1(){  
    3.         System.out.println("Field1构造方法");  
    4.     }  
    5. }  
    6. public class Field2{  
    7.     public Field2(){  
    8.         System.out.println("Field2构造方法");  
    9.     }  
    10. }  

    下面是演示初始化顺序的代码:

    1. class InitClass2{  
    2.     static{  
    3.         System.out.println("运行父类静态代码");  
    4.     }  
    5.     public static Field1 f1 = new Field1();  
    6.     public static Field1 f2;   
    7. }  
    8.   
    9. class SubInitClass2 extends InitClass2{  
    10.     static{  
    11.         System.out.println("运行子类静态代码");  
    12.     }  
    13.     public static Field2 f2 = new Field2();  
    14. }  
    15.   
    16. public class Test2 {  
    17.     public static void main(String[] args) throws ClassNotFoundException{  
    18.         new SubInitClass2();  
    19.     }  
    20. }  

    上面的代码中,初始化的顺序是:第03行,第05行,第11行,第13行。第04行是声明操作,没有赋值,所以不会被运行。而下面的代码:

    1. class InitClass2{  
    2.     public static Field1 f1 = new Field1();  
    3.     public static Field1 f2;  
    4.     static{  
    5.         System.out.println("运行父类静态代码");  
    6.     }  
    7. }  
    8.   
    9. class SubInitClass2 extends InitClass2{  
    10.     public static Field2 f2 = new Field2();  
    11.     static{  
    12.         System.out.println("运行子类静态代码");  
    13.     }  
    14. }  
    15.   
    16. public class Test2 {  
    17.     public static void main(String[] args) throws ClassNotFoundException{  
    18.         new SubInitClass2();  
    19.     }  
    20. }  

     初始化顺序为:第02行、第05行、第10行、第12行,各位可以运行程序查看结果。

           在类的初始化阶段,只会初始化与类相关的静态赋值语句和静态语句,也就是有static关键字修饰的信息,而没有static修饰的赋值语句和执行语句在实例化对象的时候才会运行。

    四使用

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

    • 引用父类的静态字段,只会引起父类的初始化,而不会引起子类的初始化。
    • 定义类数组,不会引起类的初始化。
    • 引用类的常量,不会引起类的初始化

    被动引用的示例代码:

    1. class InitClass{  
    2.     static {  
    3.         System.out.println("初始化InitClass");  
    4.     }  
    5.     public static String a = null;  
    6.     public final static String b = "b";  
    7.     public static void method(){}  
    8. }  
    9.   
    10. class SubInitClass extends InitClass{  
    11.     static {  
    12.         System.out.println("初始化SubInitClass");  
    13.     }  
    14. }  
    15.   
    16. public class Test4 {  
    17.   
    18.     public static void main(String[] args) throws Exception{  
    19.     //  String a = SubInitClass.a;// 引用父类的静态字段,只会引起父类初始化,而不会引起子类的初始化  
    20.     //  String b = InitClass.b;// 使用类的常量不会引起类的初始化  
    21.         SubInitClass[] sc = new SubInitClass[10];// 定义类数组不会引起类的初始化  
    22.     }  

      最后总结一下使用阶段:使用阶段包括主动引用和被动引用,主动饮用会引起类的初始化,而被动引用不会引起类的初始化。

      当使用阶段完成之后,java类就进入了卸载阶段。

    五卸载

           在类使用完之后,如果满足下面的情况,类就会被卸载:

    • 该类所有的实例都已经被回收,也就是java堆中不存在该类的任何实例。
    • 加载该类的ClassLoader已经被回收。
    • 该类对应的java.lang.Class对象没有任何地方被引用,无法在任何地方通过反射访问该类的方法。

            如果以上三个条件全部满足,jvm就会在方法区垃圾回收的时候对类进行卸载,类的卸载过程其实就是在方法区中清空类信息,java类的整个生命周期就结束了。


    总结

            做java的朋友对于对象的生命周期可能都比较熟悉,对象基本上都是在jvm的堆区中创建,在创建对象之前,会触发类加载(加载、连接、初始化),当类初始化完成后,根据类信息在堆区中实例化类对象,初始化非静态变量、非静态代码以及默认构造方法,当对象使用完之后会在合适的时候被jvm垃圾收集器回收。读完本文后我们知道,对象的生命周期只是类的生命周期中使用阶段的主动引用的一种情况(即实例化类对象)。而类的整个生命周期则要比对象的生命周期长的多。






    展开全文
  • Mybatis核心对象生命周期

    千次阅读 2017-06-02 18:37:22
    Mybatis核心对象生命周期SqlSessionFactory每 一 个 MyBatis 的 应 用 程 序 都 以 一 个 SqlSessionFactory 对 象 的 实 例 为 核 心 。 SqlSessionFactory 对 象 的 实 例 可 以 通 过 SqlSessionFactoryBuilder...

    Mybatis核心对象及生命周期

    SqlSessionFactory

    每 一 个 MyBatis 的 应 用 程 序 都 以 一 个 SqlSessionFactory 对 象 的 实 例 为 核 心 。 SqlSessionFactory 对 象 的 实 例 可 以 通 过 SqlSessionFactoryBuilder 对 象 来 获 得 。 SqlSessionFactoryBuilder 对象可以从 XML 配置文件,或从 Configuration 类的习惯准备的实 例中构建 SqlSessionFactory 对象。

    从 XML 文件中构建 SqlSessionFactory 的实例非常简单。这里建议你使用类路径下的资 源文件来配置,但是你可以使用任意的 Reader 实例,这个实例包括由文字形式的文件路径 或 URL 形式的文件路径 file://来创建。MyBatis 包含了一些工具类,称作为资源,这些工具 类包含一些方法,这些方法使得从类路径或其他位置加载资源文件更加简单。

    String resource = "org/mybatis/example/mybatis-config.xml";
    InputStream inputStream = Resources.getResourceAsStream(resource);
    sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream);

    SqlSession

    现在,我们已经知道如何获取 SqlSessionFactory 对象了,基于同样的启示,我们就可以 获得 SqlSession 的实例了。 SqlSession 对象完全包含以数据库为背景的所有执行 SQL 操作的 方法。你可以用 SqlSession 实例来直接执行已映射的 SQL 语句。例如:

    SqlSession session = sqlSessionFactory.openSession();
    try {
    Blog blog = (Blog) session.selectOne("org.mybatis.example.BlogMapper.selectBlog", 101);
    } finally {
    session.close();
    }

    这种方法起到的作用, 和我们使用之前的 MyBatis 版本是相似的, 现在有一种更简洁的 方法。使用合理描述参数和 SQL 语句返回值的接口(比如 BlogMapper.class) ,这样现在就 可以至此那个更简单,更安全的代码,没有容易发生的字符串文字和转换的错误。
    例如:

    SqlSession session = sqlSessionFactory.openSession();
    try {
    BlogMapper mapper = session.getMapper(BlogMapper.class);
    Blog blog = mapper.selectBlog(101);
    } finally {
    session.close();
    }

    范围和生命周期

    理解我们目前已经讨论过的不同范围和生命周期类是很重要的。 不正确的使用它们会导 致严重的并发问题。

    SqlSessionFactoryBuilder

    这个类可以被实例化,使用和丢弃。一旦你创建了 SqlSessionFactory 后,这个类就不需 要存在了。 因此 SqlSessionFactoryBuilder 实例的最佳范围是方法范围 (也就是本地方法变量)。 你可以重用 SqlSessionFactoryBuilder 来创建多个 SqlSessionFactory 实例, 但是最好的方式是 不需要保持它一直存在来保证所有 XML 解析资源,因为还有更重要的事情要做。

    SqlSessionFactory

    一旦被创建,SqlSessionFactory 应该在你的应用执行期间都存在。没有理由来处理或重 新创建它。 使用 SqlSessionFactory 的最佳实践是在应用运行期间不要重复创建多次。 这样的 操作将被视为是非常糟糕的。 因此 SqlSessionFactory 的最佳范围是应用范围。 有很多方法可 以做到, 最简单的就是使用单例模式或者静态单例模式。

    SqlSession

    每个线程都应该有它自己的 SqlSession 实例。SqlSession 的实例不能被共享,也是线程 不安全的。因此最佳的范围是请求或方法范围。绝对不能将 SqlSession 实例的引用放在一个 类的静态字段甚至是实例字段中。 也绝不能将 SqlSession 实例的引用放在任何类型的管理范 围中, 比如 Serlvet 架构中的 HttpSession。 如果你现在正用任意的 Web 框架, 要考虑 SqlSession 放在一个和 HTTP 请求对象相似的范围内。换句话说,基于收到的 HTTP 请求,你可以打开 了一个 SqlSession,然后返回响应,就可以关闭它了。关闭 Session 很重要,你应该确保使 用 finally 块来关闭它。下面的示例就是一个确保 SqlSession 关闭的基本模式:

    SqlSession session = sqlSessionFactory.openSession();
    try {
    // do work
    } finally {
    session.close();
    }

    Mapper 实例

    映射器是你创建绑定映射语句的接口。映射器接口的实例可以从 SqlSession 中获得。那 么从技术上来说,当被请求时,任意映射器实例的最宽范围和 SqlSession 是相同的。然而, 映射器实例的最佳范围是方法范围。也就是说,它们应该在使用它们的方法中被请求,然后 就抛弃掉。它们不需要明确地关闭,那么在请求对象中保留它们也就不是什么问题了,这和 SqlSession 相似。你也许会发现,在这个水平上管理太多的资源的话会失控。保持简单,将 映射器放在方法范围内。下面的示例就展示了这个实践:

    SqlSession session = sqlSessionFactory.openSession();
    try {
    BlogMapper mapper = session.getMapper(BlogMapper.class);
    // do work
    } finally {
    session.close();
    }
    展开全文
  • A.Servlet的生命周期由Servlet实例控制 B.init()方法在创建完Servlet实例后对其进行初始化,传递的参数为实现ServletContext接口的对象 C.service()方法响应客户端发出的请求 D.destroy()方法释放...
  • 下列有关Servlet的生命周期,说法不正确的是? 正确答案: Servlet的生命周期分为5个阶段:加载、创建、初始化、处理客户请求、卸载
  • activity的生命周期描述

    千次阅读 2014-09-23 13:47:30
    下表详细描述了这些方法,并在activity的整个生命周期中定位了它们。 方法 描述 是否可被杀死(Killable?) 下一个 onCreate() 在activity第一次被创建的时候调用...
  • JSP生命周期

    千次阅读 2019-01-12 15:26:43
    说到jsp的生命周期,其实看可以参考servlet的生命周期,jsp先将jsp转换为servlet,然后再将servlet编译成可执行类,再将原始请求交给servlet引擎,接下来就是跟servlet差不多的与生命周期过程了;实际上,JSP的生命...
  • 创建对象的方式用new语句创建对象。使用反射,调用java.lang.Class或java.lang.reflect.Constructor的newInstance()实例方法。调用对象的clone()方法使用反序列化手段,调用java.io.ObjectInputStream对象的...
  • 容器生命周期--Lifecycle

    万次阅读 2021-03-09 00:28:15
    Spring容器级生命周期回调——LifeCycle_tinysakura的博客-CSDN博客【Spring源码分析】39-Spring容器生命周期回调接口——LifeCycle_通往神秘的道路的专栏-CSDN博客Lifecycle和SmartLifecycle的正确使用姿势_咔咔...
  • 项目的生命周期描述项目从开始到结束所经历的各个阶段。由阶段组成(通常包括项目规划阶段、实施阶段和完成阶段等,每个阶段确定了开始和结束点,每个阶段都有质量保证QA/质量测试QC人员对阶段的里程碑点进行检查...
  • servlet的生命周期描述

    千次阅读 2018-10-21 19:37:55
    加载并实例化 -> 初始化 -> 应用阶段(请求处理) -> 销毁 1.创建 Servlet 实例。 2.Web 容器调用 Servlet 的init()方法,对Servlet 进行初始化。 ...3.Servlet 初始化后,将一直存在于容器中,用于响应...
  • 本节将Activity的生命周期分为两部分内容,一部分是典型情况下的生命周期,另一部分是异常情况下的生命周期。所谓典型情况下的生命周期,是指在有用户参与的情况下,Activity所经过的生命周期的改变;而异常情况下的...
  • 微信小程序生命周期可分为应用生命周期、页面生命周期和组件生命周期,本文结合微信官方文档浅析微信小程序的应用生命周期。从注册一个小程序生命周期开始讲起,到一些关于开发的小经验。
  • 什么是软件生命周期

    千次阅读 2017-03-03 16:20:35
    软件生命周期又称为软件生存周期或系统开发生命周期,是软件的产生直到报废的生命周期,周期内有问题定义、可行性分析、总体描述、系统设计、编码、调试和测试、验收与运行、维护升级到废弃等阶段,这种按时间分程的...
  • 微信小程序生命周期

    2020-07-31 08:05:41
    目录应用生命周期页面生命周期A页面wx.navigateTo跳转到B页面时,两个页面生命周期的逻辑应用生命周期影响页面声明周期组件生命周期 应用生命周期                  ...
  • 软件生命周期又称为软件生存周期或系统开发生命周期,是软件的产生直到报废的生命周期,周期内有问题定义、可行性分析、总体描述、系统设计、编码、调试和测试、验收与运行、维护升级到废弃等阶段,这种按时间分程的...
  • 这一章以一个Java类型(类或接口)的生命周期为例来讨论开始阶段的装载、连接和初始化,以及占Java类型生命周期绝大部分时间的对象实例化、垃圾收集和对象终结,然后是java类型生命周期的结束,也就是从虚拟机中卸载...
  • Spring容器中bean的生命周期

    千次阅读 多人点赞 2019-11-01 23:43:11
    在传统的Java应用中,bean的生命周期很简单。使用Java关键字new进行bean实例化...正确理解Spring bean的生命周期非常重要,因为你或许要利用Spring提供的扩展点来自定义bean的创建过程。下图展示了bean装载到Spring...
  • java的生命周期

    千次阅读 2019-08-20 17:57:49
    类的生命周期总览 类从被加载到虚拟机内存中开始, 到卸载出内存为止, 它的整个生命周期包括: 加载 (Loading) 、 验证(Verification) 、 准备(Preparation) 、 解析(Resolution) 、 初始化...
  • 欢迎访问我的个人网站获取更佳阅读排版 golang 网络编程之如何正确关闭tcp连接以及管理它的生命周期 | yoko blog (https://pengrl.com/p/47401/) 本篇文章部分内容涉及到tcp协议以及socket编程的通用底层知识。讨论...
  • Android MediaPlayer的生命周期

    千次阅读 2016-10-23 22:57:20
    MediaPlayer的状态转换图也表征了它的生命周期,搞清楚这个图可以帮助我们在使用MediaPlayer时考虑情况更周全,写出的代码也更具健壮性。 接下来用几张图,来慢慢演变它的生命周期过程: 图一:初识几个API,了解...
  • 关于Spring Bean的生命周期

    万次阅读 多人点赞 2018-10-08 10:47:04
     Spring Bean 的生命周期在整个 Spring 中占有很重要的位置,从BeanFactory或ApplicationContext取得的实例为Singleton,也就是预设为每一个Bean的别名只能维持一个实例,而不是每次都产生一个新的对象使用...
  • 类的生命周期

    2017-09-18 17:59:44
    四反射中ClassforName和ClassLoaderloadClass的区别一:类生命周期概况JVM为Java程序提供运行时环境(runtime environment),管理类和对象生命周期是JVM的重要工作之一。类的生命周期从类被加载、连接、初始化...
  • 描述下Activity的生命周期

    千次阅读 2011-05-27 15:55:00
    Activity 生命周期
  • 关于vue生命周期的简述

    千次阅读 2019-01-28 10:35:45
    学习vue有一段时间了,简单谈一下关于vue生命周期,因为这个还是很重要的,理解了vue的生命周期对于以后我们做复杂的大型项目的是很有帮助的,写在前面,本人也是搬运工一枚,在网上搜集的资料然后自己动手操作,如...
  • React生命周期及事件详解

    万次阅读 2016-03-21 10:01:31
    一、组件的详细说明和生命周期...当通过调用 React.createClass() 来创建组件的时候,你应该提供一个包含 render 方法的对象,并且也可以包含其它的在这里描述生命周期方法。 render ReactComponent render

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 88,791
精华内容 35,516
关键字:

对象生命周期描述正确的是