深刻Java虚拟机(五)垃圾收集

Java 虚拟机的堆里存放着程序运行中所建立的全部对象。虚拟机可使用newnewarrayanewarraymultianewarray指令来建立对象,可是没有明确的代码来释放它们。垃圾收集就是自动释放再也不被程序所使用的对象的过程。java

本篇文章并非要描述正式的 Java 垃圾收集器,由于根本不存在这样一个正式的描述。前面说过,Java 虚拟机规范不要求任何特定的垃圾收集技术,这根本不是必需的。可是在发明能够无限使用的内存前,大部分的 Java 虚拟机都会附带垃圾收集功能程序员

为何要使用垃圾收集

垃圾收集这个名字暗示着程序再也不须要的对象就是垃圾,能够被丢弃。更准确地说,应该被叫作内存回收。当一个对象再也不被程序所引用时,它所使用的堆空间能够被回收,一遍后续的新对象所使用。算法

垃圾收集器必须可以确认哪些对象是再也不被引用的,而且可以把它们所占据的堆空间释放出来。在释放对象的过程当中,垃圾收集器还要运行将要被释放对象的终结方法(finalizer)。数据库

除了释放再也不被引用的对象,垃圾收集器还要处理堆碎块。堆碎块是在程序运行过程当中产生的。在请求分配新对象的内存空间时,可能不得不增大堆空间的大小,虽然可使用的总空闲空间是足够的。这是由于堆中的空闲空间并不连续,没法放下一个新的对象。编程

把内存回收的任务交给虚拟机有几个好处:缓存

  • 提升生产效率。在一个不具有垃圾收集机制的语言下编程时,你可能须要花费好多时间来查找难以琢磨的内存问题(C/C++用得少,没机会体验。。。。。)
  • 保持程序的完整性。垃圾回收技术是Java安全策略的一个重要部分,Java程序员不可能由于错误释放内存而致使Java虚拟机的崩溃

可是使用垃圾收集,有一个潜在的缺陷就是加大了程序的负担,可能影响程序性能。Java虚拟机必须追踪哪些对象正在使用,哪些对象须要释放。和明确释放内存比起来,内存释放过程还会须要更多的CPU时间片。安全

垃圾收集算法

任何垃圾收集算法必须作两件事:网络

  • 检测垃圾对象
  • 回收垃圾对象所使用的堆空间并还给程序

根对象(Root)

垃圾检测一般经过创建一个根对象的集合而且检查从这些跟对象开始的可触及性来实现。若是正在执行的程序能够访问到跟对象和某个对象之间存在引用路径,这个对象就是可触及的。对于程序来讲,跟对象老是能够访问的。从这些根对象开始,任何能够被触及的对象都被认为是活动的对象。没法被触及的对象被认为是垃圾,它们再也不被程序使用到。数据结构

任何根对象引用的对象都是可触及的,从而是活动的。另外,任何被活动的对象引用对象都是可触及的。程序能够访问任何可触及的对象,因此这些对象必须保存在堆里面。任何不可触及的对象均可以被收集,由于程序被办法访问它们。jvm

来源

Java 虚拟机的根对象集合根据实现方式各有不一样,可是总会包含局部变量中的对象引用和栈帧的操做数栈(以及类变量中的对象引用)。关于根对象的来源大概有这几种:

  • 一个来源是用类的常量池中的对象引用,好比字符串。被加载的类的常量池可能指向保存在堆中的字符串,好比类名字、超类的名字、字段名、字段特征签名、方法名或者方法特征签名。
  • 还有一个来源是传递到本地方法中的,没有被本地方法释放的对象引用(根据本地方法接口,本地方法能够经过简单地返回来释放引用;或者显示地调用一个回调函数来释放传递来的引用,或者是这二者的结合)
  • 再一个潜在的对象来源是Java虚拟机的运行时数据区中从具备垃圾收集功能的堆中分配的部分。举例来讲,在某些实现中,方法区中的类数据自己可能被存放在使用垃圾收集器的堆中,以便使用和对象一样的垃圾回收算法检测和卸载再也不使用的类。

说实话这三条咋这么抽象嘞,先往下看看把

基本类型的干扰

在 Java 虚拟机的实现中,有些垃圾收集器能够区分真正的对象引用和看上去很像合法对象引用的基本类型(好比一个 int 变量)之间的差异。(例如一个 int 整数,若是被解释是一个本地指针,可能指向堆中的一个对象)但是某些垃圾收集器仍然选择不区分真正的对象引用和假装品,这种垃圾收集器被称为保守的,由于它们可能作不到释放掉每个再也不引用的对象。对于保守的收集器,有时候垃圾对象也被错误的判断为活动的,由于有一个看上去像是对象引用的基本类型"引用"了对象。这种保守的垃圾收集器是垃圾回收速度提升了,由于有一些垃圾被遗忘了。

基础算法分类

区分活动对象和垃圾对象的两个基本方法是引用计数和跟踪。

  • 引用计数垃圾收集器经过堆中的每个对象保存一个计数来区分活动对象和垃圾对象。这个计数记录下了对象被引用的次数。
  • 跟踪垃圾收集器是追踪从根节点开始的引用图。在追踪过程当中赶上的对象以某种方式打上标记,当追踪结束,没有被打上标记的对象就被断定为不可触及的,能够被回收。

引用计数收集器

引用计数是垃圾回收的早期策略。在这种方法中,堆中每个对象都有一个引用计数。

规则

规则包括:

  • 当一个对象被建立,而且将指向该对象的引用分配给一个变量,这个对象的引用计数被置为1。
  • 当任何其余变量被赋值为这个对象的引用时,计数加1。
  • 当一个对象的引用超过了生存期或者被置为一个新的值时,对象的引用计数减1。
  • 任何计数为0的对象均可以被当作垃圾回收。
  • 当一个对象被回收时,它所引用的对象的计数也要减1。

在这种方法中,一个对象被垃圾收集后可能会触发后续其余对象的垃圾收集行动。

弊端

引用计数没法检测出循环引用(即两个或者更多对象之间的相互引用)

循环引用的例子如

class A{
  public B b;
}
class B{
  public A a;
}
public class Main{
    public static void main(String[] args){
        A a = new A();
        B b = new B();
        a.b=b;
        b.a=a;
        a=null;
        b=null;
    }
}
复制代码

a 和 b 虽然置为了 null,可是按照引用计数的规则永远不会被收集,由于 a 和 b 分别持有各自的引用。

跟踪收集器

跟踪收集器是追踪从根节点开始的对象引用图。在追踪过程当中遇到的对象以某种方式打上标记。标记时,要么在对象自己设置标记,要么用一个独立的位图来设置标记。当追踪结束时,未被标记的对象就知道是没法触及的,从而能够被收集。

基本的追踪算法被称做标记-清除算法。这个名字指出了垃圾收集过程的两个阶段:

  • 标记阶段:垃圾收集遍历引用树,标记每个遇到的对象。
  • 清除阶段:释放未被标记对象所占用的内存。这个阶段就要触发对象的终结方法。

压缩收集器

Java虚拟机的垃圾收集器可能有对付堆碎块的策略。标记-清除收集器一般使用的两种策略是压缩和拷贝。这两种方式都是经过快速地移动对象来减小堆碎块。

压缩收集器把活动的对象越过空闲区移动到堆的另外一端,这样堆的另外一端会出现一个大的连续空间。此后,全部被移动的对象的引用也会被更新,指向新的内存地址。

更新被移动对象的引用有时候会经过一个间接对象引用层,不直接引用堆中的对象,对象的引用实际上指向一个对象句柄表。对象句柄才是真正指向堆中对象的实际位置。当对象被移动了,只须要更新句柄表就能够。不过在对象的访问上,由于增长了一个句柄表,性能有所损失。

拷贝收集器

拷贝垃圾收集器把全部的活动对象都移动到一个新的区域。在拷贝的过程当中,它们被紧挨着布置,因此能够消除本来它们在旧区域的空隙。而原有的区域被认为是空闲区。

这种方法的好处就是从根对象开始遍历的过程当中,一旦发现对象就进行拷贝,再也不有标记和清除的区分。对象被快速拷贝到新区域,同时转向指针仍然留在原来的位置。转向指针可让垃圾收集器发现已经被转移对象的引用。而后垃圾收集器把和这个对象有关的引用设置为转向指针的值。

通常的拷贝收集器算法被称为中止-拷贝。方案以下:

  • 堆被分为两个区域,任什么时候候都只是用其中的一个区域。
  • 对象在同一个区域分配,直到这个区域被耗尽。
  • 此时,程序执行被终止,堆开始遍历,遍历时遇到的对象被拷贝到另外的一个区域。
  • 中止-拷贝过程结束后,程序恢复执行。
  • 内存将重新的堆区域开始分配,直到它也被用尽。这时程序再次停止,重复上面的步骤。

这种方法的代价就是,对于指定大小的堆来讲,实际上须要两倍的内存来运行。

图形描述以下:

image
上图一共是9张堆内存快照:

  • 快照1中,堆的下半部分没有被使用,上半部分零散的被对象填充(包含对象的部分用橘黄色表示)
  • 快照2中,随着程序的运行,上半部分逐渐被对象填充。
  • 快照3中,对象已经把上半部分填满了。
  • 快照4中,由于快照3已经把上半部分填满了,此时,垃圾收集器中止程序执行,从根节点开始追踪活动对象图。当遇到活动的对象时就拷贝到堆的下半部分,而且每个对象都紧挨着。
  • 快照5中,垃圾收集刚刚结束,程序已经恢复运行。上半部分被清理完成,而且做为未被使用部分。以前存活的对象移动到了下半部分。
  • 快照6中,随着程序的运行,下半部分逐渐被对象填充。
  • 快照7中,对象已经把下半部分填满了。
  • 快照8中,垃圾收集器再次停止了程序,追踪活动对象。此次它把遇到的活动对象都拷贝到堆的上半部分。
  • 快照9中,垃圾收集完成,下半部分垃圾对象被清理,变为了未使用区。以前存活的对象移动到了上半部分。

在程序执行中,这个上述过程一次有一次地重复。

按代收集的收集器

简单的中止-拷贝收集器的缺点是,每一次收集时,全部的活动对象都必须被拷贝。大部分语言的大多数程序都有一下特色,若是咱们全面考虑这些,拷贝算法是能够改进的。

  • 大部分程序建立的大部分对象都具备很短的生命期。
  • 大多数程序都会建立一些具备很是长生命周期的对象。

简单的中止-拷贝收集器浪费效率的一个主要缘由就是,它们每次把这些生命周期很长的对象来回拷贝,消耗大量的时间

按代收集的收集器经过把对象按照寿命来分组解决中止-拷贝效率低下的问题,更多地收集那些短暂出现的年幼对象,而非寿命比较长的对象。逻辑以下:

  • 堆被划分红两个或者更多的子堆
  • 每个堆为一代的对象服务
  • 最年幼的那一代进行最频繁的垃圾收集。由于大多数对象都是短暂出现的,只有不多一部分年幼对象在经历第一次收集后还存活
  • 若是一个最年幼的对象在经历了好几回垃圾收集后仍然存活,那么这个对象就成长为寿命更到的一代:转移到另一个子堆中
  • 年龄更高的每一代的收集都没有年轻的那一代来的频繁
  • 每当对象在它所属的年龄层中变得成熟(逃过了屡次垃圾收集)以后,它们就会被转移到更高的年龄层中

按代收集技术除了能够应用于中止-拷贝垃圾收集算法,也能够用于标记-清除垃圾回收算法。无论哪一种状况下,把堆按照年龄层分解均可以提升最基本的垃圾收集算法的性能。

自适应收集器

自适应收集器算法李永乐以下事实:在某种状况下某些垃圾收集算法工做的更好,而另一些收集算法在另外的状况下工做得更好

自适应算法监视堆中的情形,并对应的调整为合适的垃圾收集技术。

使用自适应方法,Java虚拟机的实现者不须要只选择一种特定的算法。可使用多种技术,以便在最擅长的场合使用它们。

火车算法(train GC

火车算法最先是有Richard HudsonEliot Moss提出的,目的是为了在成熟对象空间提供限定时间的渐进收集,最先用于Sun公司的Hotspot虚拟机。该算法详细的说明了按代收集的垃圾收集器的成熟对象空间的组织。

唏嘘的是到Sun JDK 6的时候就已经完全不包含train GC了,不过更重要的是思想,仍是看一看吧

以往收集算法存在的问题

垃圾收集算法和主动释放对象内存比起来有一个潜在的缺点,即垃圾收集算法中程序员对安排 CPU 时间进行内存回收的过程缺少控制。

要精确的预测出什么时候进行垃圾收集、收集须要多长时间基本上是不可能。由于垃圾收集通常会停止整个程序来查找和收集垃圾对象,它们可能在程序执行的任意时刻触发垃圾收集,而且停止的时间也没法肯定。这种垃圾收集的暂停有时候长得让用户注意到了。

而当一种垃圾收集算法可能致使用户可察觉到的停顿或者使得程序没法知足实时系统的要求,这种算法被称做破坏性的。

渐进式收集算法

达到非破坏性垃圾收集的方法是使用渐进式垃圾收集算法。

渐进式垃圾收集器就是不试图一次性发现并回收全部的垃圾对象,而是每次发现并回收一部分。所以每次都只有堆的一部分执行垃圾收集,所以理论上说的每一次收集会持续更短的时间。

若是可以保证每次收集不超过一个最大时间长度,就可让Java虚拟机适合实时环境,而且也能够消除用户可察觉的停顿。

一般渐进式收集器都是按代收集的收集器。

车箱、火车和火车站

火车算法把成熟的对象空间划分为固定长度的内存块,算法每次在一个块中单独执行。规则以下:

  • 每一个内存块属于某一个集合,并在集合中有序排列。
  • 集合与集合之间有序排列。

在原始论文中,内存块叫作车箱;集合叫作火车。成熟对象的内存空间叫作火车站

算法组织图以下:

image

命名计划

火车按照它们建立时的顺序分配号码。
所以,假设咱们将第一列火车(最早进入该年龄层的对象内存)被拉进轨道1,称为火车1。到达的第二辆火车被拉到轨道二,称为火车2。下一列到达的火车被拉到轨道3。
依次类推,按照这样的计划,号码较小的火车老是更早出现的火车。

在火车内部,车箱(内存块)老是附加到火车的尾部。
附加的第一节车箱被称为车箱1,这列车附加的下一节车箱被称为车箱2。 所以,在列车内部,较小的数字总能表示更早出现的车箱。

这个命名计划给出了成熟对象空间中内存块的整体顺序。

上图中显示了三列车,标记为火车1火车2火车3

  • 火车1拥有四节车箱,标记为 1.1-1.4
  • 火车2拥有三节车箱,标记为 2.1-2.3
  • 火车3拥有五节车箱,标记为 3.1-3.5

而对于加入的顺序为:

  • 车箱1.1在车箱1.2的前面,车箱1.2在车箱1.4的前面,以此类推。
  • 火车1的最后一节车箱老是在火车2的第一节车箱前面,因此车箱1.4在车箱2.1以前。同理,车箱2.1在车箱3.1以前。

火车算法每一次执行的时候,只会对一个块(号码最低的块)执行垃圾收集。对于上图,它会收集车箱1.1,下次执行时会收集车箱1.2。当它收集了火车1的最后一个车箱,算法在下一次执行时收集火车2的车箱2.1。(从这部分看,在收集完一个车箱后,算法应该是要把收集过的车箱移走)。

对象从更年轻的年龄层的子堆进入成熟对象空间,无论什么时候进入,它们都会被附加到任何已经存在的火车中(最小数字的火车除外),或者专为容纳它们而创建的一列或多列火车中。也就是说,对象有两种方法到达火车站:

  • 打包成车箱,挂接在==最小数字的火车==以外的火车尾部
  • 做为一列新的火车开进火车站

==最小数字的火车除外==是为何呢?
由于算法始终检测的是最小数字的火车,或者最小数字的火车最小数字的车箱。这列火车不会直接存放刚刚进入火车站的对象。看下面的车箱收集就明白啦!

车箱收集

每一次算法被执行的时候,它要么收集最小数字火车中的最小数字车箱,要么收集整列最小数字火车。思路以下:

  • 首先检查指向最小数字火车中任何车箱的引用,若是不存在任何来自最小数字火车外的引用指向最小数字火车内部的对象,那么整列火车包含的都是垃圾对象,能够抛弃。
  • 若是最小数字火车并不都是垃圾,那么算法把它的注意力放到火车的最小数字车箱上。在这个过程当中,算法将检测到的被引用的对象转移到其余车箱,而后任何保留着车箱里的对象都是可回收的。

咱们知道有一种循环引用的问题,而对于火车算法来讲,保证整列火车中没有循环的数据结构的关键是算法如何移动对象,包括下面几个规则:

  • 若是正在被收集的==车箱==(这个车箱其实就是最小数字火车最小数字车箱啦)中有一个对象存在来自火车站外的引用,这个对象就被转移到正在被收集的火车以外的其余车箱中去。
  • 若是对象被火车站中的其余火车引用,对象就被转移到引用它的火车中去。
    • 而后扫描被转移过去的对象,把它在原车箱所引用的的对象都转移到引用它们的车箱,这个过程不断重复,直到没有任何来自其余火车的引用指向正在被收集的那节车箱。
    • 若是接受对象的火车车箱没有空间了,那么算法会建立新的车箱,并附加到火车的尾部。
  • 一旦没有火车站外的引用,也没有火车站内其余火车的引用,那么这节正在被收集的车箱剩余的外部引用都是来自同一列火车的其余车箱。
    • 算法把这样的对象移动到最小数字火车的最后一个车箱去。
    • 而后扫描新移动过去的对象,查找是否有引用指向被收集车箱中的对象。
      • 任何新发现的对象也转移到最小数字火车的最后一个车箱中
      • 而后继续扫描新对象,整个过程不断重复
      • 直到没有任何形式的引用指向被收集的车箱
    • 而后算法归还整个最小数字车箱占据的空间,释放全部仍然留在车箱中的对象,而且返回

所以,在每次执行时,火车算法或者收集最小数字火车的最小数字车箱,或者手机整列最小数字火车。而将对应移动到引用它们的火车中,相关的对象会变得集中。最后,称为垃圾的循环数据结构中的全部对象,无论有多大,会放置到同一列火车中去。而增大循环数据结构只会增大最终组成同一列火车的车箱数。前面已经说明,火车算法会先检查最小数字火车是否彻底就是垃圾,而对于循环数据结构这种内部引用,它彻底能够完成收集

记忆集合

火车算法的目标是为了给按代收集的垃圾收集器提供限定时间内的渐进式收集。

对于车箱来讲,分配时能够指定一个最大的内存size,而且每次执行只收集一个车箱,因此大部分状况下,火车算法能够保证每次的执行时间在某个最长时间限度内,不过不能确保每一次都是,由于算法执行的过程当中不只仅是拷贝对象。

为了优化收集过程,火车算法使用了记忆集合。一个记忆集合是一个数据结构,它包含了对一节车箱或者一列火车的外部引用。算法为火车站(成熟对象空间)内的每节车箱和每列火车都维护了一个记忆集合。因此一节特定车箱的记忆集合记录了指向车箱内对象的全部引用。一个空的记忆集合显示车箱或者火车中的对象已经再也不被车箱或者火车外的任何变量引用(被遗忘了)。被遗忘的就是不可触及的,能够被回收。

记忆集合的优点

记忆集合是一种能够帮助火车算法更有效地完成工做的技术。当回车算法发现一节车箱或者一列火车的记忆集合是空的时,它就知道车箱里面全是垃圾,能够释放回收这部分占用的内存。
而且在移动一个对象到另外一节车箱是,记忆集合中的信息有助于它高效的更新全部指向被移动对象的引用。

限制

咱们能够经过限制一个车箱的大小来控制每次字节拷贝的上限,可是当移动一个很受欢迎的对象(有不少外部链接)时,所须要的工做几乎是不可能限制的,每次算法移动一个对象时,它必须遍历对象的记忆集合,更新每个链接,以便于使链接指向新的地址。由于指向一个对象的链接数是没法限定的,因此更新一个被移动对象的全部链接所须要的的时间也没法限定。

也就是说,在特定条件下,火车算法仍然多是破坏性的。不过除了这种受欢迎的清下不太实用外,火车算法大部分状况工做的很好。

再次强调下到Sun JDK 6的时候就已经完全不包含train GC了,不事后续的GC策略能和这个差异有多大呢?对吧

终结

Java语言里,一个对象能够拥有终结方法:这个方法是垃圾收集器在释放对象前必需要运行的。而这个可能存在的终结方法使得任何Java虚拟机的垃圾收集器要完成的工做更加复杂。

终结方法

给一个类加上终结方法,只须要这样:

public class FinalizerTest {
    @Override
    protected void finalize() throws Throwable {
        //do something 
        super.finalize();
    }
}
复制代码

垃圾收集器必须检查它所发现的再也不被引用的对象是否存在finalize()方法。

由于,存在终结方法时,Java虚拟机的垃圾收集器必须每次在收集时执行一些额外的步骤:

  • 首先,垃圾收集器必须使用某种方法检测出再也不被引用的对象(称为第一遍扫描)。
  • 而后,它必须检查它检测出的再也不被应用的对象是否声明了终结方法。(若是时间容许的话,可能在这个时候垃圾收集器就着手处理这些存在的终结方法)。
  • 当执行了全部的终结方法后,垃圾收集器必须从根节点开始再次检测再也不被引用的对象(称为第二遍扫描)。这个步骤是必要的,由于终结方法可能复活了某些不在引用的对象,使它们再次被引用了。
  • 最后垃圾收集器才能释放那些在第一次和第二次扫描中发现没有被引用的对象。

为了减小释放内存的时间,在扫描到某些对象拥有终结方法和运行终结方法之间,垃圾收集器能够有选择地插入一个步骤:

  • 一旦垃圾收集器执行了第一遍扫描,而且找到了一些再也不被引用的对象须要执行终结方法时,它能够运行一次小型的追踪。从须要执行终结方法的对象开始(而非根节点),执行逻辑以下:
    • 任何知足从根节点开始不可触及&&从将要被终结的对象开始不可触及这些对象不可能在执行终结方法时复活,它们能够被当即释放。
    • 请注意上条标注的两个条件,是的关系

若是一个带有终结方法的对象再也不被引用,而且它的总结方法已经执行过了,垃圾收集器必须使用某种方法记住这一点,而不能再次执行这个对象的终结方法。

若是这个对应已经被本身的终结方法或者其余对象的终结方法复活了,稍后再次再也不被引用,垃圾收集器必须像对待一个没有终结方法的对象同样对待它(也就是finalize()只会执行一次的缘由)。

使用Java编程时请记住,是垃圾收集器运行对象的终结方法。由于没法预测垃圾收集什么时候触发,因此咱们也没法预测对象的终结方法什么时候执行。

对象可触及性的生命周期

在版本1.2以前,在垃圾收集器看来,堆中的每个对象都有三种状态:

  • 可触及的:垃圾收集器经过根节点能够追踪到的对象
  • 可复活的:一旦程序释放了全部该对象的引用(从根节点追踪图中不可触及),这个对象就变成了可复活状态。关于可复活,请注意:
    • 不只仅是声明了finalize()方法的对象,而是全部的对象都会通过可复活状态。
    • 因为能够自定义对象的finalize()方法(再次引用一个对象),任何处于可复活状态的对象均可能再次复活
    • 垃圾收集器会在保证全部可复活对象执行过finalize()(若是声明了的话)后,再把可复活对象的状态或者转化为可触及,或者转化为不可触及。
  • 不可触及的:对象再也不被触及,而且对象不可能经过任何终结方法复活。不可触及对象再也不对程序执行产生影响,能够自由回收。

在版本1.2中,对可触及状态延伸扩充了三个新状态:软可触及弱可触及影子可触及。而原来的可触及状态变成了强可触及。(其实就是咱们编程用到的弱引用、强引用啥的吧)

任何从根节点开始的任何直接引用,好比一个局部变量,是强可触及。同理,任何由强可触及对象所引用到的对象也是强可触及

引用对象(Reference

Java提供了java.lang.rf.Reference类用来管理对象链接,包含SoftReferenceWeakReferencePhantomReference三个实现类,继承图以下:

image

  • SoftReference:封装对引用目标的软引用
  • WeakReference:封装对引用目标的弱引用
  • PhantomReference:封装对引用目标的影子引用

强引用和上述三种引用的区别是,强引用禁止引用目标被垃圾收集,而软引用、弱引用、影子引用不由止。

当须要建立一个Reference的对象时,简单的把强引用传递到对应的Reference实现类的构造方法中去就能够。以SoftReference为例:

public class ReferenceTest{
    public static void main(String[] args) {
        Cow c = new Cow();
        SoftReference<Cow> softReference = new SoftReference<Cow>(c);
        c = null;
    }
}
class Cow{}
复制代码

咱们经过维护softReference来维护关于Cow实例对象的软引用。引用示意图以下:

image

SoftReference对象封装了一个Cow对象的软引用。SoftReference对象被一个局部变量softReference强引用,==和全部的局部变量同样,对于与垃圾收集器来讲这是一个根节点==(这部分存疑哈)。

一旦一个引用对象建立后,它将一直维持到它的引用目标的软引用,直到它被程序或者垃圾收集器清除。要清楚一个引用对象,程序或者垃圾收集器只需调用Referece对象的clear()方法。

可触及状态的变化

前面讲到,引用对象的目的是为了可以指向某些对象,使这些对象能够随时被垃圾收集器收集。换个说法就是,垃圾收集器能够随意改变不是强可触及对象的可触及状态。

若是想监听这种状态的变化,咱们可使用java.lang.rf.ReferenceQueue<T>类。怎么用呢,咱们看下Reference的构造方法:

public abstract class Reference<T> {
    Reference(T referent) {
        this(referent, null);
    }
    Reference(T referent, ReferenceQueue<? super T> queue) {
        this.referent = referent;
        this.queue = (queue == null) ? ReferenceQueue.NULL : queue;
    }
}
复制代码

而对于Reference的总体结构,以下图:

image

==说实话,Reference的体系以前没怎接触,只是在Android中简单使用WeakReferenceget方法,等结束本篇垃圾收集,单独撩拨一下==

那咱们就能够这样写:

class ReferenceTest {
    public static void main(String[] args) throws InterruptedException {
        ReferenceQueue<Cow> referenceQueue = new ReferenceQueue<>();
        Cow c = new Cow();
        WeakReference<Cow> softReference = new WeakReference<Cow>(c, referenceQueue);
        //把对cow的强引用置为空
        c = null;
        
        softReference.clear();
        System.out.println("clear Reference");

        System.out.println("获取软引用下的Cow = " + softReference.get());

        //加入队列,这一步虚拟机会去作,可是由于时间上的问题,咱们手动触发一下
        softReference.enqueue();
        
        Reference<? extends Cow> cow = referenceQueue.remove();
        System.out.println("从释放队列中获取Cow = " + cow);

    }
}
class Cow{}
复制代码

当垃圾收集器决定收集弱可触及对象的时候,它会清除WeakReference对象中引用的Cow对象(经过clear()方法)。而后可能当即把这个WeakReference对象当即加入它的引用队列中,也可能在稍后的某个时间加入。

为了把引用对象加入它所关联的队列中,垃圾收集器会执行它的enqueue()方法。只有在建立引用对象时关联了一个队列,而且当且仅当该对象第一次执行enqueue()方法时,才把引用对象加入这个队列中。

在不一样的状况下,垃圾收集器把软引用、弱引用、影子引用对象加入队列表示三种不一样的可触及性状态的转变。这一共表示了6中可触及状态,状况以下:

  • 强可触及:对象能够从根节点不经过任何引用对象搜索到。对象生命周期从强可触及状态开始,而且只要有根节点或者另一个强可触及对象引用它,就保持强可触及状态。垃圾收集器不会试图回收该状态下的对象。
  • 软可触及:对象不是强可触及,可是能够从根节点开始经过一个或者多个软引用对象触及。垃圾收集器==可能==回收软可触及对象。若是发生了,它会清除全部到此软可触及对象的软引用。当垃圾收集器清除一个和引用队列有关联的软引用对象时,它会把该软引用对象加入队列。
  • 弱可触及:对象既不是强可触及也不是软可触及,可是从根节点开始能够经过一个或多个弱引用对象触及。垃圾收集器==必须==回收弱可触及对象占用的内存。垃圾收集器回收时,它会清除全部到此弱可触及对象的弱引用,并将弱引用对象加入队列(若是有关联的话)
  • 可复活的:对象既不是强可触及、软可触及、也不是弱可触及,可是仍然可能经过某些终结方法复活到这几个状态之一。
  • 影子可触及:对象既不是强可触及、软可触及、也不是弱可触及,而且已经判定不会被任何终结方法复活(若是对象定义了终结方法。此时终结方法已经被执行过了),而且它能够从根节点开始经过一个或多个影子引用对象触及。一个某个影子引用的对象变成影子可触及状态,垃圾收集器马上把该引用对象加入队列。==垃圾收集器不会清除一个影子引用,全部的影子引用必须由程序明确地清除==
  • 不可触及:一个对象不是强可触及、软可触及、弱可触及,也不是影子可触及,而且他不可复活。不可触及对象已经准备好被回收了。

请注意,垃圾收集器再把软引用和弱引用对象加入关联队列时,是在他们的引用目标离开相应的可触及状态时(调用clear
而影子引用对象加入队列是在引用目标进入相应状态时(也就是构造一个影子引用对象,并执行enqueue()后)。
也就是说垃圾收集器把软引用或者弱引用对象加入队列标志着引用对象刚刚离开了软可触及或者弱可触及状态;而垃圾收集器把影子引用加入队列标志着引用目标已经进入了影子可触及状态。==影子可触及对象会保持影子可触及状态,直到程序显式地清除了引用对象==。

不一样类型引用的用法

垃圾收集器对待软、弱和影子对象的方法不一样,是由于每一种都是被设计成为程序提供不一样的服务。

  • 软引用能够建立内存中的缓存,它与程序的总体内存需求有关。
  • 弱引用能够建立规范映射,好比哈希表,它的关键字和值在没有其余引用时能够从映射表中清除。
  • 影子引用能够实现除终结方法之外的更加复杂的临终清理政策。

影子引用的一些注意事项

请注意,要使用软引用或者弱引用的引用目标,能够调用对象的get()方法。若是引用目标没有被清除,则返回被引用的对象;若是被清除了,则返回null

可是对于影子引用对象的get()方法,始终返回null;咱们看下PhantomReference的源码

public class PhantomReference<T> extends Reference<T> {
    public T get() {
        return null;
    }
    public PhantomReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}
复制代码

真滴是简洁啊。。。。==为何要这样呢?==
咱们前面描述了6个状态,而对于影子可触及状态来讲,它表示对象是不可复活的。若是影子引用的get()方法有返回对象的话,那么这个规则就要被打破了。
请记住==若是一个对象达到了影子可触及状态,它不能再复活。==

不过虚拟机设计的真的这么严谨么?
咱们看下面的代码:

public static void main(String[] args) {
        ReferenceQueue<Cow> referenceQueue = new ReferenceQueue<>();
        Cow c = new Cow();
        PhantomReference<Cow> softReference = new PhantomReference<Cow>(c, referenceQueue);
        //把对cow的强引用置为空
        c = null;

        System.out.println("get()获取影子引用下的Cow = " + softReference.get());

        //加入队列,这一步虚拟机会去作,可是由于时间上的问题,咱们手动触发一下
        softReference.enqueue();
        
        //remove 后就能够取得影子引用对象了
        Reference<? extends Cow> cow = referenceQueue.remove();
        
        //上面的get()没取到对象,那咱们反射试一下
        Field field = Reference.class.getDeclaredField("referent");
        field.setAccessible(true);
        Object obj = field.get(cow);
        System.out.println("从释放队列中获取Cow = " + obj);
        
        //手动释放一下
        cow.clear();
        
        //再反射获取一下
        obj = field.get(cow);
        System.out.println("从释放队列中获取Cow = " + obj);
    }
复制代码

输出以下:

获取影子引用下的Cow = null
从释放队列中获取Cow = hua.lee.jvm.Cow@60e53b93
从释放队列中获取Cow = null
复制代码

咱们看到,==影子可触及状态的对象也是能够被拿出来的嘛==
另外,有一点须要注意的是==影子可触及状态的对象是不会被垃圾收集器给回收的,咱们须要像上面的示例同样手动clear()来释放对象==

软引用的经常使用场景

虚拟机的实现须要在抛出OOM以前清除掉软引用,但在其余状况下能够自行选择清理的时间或者是否清除。实现最好是只在内存不足的状况下才去清除软引用,清除的时候先清除老的而不是新的,清除长期未用的而不是最近使用的。

软引用可让你在内存中缓存那些须要从外部费时获取的数据,好比文件中、数据库里或者网络上的数据。
只要虚拟机有足够的内存,能够在堆中保存全部的强引用数据和软引用数据。
若是内存紧张,垃圾收集器会决定清除软引用,回收被软引用的数据所占用的空间。下一次程序须要使用这个数据时,可能不得再也不次从外部数据源进行加载。

弱引用的经常使用场景

弱引用相似于软引用,但不一样的是:

  • 垃圾收集器能够自行决定是否清除处于软可触及状态的对象的软引用。
  • 而对于弱可触及状态的对象,垃圾收集器会当即清除相关的弱引用。

弱引用的这种特性使得咱们能够用关键字和值来建立规范映射。java.lang.WeakHashMap类就是用弱引用提供这样的规范映射。
能够经过put()方法加入键值对到WeakHashMap的实例。不过和HashMap不一样的是,在WeakHashMap中,关键字对象是经过一个关联到引用队列的弱引用实现的。若是垃圾收集器检测到关键字对象时弱可触及的,它会清除引用而且把弱引用到该对象的引用对象加入到各自的队列。下次WeakHashMap被访问的时候,它会从引用队列中拉出全部的被垃圾收集器存放的弱引用对象,并清除和其有关的映射关系。

相关文章
相关标签/搜索