Java内存储器模型7
Java内存模型7 2)软可及对象(softly reachable): 不通过强引用访问的对象,即不是强可及对象,但是可以通过
Java内存模型7
2)软可及对象(softly reachable): 不通过强引用访问的对象,即不是强可及对象,但是可以通过软引用访问的对象就成为软可及对象,软可及对象就需要使用类SoftReference(java.lang.ref.SoftReference)。此种类型的引用主要用于内存比较敏感的高速缓存,而且此种引用还是具有较强的引用功能,当内存不够的时候GC会回收这类内存,因此如果内存充足的时候,这种引用通常不会被回收的。不仅仅如此,这种引用对象在JVM里面保证在抛出OutOfMemory异常之前,设置成为null。通俗地讲,这种类型的引用保证在JVM内存不足的时候全部被清除,但是有个关键在于:垃圾收集器在运行时是否释放软可及对象是不确定的,而且使用垃圾回收算法并不能保证一次性寻找到所有的软可及对象。当垃圾回收器每次运行的时候都可以随意释放不是强可及对象占用的内存,如果垃圾回收器找到了软可及对象过后,可能会进行以下操作:
- 将SoftReference对象的referent域设置成为null,从而使该对象不再引用heap对象。
- SoftReference引用过的内存堆上的对象一律被生命为finalizable。
- 当内存堆上的对象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 =?new?WeakReference(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回收器遇到了弱引用的时候有可能会执行以下操作:
- 将WeakReference对象的referent域设置成为null,从而使该对象不再引用heap对象。
- WeakReference引用过的内存堆上的对象一律被生命为finalizable。
- 当内存堆上的对象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 当垃圾回收器遇到了虚引用的时候将有可能执行以下操作:
- PhantomReference引用过的heap对象声明为finalizable;
- 虚引用在堆对象释放之前就添加到了它的ReferenceQueue里面,这种情况使得我们可以在堆对象被回收之前采取操作【*:再次提醒,PhantomReference对象必须经过关联的ReferenceQueue来创建,就是说必须和ReferenceQueue类配合操作】 看似没有用处的虚引用,有什么用途呢?
- 首先,我们可以通过虚引用知道对象究竟什么时候真正从内存里面移除的,而且这也是唯一的途径。
- 虚引用避过了finalize()方法,因为对于此方法的执行而言,虚引用真正引用到的对象是异常对象,若在该方法内要使用对象只能重建。一般情况垃圾回收器会轮询两次,一次标记为finalization,第二次进行真实的回收,而往往标记工作不能实时进行,或者垃圾回收其会等待一个对象去标记finalization。这种情况很有可能引起MemoryOut,而使用虚引用这种情况就会完全避免。因为虚引用在引用对象的过程不会去使得这个对象由Dead复活,而且这种对象是可以在回收周期进行回收的。 在JVM内部,虚引用比起使用finalize()方法更加安全一点而且更加有效。而finaliaze()方法回收在虚拟机里面实现起来相对简单,而且也可以处理大部分工作,所以我们仍然使用这种方式来进行对象回收的扫尾操作,但是有了虚引用过后我们可以选择是否手动操作该对象使得程序更加高效完美。 iv.防止内存泄漏[来自IBM开发中心]: 1)使用软引用阻止泄漏: [1]在Java语言中有一种形式的内存泄漏称为对象游离(Object Loitering): ——[$]对象游离——// 注意,这段代码属于概念说明代码,实际应用中不要模仿public class?LeakyChecksum{?? ?private byte[] byteArray;
????public synchronized int?getFileCheckSum(String?filename)
?? ?{
?? ??? ?int?len = getFileSize(filename);
?? ??? ?if( byteArray ==?null?|| byteArray.length < len )
?? ??? ??? ?byteArray =?new?byte[len];
?? ??? ?readFileContents(filename,byteArray);
?? ??? ?// 计算该文件的值然后返回该对象
?? ?}
} 上边的代码是类LeakyChecksum用来说明对象游离的概念,里面有一个getFileChecksum()方法用来计算文件内容校验和,getFileCheckSum方法将文件内容读取到缓冲区中计算校验和,更加直观的实现就是简单地将缓冲区作为getFileChecksum中的本地变量分配,但是上边这个版本比这种版本更加“聪明”,不是将缓冲区缓冲在实例中字段中减少内存churn。该“优化”通常不带来预期的好处,对象分配比很多人期望的更加便宜。(还要注意,将缓冲区从本地变量提升到实例变量,使得类若不带有附加的同步,就不再是线程安全的了。直观的实现不需要将 getFileChecksum() 声明为 synchronized,并且会在同时调用时提供更好的可伸缩性。) 这个类存在很多的问题,但是我们着重来看内存泄漏。缓存缓冲区的决定很可能是根据这样的假设得出的,即该类将在一个程序中被调用许多次,因此它应该更加有效,以重用缓冲区而不是重新分配它。但是结果是,缓冲区永远不会被释放,因为它对程序来说总是可及的(除非LeakyChecksum对象被垃圾收集了)。更坏的是,它可以增长,却不可以缩小,所以 LeakyChecksum 将永久保持一个与所处理的最大文件一样大小的缓冲区。退一万步说,这也会给垃圾收集器带来压力,并且要求更频繁的收集;为计算未来的校验和而保持一个大型缓冲区并不是可用内存的最有效利用。LeakyChecksum 中问题的原因是,缓冲区对于 getFileChecksum() 操作来说逻辑上是本地的,但是它的生命周期已经被人为延长了,因为将它提升到了实例字段。因此,该类必须自己管理缓冲区的生命周期,而不是让 JVM 来管理。 这里可以提供一种策略就是使用Java里面的软引用: 弱引用如何可以给应用程序提供当对象被程序使用时另一种到达该对象的方法,但是不会延长对象的生命周期。Reference?的另一个子类——软引用——可满足一个不同却相关的目的。其中弱引用允许应用程序创建不妨碍垃圾收集的引用,软引用允许应用程序通过将一些对象指定为 “expendable” 而利用垃圾收集器的帮助。尽管垃圾收集器在找出哪些内存在由应用程序使用哪些没在使用方面做得很好,但是确定可用内存的最适当使用还是取决于应用程序。如果应用程序做出了不好的决定,使得对象被保持,那么性能会受到影响,因为垃圾收集器必须更加辛勤地工作,以防止应用程序消耗掉所有内存。高速缓存是一种常见的性能优化,允许应用程序重用以前的计算结果,而不是重新进行计算。高速缓存是 CPU 利用和内存使用之间的一种折衷,这种折衷理想的平衡状态取决于有多少内存可用。若高速缓存太少,则所要求的性能优势无法达到;若太多,则性能会受到影响,因为太多的内存被用于高速缓存上,导致其他用途没有足够的可用内存。因为垃圾收集器比应用程序更适合决定内存需求,所以应该利用垃圾收集器在做这些决定方面的帮助,这就是件引用所要做的。如果一个对象惟一剩下的引用是弱引用或软引用,那么该对象是软可及的(softly reachable)。垃圾收集器并不像其收集弱可及的对象一样尽量地收集软可及的对象,相反,它只在真正?“需要”?内存时才收集软可及的对象。软引用对于垃圾收集器来说是这样一种方式,即?“只要内存不太紧张,我就会保留该对象。但是如果内存变得真正紧张了,我就会去收集并处理这个对象。”?垃圾收集器在可以抛出OutOfMemoryError?之前需要清除所有的软引用。通过使用一个软引用来管理高速缓存的缓冲区,可以解决?LeakyChecksum中的问题,如上边代码所示。现在,只要不是特别需要内存,缓冲区就会被保留,但是在需要时,也可被垃圾收集器回收: ——[$]使用软引用修复上边代码段——public class?CachingChecksum{?? ?private?SoftReference<byte[]> bufferRef;
?? ?public synchronized int?getFileChecksum(String?filename)
?? ?{
?? ??? ?int?len = getFileSize(filename);
?? ??? ?byte[] byteArray = bufferRef.get();
?? ??? ?if( byteArray ==?null?|| byteArray.length < len )
?? ??? ?{
?? ??? ??? ?byteArray =?new byte[len];
?? ??? ??? ?bufferRef.set(byteArray);?? ??? ?}
?? ??? ?readFileContents(filename,byteArray);
?? ?}
} 一种廉价缓存: CachingChecksum使用一个软引用来缓存单个对象,并让 JVM 处理从缓存中取走对象时的细节。类似地,软引用也经常用于 GUI 应用程序中,用于缓存位图图形。是否可使用软引用的关键在于,应用程序是否可从大量缓存的数据恢复。如果需要缓存不止一个对象,您可以使用一个?Map,但是可以选择如何使用软引用。您可以将缓存作为?Map<K, SoftReference<V>>?或SoftReference<Map<K,V>>?管理。后一种选项通常更好一些,因为它给垃圾收集器带来的工作更少,并且允许在特别需要内存时以较少的工作回收整个缓存。弱引用有时会错误地用于取代软引用,用于构建缓存,但是这会导致差的缓存性能。在实践中,弱引用将在对象变得弱可及之后被很快地清除掉——通常是在缓存的对象再次用到之前——因为小的垃圾收集运行得很频繁。对于在性能上非常依赖高速缓存的应用程序来说,软引用是一个不管用的手段,它确实不能取代能够提供灵活终止期、复制和事务型高速缓存的复杂的高速缓存框架。但是作为一种 “廉价(cheap and dirty)” 的高速缓存机制,它对于降低价格是很有吸引力的。正如弱引用一样,软引用也可创建为具有一个相关的引用队列,引用在被垃圾收集器清除时进入队列。引用队列对于软引用来说,没有对弱引用那么有用,但是它们可以用于发出管理警报,说明应用程序开始缺少内存。 2)垃圾回收对引用的处理: 弱引用和软引用都扩展了抽象的?Reference?类虚引用(phantom references),引用对象被垃圾收集器特殊地看待。垃圾收集器在跟踪堆期间遇到一个?Reference?时,不会标记或跟踪该引用对象,而是在已知活跃的?Reference?对象的队列上放置一个?Reference。在跟踪之后,垃圾收集器就识别软可及的对象——这些对象上除了软引用外,没有任何强引用。垃圾收集器然后根据当前收集所回收的内存总量和其他策略考虑因素,判断软引用此时是否需要被清除。将被清除的软引用如果具有相应的引用队列,就会进入队列。其余的软可及对象(没有清除的对象)然后被看作一个根集(root set),堆跟踪继续使用这些新的根,以便通过活跃的软引用而可及的对象能够被标记。处理软引用之后,弱可及对象的集合被识别 —— 这样的对象上不存在强引用或软引用。这些对象被清除和加入队列。所有?Reference?类型在加入队列之前被清除,所以处理事后检查(post-mortem)清除的线程永远不会具有 referent 对象的访问权,而只具有Reference?对象的访问权。因此,当?References?与引用队列一起使用时,通常需要细分适当的引用类型,并将它直接用于您的设计中(与?WeakHashMap?一样,它的?Map.Entry?扩展了?WeakReference)或者存储对需要清除的实体的引用。 3)使用弱引用堵住内存泄漏: