Java 虚拟机的堆里存放着程序运行中所建立的全部对象。虚拟机可使用new
、newarray
、anewarray
和multianewarray
指令来建立对象,可是没有明确的代码来释放它们。垃圾收集
就是自动释放再也不被程序所使用的对象的过程。java
本篇文章并非要描述正式的 Java 垃圾收集器,由于根本不存在这样一个正式的描述。前面说过,Java 虚拟机规范不要求任何特定的垃圾收集技术,这根本不是必需的。可是在发明能够无限使用的内存前,大部分的 Java 虚拟机都会附带垃圾收集功能。程序员
垃圾收集
这个名字暗示着程序再也不须要的对象就是垃圾
,能够被丢弃。更准确地说,应该被叫作内存回收
。当一个对象再也不被程序所引用时,它所使用的堆空间
能够被回收,一遍后续的新对象所使用。算法
垃圾收集器必须可以确认哪些对象是再也不被引用的,而且可以把它们所占据的堆空间释放出来。在释放对象的过程当中,垃圾收集器还要运行将要被释放对象的终结方法(finalizer
)。数据库
除了释放再也不被引用的对象,垃圾收集器还要处理堆碎块。堆碎块是在程序运行过程当中产生的。在请求分配新对象的内存空间时,可能不得不增大堆空间的大小,虽然可使用的总空闲空间是足够的。这是由于堆中的空闲空间并不连续,没法放下一个新的对象。编程
把内存回收的任务交给虚拟机有几个好处:缓存
可是使用垃圾收集,有一个潜在的缺陷就是加大了程序的负担,可能影响程序性能。Java虚拟机必须追踪哪些对象正在使用,哪些对象须要释放。和明确释放内存比起来,内存释放过程还会须要更多的CPU时间片。安全
任何垃圾收集算法必须作两件事:网络
垃圾检测一般经过创建一个根对象
的集合而且检查从这些跟对象开始的可触及性来实现。若是正在执行的程序能够访问到跟对象和某个对象之间存在引用路径,这个对象就是可触及的。对于程序来讲,跟对象老是能够访问的。从这些根对象开始,任何能够被触及的对象都被认为是活动
的对象。没法被触及的对象被认为是垃圾
,它们再也不被程序使用到。数据结构
任何根对象引用的对象都是可触及的,从而是活动的。另外,任何被活动的对象引用对象都是可触及的。程序能够访问任何可触及的对象,因此这些对象必须保存在堆里面。任何不可触及的对象均可以被收集,由于程序被办法访问它们。jvm
Java 虚拟机的根对象集合根据实现方式各有不一样,可是总会包含局部变量中的对象引用和栈帧的操做数栈(以及类变量中的对象引用)。关于根对象的来源大概有这几种:
说实话这三条咋这么抽象嘞,先往下看看把
在 Java 虚拟机的实现中,有些垃圾收集器能够区分真正的对象引用和看上去很像合法对象引用的基本类型(好比一个 int 变量)之间的差异。(例如一个 int 整数,若是被解释是一个本地指针,可能指向堆中的一个对象)但是某些垃圾收集器仍然选择不区分真正的对象引用和假装品
,这种垃圾收集器被称为保守的,由于它们可能作不到释放掉每个再也不引用的对象。对于保守的收集器,有时候垃圾对象也被错误的判断为活动的,由于有一个看上去像是对象引用的基本类型"引用"了对象。这种保守的垃圾收集器是垃圾回收速度提升了,由于有一些垃圾被遗忘了。
区分活动对象和垃圾对象的两个基本方法是引用计数和跟踪。
引用计数是垃圾回收的早期策略。在这种方法中,堆中每个对象都有一个引用计数。
规则包括:
在这种方法中,一个对象被垃圾收集后可能会触发后续其余对象的垃圾收集行动。
引用计数没法检测出循环引用(即两个或者更多对象之间的相互引用)。
循环引用的例子如
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虚拟机的垃圾收集器可能有对付堆碎块的策略。标记-清除
收集器一般使用的两种策略是压缩和拷贝。这两种方式都是经过快速地移动对象来减小堆碎块。
压缩收集器把活动的对象越过空闲区移动到堆的另外一端,这样堆的另外一端会出现一个大的连续空间。此后,全部被移动的对象的引用也会被更新,指向新的内存地址。
更新被移动对象的引用有时候会经过一个间接对象引用层
,不直接引用堆中的对象,对象的引用实际上指向一个对象句柄表。对象句柄才是真正指向堆中对象的实际位置。当对象被移动了,只须要更新句柄表就能够。不过在对象的访问上,由于增长了一个句柄表,性能有所损失。
拷贝垃圾收集器把全部的活动对象都移动到一个新的区域。在拷贝的过程当中,它们被紧挨着布置,因此能够消除本来它们在旧区域的空隙。而原有的区域被认为是空闲区。
这种方法的好处就是从根对象开始遍历的过程当中,一旦发现对象就进行拷贝,再也不有标记和清除的区分。对象被快速拷贝到新区域,同时转向指针
仍然留在原来的位置。转向指针
可让垃圾收集器发现已经被转移对象的引用。而后垃圾收集器把和这个对象有关的引用设置为转向指针
的值。
通常的拷贝收集器算法被称为中止-拷贝
。方案以下:
中止-拷贝
过程结束后,程序恢复执行。这种方法的代价就是,对于指定大小的堆来讲,实际上须要两倍的内存来运行。
图形描述以下:
在程序执行中,这个上述过程一次有一次地重复。
简单的中止-拷贝
收集器的缺点是,每一次收集时,全部的活动对象都必须被拷贝。大部分语言的大多数程序都有一下特色,若是咱们全面考虑这些,拷贝算法是能够改进的。
简单的中止-拷贝
收集器浪费效率的一个主要缘由就是,它们每次把这些生命周期很长的对象来回拷贝,消耗大量的时间。
按代收集的收集器经过把对象按照寿命来分组解决中止-拷贝
效率低下的问题,更多地收集那些短暂出现的年幼对象,而非寿命比较长的对象。逻辑以下:
按代收集技术除了能够应用于中止-拷贝
垃圾收集算法,也能够用于标记-清除
垃圾回收算法。无论哪一种状况下,把堆按照年龄层分解均可以提升最基本的垃圾收集算法的性能。
自适应收集器算法李永乐以下事实:在某种状况下某些垃圾收集算法工做的更好,而另一些收集算法在另外的状况下工做得更好。
自适应算法监视堆中的情形,并对应的调整为合适的垃圾收集技术。
使用自适应方法,Java虚拟机的实现者不须要只选择一种特定的算法。可使用多种技术,以便在最擅长的场合使用它们。
train GC
)火车算法最先是有Richard Hudson
和Eliot Moss
提出的,目的是为了在成熟对象空间提供限定时间的渐进收集
,最先用于Sun公司的Hotspot
虚拟机。该算法详细的说明了按代收集的垃圾收集器的成熟对象空间的组织。
唏嘘的是到Sun JDK 6的时候就已经完全不包含train GC了,不过更重要的是思想,仍是看一看吧。
垃圾收集算法和主动释放对象内存比起来有一个潜在的缺点,即垃圾收集算法中程序员对安排 CPU 时间进行内存回收的过程缺少控制。
要精确的预测出什么时候进行垃圾收集、收集须要多长时间基本上是不可能。由于垃圾收集通常会停止整个程序来查找和收集垃圾对象,它们可能在程序执行的任意时刻触发垃圾收集,而且停止的时间也没法肯定。这种垃圾收集的暂停有时候长得让用户注意到了。
而当一种垃圾收集算法可能致使用户可察觉到的停顿或者使得程序没法知足实时系统的要求,这种算法被称做破坏性的。
达到非破坏性垃圾收集的方法是使用渐进式垃圾收集算法。
渐进式垃圾收集器就是不试图一次性发现并回收全部的垃圾对象,而是每次发现并回收一部分。所以每次都只有堆的一部分执行垃圾收集,所以理论上说的每一次收集会持续更短的时间。
若是可以保证每次收集不超过一个最大时间长度,就可让Java虚拟机适合实时环境,而且也能够消除用户可察觉的停顿。
一般渐进式收集器都是按代收集的收集器。
火车算法把成熟的对象空间划分为固定长度的内存块,算法每次在一个块中单独执行。规则以下:
在原始论文中,内存块叫作车箱
;集合叫作火车
。成熟对象的内存空间叫作火车站
。
算法组织图以下:
火车按照它们建立时的顺序分配号码。
所以,假设咱们将第一列火车(最早进入该年龄层的对象内存)被拉进轨道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
的最后一节车箱老是在火车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
类用来管理对象链接,包含SoftReference
、WeakReference
、PhantomReference
三个实现类,继承图以下:
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
实例对象的软引用。引用示意图以下:
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
的总体结构,以下图:
==说实话,Reference的体系以前没怎接触,只是在Android
中简单使用WeakReference
的get
方法,等结束本篇垃圾收集,单独撩拨一下==
那咱们就能够这样写:
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
被访问的时候,它会从引用队列中拉出全部的被垃圾收集器存放的弱引用对象,并清除和其有关的映射关系。