Java对象的生命周期与垃圾回收以及四种引用

导读:本篇文章讲解 Java对象的生命周期与垃圾回收以及四种引用,希望对大家有帮助,欢迎收藏,转发!站点地址:www.bmabk.com

创建对象的方式

  • new语句创建对象。
  • 使用反射,调用java.lang.Class或java.lang.reflect.Constructor的newInstance()实例方法。
  • 调用对象的clone()方法
  • 使用反序列化手段,调用java.io.ObjectInputStream对象的readObject()方法。 

还有其他一些隐式创建对象的方法: 

  • 对于java命令中的每个命令行参数,Java虚拟机都会创建相应的String对象,并把它们组织到一个String数组中,再把该数组作为参数传递给程序入口main(String args[])方法。
  • String类型的直接量对应一个String对象。
  • 字符串操作符”+”的运算结果为一个新的String对象。
  • 当JVM加载一个类时,会隐含地创建描述这个类的Class实例。 

不管采取哪种方式创建对象,JVM创建一个对象都包含以下步骤:

(1) 给对象分配内存。

(2) 将对象的实例变量自动初始化为其类型的默认值。

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

 

对于以上第三个步骤,JVM可采用3种方式来初始化对象,到底采用何种初始化方式取决于创建对象的方式。

 

  • 如果对象是通过clone()方法创建的,那么JVM把原来被克隆对象的实例变量的值拷贝到新对象中。
  • 如果对象通过ObjectInputStream类的readObject()方法创建的,那么JVM通过输入流中读入的序列化数据来初始化那些非暂时性的实例变量。
  • 在其他情况下,如果实例变量在声明时被显示初始化,那么就把初始化值赋给实例变量,接着再执行构造方法。这是最简单初始化对象的方式。 

垃圾回收

Java语言中,内存回收任务由JVM来担当。

在程序的运行环境中,JVM提供了一个系统级的垃圾回收器线程,它负责自动回收那些无用对象所占用的内存。这种内存回收的过程被称为垃圾回收。

 

垃圾回收具有以下优点:

 

  • 程序员从复杂的内存追踪,监测和释放等工作解放出来,减轻程序员进行内存管理的负担。
  • 防止系统内存被非法释放,从而使系统更加健壮和稳定。
  • 只有当对象不再被程序中的任何引用变量引用时,它的内存才可能被回收。
  • 程序无法迫使垃圾回收器立即执行垃圾回收操作。
  • 当垃圾回收器将要回收无用对象的内存时,先调用该对象的finalize()方法,该方法有可能使对象使对象复活,导致垃圾回收器取消回收该对象的内存。 

对象的可触及性

在JVM的垃圾回收器来看。堆区中的每个对象都肯能处于以下三个状态之一:

 

  • 可触及状态:当一个对象被创建后,只要程序中还有引用变量引用该对象,那么它就始终处于可触及状态。
  • 可复活状态:当程序不再有任何引用变量引用对象时,它就进入可复活状态。该状态的对象,垃圾回收器会准备释放它占用的内存,在释放前,会调用它的finalize()方法,这些finalize()方法有可能使对象重新转到可触及状态。
  • 不可触及状态:当JVM执行完所有的可复活状态的finalize()方法后,假如这些方法都没有使对象转到可触及状态。那么该对象就进入不可触及状态。只有当对象处于不可触及状态时,垃圾回收器才会真正回收它占用的内存。 

垃圾回收的时间

    当一个对象处于可复活状态时,垃圾回收线程执行它的finalize()方法,任何使它转到不可触及状态,任何回收它占用的内存,这对于程序来说都是透明的。程序只能决定一个对象任何不再被任何引用变量引用,使得它成为可以被回收的垃圾。

 

    类比:居民把无用物品放在指定的地方,清洁工人会把它收拾走。但垃圾被收走的时间,居民是不知道的,也无需了解。

 

    垃圾回收器作为低优先级线程独立运行。在任何时候,程序都无法迫使垃圾回收器立即执行垃圾回收操作。

程序中可调用System.gc()或Runtime.gc()方法提示垃圾回收器尽快执行垃圾回收操作,但是不能保证调用后垃圾回收器会立即执行垃圾回收。

 

    类比:小区垃圾成堆时,居民打电话给环保局,催促清洁工尽快来处理垃圾。但是清洁工不一定立即就来了,也有可能很长时间后再来。 

 

对象的finalize()方法简介

finalize()定义在Object类中:

protected void finalize() throws Throwable

因为该方法为protected,所以任何Java类都可以覆盖finalize()方法,该方法中进行释放对象所占的相关资源的操作。

 

注意:

JVM的垃圾回收操作对程序来说都是透明的。因此程序无法预料某个无用对象的finalize()方法何时被调用。

 

 

finalize()方法的特点:

 

  • 垃圾回收器是否会执行该方法及何时执行该方法,都是不确定的。
  • finalize()方法有可能使对象复活,使它恢复到可触及状态。
  • 垃圾回收器在执行finalize()方法时,如果出现异常,垃圾回收器不会报告异常,程序继续正常运行。

 

对象的强,软,弱,虚引用

 

强引用

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

 

软引用

    如果一个对象具有软引用。如果内存空间足够。垃圾回收器不会回收它。如果内存不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。

    软引用可以和一个引用队列(ReferenceQueue)联合使用,如果软引用所引用的对象被垃圾回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列中。

    软引用可用来实现内存敏感的高速缓存。

 

弱引用

    如果一个对象具有弱引用。当垃圾回收器发现只具有弱引用对象,不管当前内存空间足够与否,都会回收它的内存。

不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现只具有弱引用的对象。 

 

虚引用

    虚引用不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列(ReferenceQueue)联合使用。当垃 圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是 否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。程序如果发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

 

下面这个实验代码很好的探究了四种引用级别对垃圾回收的影响

[java] 
view plain
 copy

  1. import java.lang.ref.PhantomReference; //虚引用  
  2. import java.lang.ref.ReferenceQueue; //引用队列  
  3. import java.lang.ref.SoftReference; //软引用  
  4. import java.lang.ref.WeakReference; //弱引用  
  5.   
  6. class Obj   
  7. {  
  8.     private final String name;  
  9.     public Obj(String name)  
  10.     {  
  11.         this.name = name;  
  12.     }  
  13.       
  14.     @Override  
  15.     protected void finalize() throws Throwable  
  16.     {  
  17.         System.out.println(“执行finalize方法”+name);  
  18.         super.finalize();  
  19.     }  
  20. }  
  21.   
  22. public class Test  
  23. {  
  24.     public static void main (String[] args)  
  25.     {  
  26. //      hardTest();  
  27. //      softTest();  
  28. //      weakTest();  
  29.         phanTest();  
  30.     }  
  31.       
  32.     public static void hardTest()  
  33.     {  
  34.         Obj hard = new Obj(“hard”);  
  35. //      hard = null;  
  36.         System.gc();  
  37.         System.out.println(hard);  
  38.     }  
  39.       
  40.     public static void softTest()  
  41.     {  
  42.         SoftReference <Obj> soft = new SoftReference<Obj>(new Obj(“soft”));  
  43.         System.gc();  
  44.         System.out.println(soft.get());  
  45.     }  
  46.       
  47.     public static void weakTest()  
  48.     {  
  49.         WeakReference <Obj> weak = new WeakReference<Obj>(new Obj(“weak”));  
  50.         System.gc();  
  51.         System.out.println(weak.get());  
  52.     }  
  53.       
  54.     public static void phanTest()  
  55.     {  
  56.         ReferenceQueue<Obj> rq = new ReferenceQueue<Obj>();  
  57.         PhantomReference<Obj> phan = new PhantomReference<Obj>(new Obj(“phan”), rq);  
  58.         System.out.println(phan.get());  
  59.     }  
  60. }  

 

分别执行四个函数, 并控制强/软引用中的obj是否为null, 共6次实验, 我们可以总结出:

1. 显式的把(强引用)对象置为null,会大大加大 垃圾回收执行频率。几乎只要我们给出建议,jvm就会回收。 

2. 对于软引用,如果不显式的置为null的话,和强引用差不多,垃圾回收不会执行。和强引用的区别是:只会等到内存不足的时候才会执行。 

3. 对于弱引用,就算你不显式的把他置为null,垃圾回收也会立即执行。

4. 虚引用,相当于null。如果一个对象的虚引用加入了引用队列, 那么这个对象行将就木了


1.Java垃圾回收器概述

  Java2平台里面引入了java.lang.ref包,这个包中的类可以让我们引用对象,而是的这些对象不用停留在内存中。不仅仅如此,这些类和 Java本身的垃圾回收器还存在一定的交互。我们平时开发的时候有时候会用到System.gc()方法,而Java里面GC的基本工作原理又是如何呢?当然使用Java引用类主要目的是为了和Java垃圾回收器进行有限的交互,从而全面提升内存的管理效率,这种情况在嵌入式系统和实时系统以及一些对内存要求严格的系统里面比较凑效。

1)GC的基本原理

  Java的内存管理主要指的是对内存中的对象的管理,包括针对这些对象进行内存的分配和释放等各种操作,学过Java的人都了解Java本身的内存模型,对于一个Java的对象而言,存储主要分为两种,一种是内存堆(Heap),内存堆是无序的,主要用来存放创建的Java对象;一种是内存栈 (Stack),主要用来存放Java引用,然后在管理过程使用Java引用指向Java对象。而GC就是负责在对象“不可达”的时候将对象回收,常见的语句例如:

Object o = null;

  而CG本身是如何工作的呢?当系统在创建对象的时候,即当我们使用new关键字创建一个对象的时候,GC就开始监控对象的地址、大小以及使用状态。一般情况下,Java的GC机制都有特定的回收算法,GC通常会使用有向图的方式来记录队中的所有对象,通过此种方式确定哪些对象是“可达的”,而哪些是“不可达的”。当GC判断一些对象不可达的时候,GC就有责任回收相关内存空间。但是,因为平台的不同,往往在调用System.gc()的时候,存在太多不确定性,可以这样认为,当程序调用了System.gc()过后,仅仅是程序向JVM发送了请求,至于JVM究竟在什么时候进行垃圾回收,不同的平台不一样。(*:需要解决的误区是不要觉得System.gc()调用了过后,垃圾回收器一定会对系统内存进行回收,系统回收相关内存取决于平台和系统。)

2)增量方式GC(IncrementalGC)

  GC 在JVM中通常是启动了一个新的进程或者一组新的进程,它本身和Java用户程序一样需要占用heap空间,运行时也占用CPU。设计GC的时候,必须要在停顿时间和回收率之间进行权衡,原因在于它本身占用了Heap,如果GC运行时间太长,用户就会感觉到Java程序本身会有一定的停顿,如果运行时间太短,则有很多内存没有回收,使得程序里面创建的Java对象占用了大量的内存。增量方式的GC就是通过一定的回收算法,把一个长时间的中断,分成很多小的中断,通过这种方式减少GC对程序本身的影响。其实增量方式整体性能比不上普通的高,但是能够减少停顿时间,改善使用者的用户体验。当然除了这样的方式,GC整体的方式为:

  引用计数法(Reference Counting Collector) ;

  Tracing算法(Tracing Collector);

  Compacting算法(Compacting Collector) ;

  Copying算法(Coping Collector) ;

  Generation算法(Generational Collector) ;

  Adaptive算法(Adaptive Collector) ;

  至于相关的算法可以去查阅相关的文档

2.Java中的对象引用分类

  Java中的对象引用主要有以下几种类型:

  1)强可及对象(strongly reachable):

  可以通过强引用访问的对象,一般来说,我们平时写代码的方式都是使用的强引用对象,比如下边的代码段:

  StringBuilder builder= new StringBuilder();

  上边代码部分引用obj这个引用将引用内存堆中的一个对象,这种情况下,只要obj的引用存在,垃圾回收器就永远不会释放该对象的存储空间。这种对象我们又成为强引用(Strong references),这种强引用方式就是Java语言的原生的Java引用,我们几乎每天编程的时候都用到。上边代码JVM存储了一个StringBuilder类型的对象的强引用在变量builder呢。强引用和GC的交互是这样的,如果一个对象通过强引用可达或者通过强引用链可达的话这种对象就成为强可及对象,这种情况下的对象垃圾回收器不予理睬。如果我们开发过程不需要垃圾回器回收该对象,就直接将该对象赋为前引用。

  2)软可及对象(softly reachable):

  不通过强引用访问的对象,即不是强可及对象,但是可以通过软引用访问的对象就成为软可及对象,软可及对象就需要使用类SoftReference(java.lang.ref.SoftReference)。此种类型的引用主要用于内存比较敏感的高速缓存,而且此种引用还是具有较强的引用功能,当内存不够的时候GC会回收这类内存,因此如果内存充足的时候,这种引用通常不会被回收的。不仅仅如此,这种引用对象在JVM里面保证在抛出OutOfMemory异常之前,设置成为null。通俗地讲,这种类型的引用保证在JVM内存不足的时候全部被清楚,但是有个关键在于:垃圾收集器在运行时是否释放软可及对象是不确定的,而且使用垃圾回收算法并不能保证一次性寻找到所有的软可及对象。当垃圾回收器每次运行的时候都可以随意释放不是强可及对象占用的内存,如果垃圾回收器找到了软可及对象过后,可能会进行以下操作:

【1】将SoftReference对象的referent域设置成为null,从而使该对象不再引用heap对象。

【2】SoftReference引用过的内存堆上的对象一律被生命为finalizable。

【3】当内存堆上的对象finalize()方法被运行而且该对象占用的内存被释放,SoftReference对象就会被添加到它的ReferenceQueue,前提条件是ReferenceQueue本身是存在的。

  既然Java里面存在这样的对象,那么我们在编写代码的时候如何创建这样的对象呢?创建步骤如下:

  先创建一个对象,并使用普通引用方式【强引用】,然后再创建一个SoftReference来引用该对象,最后将普通引用设置为null,通过这样的方式,这个对象就仅仅保留了一个SoftReference引用,同时这种情况我们所创建的对象就是SoftReference对象。一般情况下,我们可以使用该引用来完成Cache功能,就是前边说的用于高速缓存,保证最大限度使用内存而不会引起内存泄漏的情况。下边的代码段:

  public static void main(String args[])

  {

    //创建一个强可及对象

    A a = new A();

    //创建这个对象的软引用SoftReference

    SoftReference sr = new SoftReference(a);

    //将强引用设置为空,以遍垃圾回收器回收强引用

    a = null;

    //下次使用该对象的操作

    if( sr != null ){

      a = (A)sr.get();

    }else{

      //这种情况就是由于内存过低,已经将软引用释放了,因此需要重新装载一次

      a = new A();

      sr = new SoftReference(a);

    }

  }

  软引用技术使得Java系统可以更好地管理内存,保持系统稳定,防止内存泄漏,避免系统崩溃,因此在处理一些内存占用大而且生命周期长使用不频繁的对象可以使用该技术。

  3)弱可及对象(weakly reachable):

  不是强可及对象同样也不是软可及对象,仅仅通过弱引用WeakReference(java.lang.ref.WeakReference)访问的对象,这种对象的用途在于规范化映射(canonicalized mapping),对于生存周期相对比较长而且重新创建的时候开销少的对象,弱引用也比较有用,和软引用对象不同的是,垃圾回收器如果碰到了弱可及对象,将释放WeakReference对象的内存,但是垃圾回收器需要运行很多次才能够找到弱可及对象。弱引用对象在使用的时候,可以配合ReferenceQueue类使用,如果弱引用被回收,JVM就会把这个弱引用加入到相关的引用队列中去。最简单的弱引用方法如以下代码:

  WeakReference weakWidget = newWeakReference(classA);

  在上边代码里面,当我们使用weakWidget.get()来获取classA的时候,由于弱引用本身是无法阻止垃圾回收的,所以我们也许会拿到一个null返回。【*:这里提供一个小技巧,如果我们希望取得某个对象的信息,但是又不影响该对象的垃圾回收过程,我们就可以使用WeakReference来记住该对象,一般我们在开发调试器和优化器的时候使用这个是很好的一个手段。】

  如果上边的代码部分,我们通过weakWidget.get()返回的是null就证明该对象已经被垃圾回收器回收了,而这种情况下弱引用对象就失去了使用价值,GC就会定义为需要进行清除工作。这种情况下弱引用无法引用任何对象,所以在JVM里面就成为了一个死引用,这就是为什么我们有时候需要通过ReferenceQueue类来配合使用的原因,使用了ReferenceQueue后,就使得我们更加容易监视该引用的对象,如果我们通过一ReferenceQueue类来构造一个若引用,当若引用的对象已经被回收的时候,系统将自动使用对象引用队列来代替对象引用,而且我们可以通过ReferenceQueue类的运行来决定是否真正要从垃圾回收器里面将该死引用(Dead Reference)清除。

  弱引用代码段:

  //创建普通引用对象

  MyObject object = new MyObject();

  //创建一个引用队列

  ReferenceQueue rq = new ReferenceQueue();

  //使用引用队列创建MyObject的弱引用

  WeakReference wr = new WeakReference(object,rq);

  这里提供两个实在的场景来描述弱引用的相关用法:

  (1)你想给对象附加一些信息,于是你用一个 Hashtable 把对象和附加信息关联起来。你不停的把对象和附加信息放入 Hashtable 中,但是当对象用完的时候,你不得不把对象再从 Hashtable 中移除,否则它占用的内存变不会释放。万一你忘记了,那么没有从 Hashtable 中移除的对象也可以算作是内存泄漏。理想的状况应该是当对象用完时,Hashtable 中的对象会自动被垃圾收集器回收,不然你就是在做垃圾回收的工作。

  (2)你想实现一个图片缓存,因为加载图片的开销比较大。你将图片对象的引用放入这个缓存,以便以后能够重新使用这个对象。但是你必须决定缓存中的哪些图片不再需要了,从而将引用从缓存中移除。不管你使用什么管理缓存的算法,你实际上都在处理垃圾收集的工作,更简单的办法(除非你有特殊的需求,这也应该是最好的办法)是让垃圾收集器来处理,由它来决定回收哪个对象。

  当Java回收器遇到了弱引用的时候有可能会执行以下操作:

 

【1】将WeakReference对象的referent域设置成为null,从而使该对象不再引用heap对象。

【2】WeakReference引用过的内存堆上的对象一律被生命为finalizable。

【3】当内存堆上的对象finalize()方法被运行而且该对象占用的内存被释放,WeakReference对象就会被添加到它的ReferenceQueue,前提条件是ReferenceQueue本身是存在的。

  4)清除:

  当引用对象的referent域设置为null,并且引用类在内存堆中引用的对象声明为可结束的时候,该对象就可以清除,清除不做过多的讲述

  5)虚可及对象(phantomly reachable):

  不是强可及对象,也不是软可及对象,同样不是弱可及对象,之所以把虚可及对象放到最后来讲,主要也是因为它的特殊性,有时候我们又称之为“幽灵对象”,已经结束的,可以通过虚引用来访问该对象。我们使用类PhantomReference(java.lang.ref.PhantomReference)来访问,这个类只能用于跟踪被引用对象进行的收集,同样的,可以用于执行per-mortern清除操作。PhantomReference必须ReferenceQueue类一起使用。需要使用ReferenceQueue是因为它能够充当通知机制,当垃圾收集器确定了某个对象是虚可及对象的时候,PhantomReference对象就被放在了它的ReferenceQueue上,这就是一个通知,表明PhantomReference引用的对象已经结束,可以收集了,一般情况下我们刚好在对象内存在回收之前采取该行为。这种引用不同于弱引用和软引用,这种方式通过get()获取到的对象总是返回null,仅仅当这些对象在ReferenceQueue队列里面的时候,我们可以知道它所引用的哪些对对象是死引用(Dead Reference)。而这种引用和弱引用的区别在于:

  弱引用(WeakReference)是在对象不可达的时候尽快进入ReferenceQueue队列的,在finalization方法执行和垃圾回收之前是确实会发生的,理论上这类对象是不正确的对象,但是WeakReference对象可以继续保持Dead状态,

  虚引用(PhantomReference)是在对象确实已经从物理内存中移除过后才进入的ReferenceQueue队列,而且get()方法会一直返回null

  当垃圾回收器遇到了虚引用的时候将有可能执行以下操作:

【1】PhantomReference引用过的heap对象声明为finalizable;

【2】虚引用在堆对象释放之前就添加到了它的ReferenceQueue里面,这种情况使得我们可以在堆对象被回收之前采取操作(*:再次提醒,PhantomReference对象必须经过关联的ReferenceQueue来创建,就是说必须和ReferenceQueue类配合操作)

  看似没有用处的虚引用,有什么用途呢?

1.首先,我们可以通过虚引用知道对象究竟什么时候真正从内存里面移除的,而且这也是唯一的途径。

2. 虚引用避过了finalize()方法,因为对于此方法的执行而言,虚引用真正引用到的对象是异常对象,若在该方法内要使用对象只能重建。一般情况垃圾回收器会轮询两次,一次标记为finalization,第二次进行真实的回收,而往往标记工作不能实时进行,或者垃圾回收其会等待一个对象去标记 finalization。这种情况很有可能引起MemoryOut,而使用虚引用这种情况就会完全避免。因为虚引用在引用对象的过程不会去使得这个对象由Dead复活,而且这种对象是可以在回收周期进行回收的。

  在JVM内部,虚引用比起使用finalize()方法更加安全一点而且更加有效。而finaliaze()方法回收在虚拟机里面实现起来相对简单,而且也可以处理大部分工作,所以我们仍然使用这种方式来进行对象回收的扫尾操作,但是有了虚引用过后我们可以选择是否手动操作该对象使得程序更加高效完美。

3.Java里面对象的生命周期

  在JVM运行空间里面,对象整个声明周期大致分为以下几个阶段:

  创建阶段(Creating)->应用阶段(Using)->不可视阶段(Invisible)->不可达阶段(Unreachable)->可收集阶段(Collected)->终结阶段(Finalized)->释放阶段(Free)

  【1】创建阶段:

  创建过程需要经过其中几步:

  为对象分配内存空间

  开始构造对象

  递归调用超类的构造方法

  进行对象实例初始化和变量初始化

  执行构造方法体

  【2】应用阶段特征:

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

  所有该对象的引用全部是强引用,除非我们显示声明了软引用、弱引用或者虚引用

  【3】不可是视阶段:

  不可视阶段就是我们在区域代码中不可以再引用它,就是强引用已经消失,一般情况我们把这个时候的对象设置为null,其主要目的是让JVM发现它,并且可以及时回收该对象所占用资源

  【4】不可到达阶段:

  不可达阶段的对象,在虚拟机所管理的对象引用根集合中再也找不到直接或间接的强引用,这些对象通常是指所有线程栈中的临时变量以及相关引用,这种对象都是要预备回收的对象,但是这时候不能被GC直接回收。

  【5】可收集阶段、终结阶段、释放阶段:

  对象生命周期最后一个阶段,这种阶段的对象可能处于三种状态:

  垃圾回收器发现对象已经不可达

  finalize方法已经被执行

     对象已经被重用

 

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/14334.html

(0)
小半的头像小半

相关推荐

极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!