垃圾收集器回收的是什么?固然是"垃圾"。那什么是"垃圾"呢?在生活中当一个东西对咱们来讲已经没有使用价值,没法利用时,便会做为"垃圾"被咱们丢弃掉。同理在JVM中当一个对象不会再被使用时,就成为垃圾回收器回收的对象---"垃圾"。为何要回收"垃圾"呢?生活中,咱们的房间空间是必定的,若是不清理"垃圾",会使得咱们可利用的空间会愈来愈小,JVM中若是没有垃圾回收,也就是所占据的空间将不可回收,这就会形成了内存泄露。java
读过上文的应该知道,Java中几乎全部的对象实例都在Java堆中分配内存,方法区用于存储已被虚拟机加载的类型信息、常量、静态变量、即时编译器编译后的代码缓存等数据。因此垃圾收集器进行回收的区域是:堆和方法区中。算法
引用计数算法是最古老的辨别方法。首先在对象中添加一个引用计数器,初始值为零,每当有一个地方引用它时,计数器值就加一;当引用失效时,计数器值就减一;因此任什么时候刻当一个对象的计数器值为零时,表明此对象没有被任何地方引用,也就是垃圾收集器要回收的"垃圾"。缓存
客观地说,引用计数算法虽然占用了一些额外的内存空间来进行计数,但它的原理简单,断定效率也很高,在大多数状况下它都是一个不错的算法。可是在Java 领域,至少主流的Java虚拟机里面都没有选用引用计数算法来管理内存,主要缘由是,这个看似简单的算法有不少例外状况要考虑,必需要配合大量额外处理才能保证正确地工做,譬如单纯的引用计数就很难解决对象之间相互循环引用的问题。安全
所谓对象之间的相互引用问题:除了对象a和b相互引用着对方以外,这两个对象之间再无任何引用。可是它们由于互相引用对方,致使它们的引用计数器都不为0,因而引用计数器法没法通知GC回收器回收它们。以下图红色区域。数据结构
基本思路就是经过 一系列称为“GC Roots”的根对象做为起始节点集,从这些节点开始,根据引用关系向下搜索,搜索过程所走过的路径称为“引用链”,若是某个对象能够根据引用链到达GC Root,那么说明这个对象被引用中,反之,则证实此对象是不可能再被使用的。以下图所示。jvm
在Java技术体系里面,固定可做为GC Roots的对象包括如下几种:布局
运行时常量池主要回收的是废弃的常量。那么,咱们怎么判断一个常量时废弃常量呢?优化
假如在常量池中存在字符串"abc",若是当前没有任何String对象引用该字符串常量的话,就说明常量”abc“就是废弃常量,若是这时发生内存回收的话并且有必要的话,”abc“会被系统清理出常量池。线程
须要知足如下三个条件:设计
虚拟机能够对知足上述3个条件的无用类进行回收,这里仅仅是”能够“,而并非和对象同样不适用了就必然会被回收。
不管是经过引用计数算法判断对象的引用数量,仍是经过可达性分析算法判断对象是否引用链可达,断定对象是否存活都和“引用”离不开关系。在JDK 1.2版以前,Java里面的引用是很传统的定义: 若是reference类型的数据中存储的数值表明的是另一块内存的起始地址,就称该reference数据是表明某块内存、某个对象的引用。
在JDK 1.2版以后,Java对引用的概念进行了扩充,将引用分为强引用、软 引用、弱引用和虚引用4种,这4种引用强度依次逐渐减弱。
强引用是最传统的“引用”的定义,是指在程序代码之中广泛存在的引用赋值,即相似“Object obj=new Object()”这种引用关系。不管任何状况下,只要强引用关系还存在,垃圾收集器就永远不会回收掉被引用的对象。当内存空间不足时也不回收,将抛出OutOfMemoryError异常,使程序异常终止。若是想中断强引用和某个对象之间的关联,能够显示地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象。
软引用是用来描述一些还有用,但非必须的对象。只被软引用关联着的对象,在系统将要发生内存溢出异常前,会把这些对象列进回收范围之中进行第二次回收,若是此次回收尚未足够的内存, 才会抛出内存溢出异常。所以,这一点能够很好地用来解决OOM的问题,而且这个特性很适合用来实现缓存:好比网页缓存、图片缓存等。在JDK 1.2版以后提供了SoftReference类来实现软引用。
弱引用也是用来描述那些非必须对象,可是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。当垃圾收集器开始工做,不管当前内存是否足够,都会回收掉只被弱引用关联的对象。也就是说软引用关联的对象只有在内存不足时才会被回收,而被弱引用关联的对象在JVM进行垃圾回收时总会被回收。在JDK 1.2版以后提供了WeakReference类来实现弱引用。
虚引用也称为“幽灵引用”或者“幻影引用”,它是最弱的一种引用关系。一个对象是否有虚引用的存在,彻底不会对其生存时间构成影响,也没法经过虚引用来取得一个对象实例。为一个对象设置虚引用关联的惟一目的只是为了能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2版以后提供 了PhantomReference类来实现虚引用。
引用类型 | 被回收时间 | 用途 | 生存时间 |
---|---|---|---|
强引用 | 历来不会 | 对象的通常状态 | JVM中止运行时 |
软引用 | 内存不足时 | 对象缓存 | 内存不足时 |
弱引用 | jvm垃圾回收时 | 对象缓存 | gc运行后 |
虚引用 | 未知 | 未知 | 未知 |
当前商业虚拟机的垃圾收集器,大多数都遵循了“分代收集”的理论进行设计,分代收集名为理论,实质是一套符合大多数程序运行实际状况的经验法则,它是在创建分代假说之上:
1)弱分代假说:绝大多数对象都是朝生夕灭的。
2)强分代假说:熬过越屡次垃圾收集过程的对象就越难以消亡。
3)跨代引用假说:跨代引用相对于同代引用来讲仅占极少数。
1)2)这两个分代假说共同奠基了多款经常使用的垃圾收集器的一致的设计原则:收集器应该将Java堆划分出不一样的区域,而后将回收对象依据其年龄(年龄即对象熬过垃圾收集过程的次数)分配到不一样的区域之中存储。根据前两条假说逻辑推理得出的隐含推论:存在互相引用关系的两个对象,是应该倾向于同时生存或者同时消亡的。
依据3)这条假说,咱们就不该再为了少许的跨代引用去扫描整个老年代,也没必要浪费空间专门记录每个对象是否存在及存在哪些跨代引用,只需在新生代上创建一个全局的数据结构(该结构被称为“记忆集”,Remembered Set),这个结构把老年代划分红若干小块,标识出老年代的哪一块内存会存在跨代引用。此后当发生Minor GC时,只有包含了跨代引用的小块内存里的对象才会被加入到GC Roots进行扫描。虽然这种方法须要在对象改变引用关系时维护记录数据的正确性,会增长一些运行时的开销,但比起收集时扫描整个老年代来讲仍然是划算的。
IBM公司曾有一项专门研究对新生代“朝生夕灭”的特色作了更量化的诠释——新生代中的对象有98%熬不过第一轮收集。所以并不须要按照1∶1的比例来划分新生代的内存空间。
在1989年,Andrew Appel针对具有“朝生夕灭”特色的对象,提出了一种更优化的半区复制分代策略,如今称为“Appel式回收”。HotSpot虚拟机的Serial、ParNew等新生代收集器均采用了这种策略来设计新生代的内存布局。Appel式回收的具体作法是把新生代分为一块较大的Eden空间和两块较小的 Survivor空间,每次分配内存只使用Eden和其中一块Survivor。发生垃圾搜集时,将Eden和Survivor中仍 然存活的对象一次性复制到另一块Survivor空间上,而后直接清理掉Eden和已用过的那块Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8∶1,也即每次新生代中可用内存空间为整个新生代容量的90%(Eden的80%加上一个Survivor的10%),只有一个Survivor空间,即10%的新生代是会被“浪费”的。固然,98%的对象可被回收仅仅是“普通场景”下测得的数据,任何人都没有办法百分百保证每次回收都只有很少于10%的对象存活,所以Appel式回收还有一个充当罕见状况的“逃生门”的安全设计,当Survivor空间不足以容纳一次Minor GC以后存活的对象时,就须要依赖其余内存区域(实际上大多就是老年代)进行分配担保。
它是最基础的收集算法,之因此说它是最基础的收集算法,是由于后续的收集算法大多都是以标记-清除算法为基础,对其缺点进行改进而获得的。这个算法分为两个阶段,“标记”和”清除“。首先标记出全部须要回收的对象,标记过程就是对象是否属于垃圾的断定过程。在标记完成后统一回收全部被标记的对象。也能够反过来,标记存活的对象,统一回收全部未被标记的对象。它有两个不足的地方:
标记-复制算法常被简称为复制算法。为了解决面对大量可回收对象时执行效率低的问题,复制算法出现了。它能够把内存分为大小相同的两块,每次只使用其中的一块。当这一块的内存使用完后,就将还存活的对象复制到另外一块区,而后再把使用的空间一次清理掉。这样就使每次的内存回收都是对内存区间的一半进行回收
若是内存中多数对象都是存活的,这种算法将会产生大量的内存间复制的开销,但对于多数对象都是可回收的状况,算法须要复制的就是占少数的存活对象,并且每次都是针对整个半区进行内存回收,分配内存时也就不用考虑有空间碎片的复杂状况,只要移动堆顶针,按顺序分配便可。这样实现简单,运行高效,不过其缺陷也显而易见,这种复制回收算法的代价是将可用内存缩小为了原来的一半,空间浪费未免太多了一 点。
如今的商用Java虚拟机大多都优先采用了这种收集算法去回收新生代。
标记-复制算法在对象存活率较高时就要进行较多的复制操做,效率将会下降。更关键的是,若是不想浪费50%的空间,就须要有额外的空间进行分配担保,以应对被使用的内存中全部对象都100%存活的极端状况,因此在老年代通常不能直接选用这种算法。
根据老年代的特色提出的一种标记算法,标记过程和“标记-清除”算法同样,可是后续步骤不是直接对可回收对象进行回收,而是让全部存活的对象向一段移动,而后直接清理掉边界之外的内存。
周志明《深刻理解Java虚拟机:JVM高级特性与最佳实践》