JAVA垃圾收集器

JAVA垃圾收集器

如何判断对象已死?

1.引用计数算法

给对象添加一个引用计数器,每当有一个地方引用它时,计数器就+1,当引用失效,计数器就-1。为0就死。

弊端:无法解决对象的相互循环引用的问题 问题代码如下
public class RGC {
public Object instance = null;
private static final int _1MB =10241024;
private byte[] bigSize = new byte[2
_1MB];
public static void main(String[] args){
RGC oA = new RGC();
RGC oB = new RGC();
oA.instance = oB;
oB.instance = oA;
oA = null;
oB = null;
System.gc();
}
}//实际上两个对象都不能再被访问了,但是因为他们相互引用着对方,所以两个引用计数都不为0,就无法GC。

2.可达性分析算法

以一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索
搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连
就说明该对象可回收。
--书64页--

**关于finalize方法
如果对象有finalize方法,那么在被GC处理之前会执行一次,注意,一个finalize方法只会被执行一次,如果方法响应时间过长会被认为执行失败,如果在执行后,让对象本身有了被引用,就可以逃过被GC处理的命运。

什么是对象的引用?

最初,JAVA中的引用定义是:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址就称这块内存代表一个引用。
但是有些对象,食之无味弃之可惜,如果内存空间还多,其实没必要抛弃。
所以JDK1.2以后更新了新的引用概念:
引入了强引用、软引用、弱引用、虚引用这四个概念

*1.强引用(StrongReference)
强引用不会被GC回收,并且在java.lang.ref里也没有实际的对应类型。
举例:Object obj = new Object();

*2.软引用(SoftReference)
软引用在JVM快要发生内存溢出异常时才会被GC回收,否则不会回收。
正是由于这种特性软引用在caching和pooling中用处广泛。
软引用的用法:Object obj = new Object();
SoftReference softRef = new SoftReference(obj);
// 使用 softRef.get() 获取软引用所引用的对象
Object objg = softRef.get();

*3.弱引用(WeakReference)
弱引用也是用来描述非必需对象的。但是强度比软引用更弱。
被弱引用关联的对象只能存活到下一次垃圾收集发生之前。

*4.虚引用(PhantomReference)
最弱的引用。一个对象有没有虚引用,完全不影响其生存时间。
当GC一但发现了虚引用对象,将会将PhantomReference对象插入ReferenceQueue队列,
而此时PhantomReference所指向的对象并没有被GC回收,要等到ReferenceQueue被你真正的处理后才会被回收。
举例:

1
2
3
4
5
6
7
8
9
Object obj = new Object();
ReferenceQueue<Object> refQueue = new ReferenceQueue<Object>();
PhantomReference<Object> phanRef = new PhantomReference<Object>(obj, refQueue);
// 调用phanRef.get()不管在什么情况下会一直返回null
Object objg = phanRef.get();
// 如果obj被置为null,当GC发现了虚引用,GC会将phanRef插入进我们之前创建时传入的refQueue队列
// 注意,此时phanRef所引用的obj对象,并没有被GC回收,在我们显式地调用refQueue.poll返回phanRef之后
// 当GC第二次发现虚引用,而此时JVM将phanRef插入到refQueue会插入失败,此时GC才会对obj进行回收
Reference<? extends Object> phanRefP = refQueue.poll();

各种垃圾收集算法的思想及分代收集

《深入了解JAVA虚拟机》p69页

一.标记-清除算法
最基础的算法,先标记好需要回收的对象,然后回收。
*不足:效率太低,并且容易产生不连续碎片。

二.复制算法
(新生代常用算法)
思想:将内存分为两块,每次只使用其中一块,当进行回收时,将存活对象复制到另一块,然后把原本一块全部清理
*优点:没有内存碎片,按顺序分配内存,实现简单,运行高效。

*缺点:太浪费内存,并且如果对象成活率较高,效率就会变低。


补充: 目前商业虚拟机最常用的就是复制算法。并且实际上新生代并不是1:1划分,而是较大的Eden空间和两块较小的Survivor空间,因为根据统计,实际上新生代的对象98%都是朝生夕死。HotSpot虚拟机默认Eden和Survivor大小比例8:1,也就是说新生代中每次可用内存空间为整个新生代容量的90%,当然如果Survivor不够用就会使用老年代分配担保。

三.标记-整理算法
(老年代内存存活极高,所以不适合复制算法,就可以使用标记-整理)
思想:先标记,然后让所有存活对象向一端移动,然后直接清理掉端边界以外的内存。

OopMap

在正式的GC之前,要进行可达性分析来标记出将来可能要宣告死亡的对象。如果每次GC的时候都要遍历所有的引用,这样的工作量是非常大的。因为在可达性分析的时候要保证期间不发生引用关系的变化,所有执行线程要停顿等待,称为“Stop The World”,程序中的线程需要停止来配合可达性分析。

所以,每次直接遍历整个引用链肯定是不现实的。
为了应对这种尴尬的问题,最早有保守式GC和后来的准确式GC。

这里准确式GC就会提到一个OopMap,用来保存类型的映射表。

保守式GC

在进行GC的时候,会从一些已知的位置(GC Roots)开始扫描内存,扫描到一个数字就判断他是不是可能是指向GC堆中的一个指针(这里会涉及上下边界检查(GC堆的上下界是已知的)、对齐检查(通常分配空间的时候会有对齐要求,假如说是4字节对齐,那么不能被4整除的数字就肯定不是指针),之类的。)。然后一直递归的扫描下去,最后完成可达性分析。这种模糊的判断方法因为无法准确判断一个位置上是否是真的指向GC堆中的指针,所以被命名为保守式GC。

这种可达性分析的方式因为不需要准确的判断出一个指针,所以效率快,但是也正因为这种特点,他存在下面两个明显的缺点:
1.因为是模糊的检查,所以对于一些已经死掉的对象,很可能会被误认为仍有地方引用他们,GC也就自然不会回收他们,从而引起了无用的内存占用,造成资源浪费。

2.由于不知道疑似指针是否真的是指针,所以它们的值都不能改写;移动对象就意味着要修正指针。换言之,对象就不可移动了。有一种办法可以在使用保守式GC的同时支持对象的移动,那就是增加一个间接层,不直接通过指针来实现引用,而是添加一层“句柄”(handle)在中间,所有引用先指到一个句柄表里,再从句柄表找到实际对象。这样,要移动对象的话,只要修改句柄表里的内容即可。但是这样的话引用的访问速度就降低了。Sun JDK的Classic VM用过这种全handle的设计,但效果实在算不上好。

准确式GC

与保守式GC相对的就是准确式GC,何为准确式GC?就是我们准确的知道,某个位置上面是否是指针,对于java来说,就是知道对于某个位置上的数据是什么类型的,这样就可以判断出所有的位置上的数据是不是指向GC堆的引用,包括栈和寄存器里的数据。
网上看了下说是实现这种要求的方法有好几种,但是在java中实现的方式是:从外部记录下类型信息,存成映射表,在HotSpot中把这种映射表称之为OopMap,不同的虚拟机名称可能不一样。

实现这种功能,需要虚拟机的解释器和JIT编译器支持,由他们来生成OopMap。生成这样的映射表一般有两种方式

1.每次都遍历原始的映射表,循环的一个个偏移量扫描过去;这种用法也叫“解释式”;
2.为每个映射表生成一块定制的扫描代码(想像扫描映射表的循环被展开的样子),
以后每次要用映射表就直接执行生成的扫描代码;这种用法也叫“编译式”。

总而言之,GC停顿的时候,虚拟机可以通过OopMap这样的一个映射表知道,在对象内的什么偏移量上是什么类型的数据,而且特定的位置记录着栈和寄存器中哪些位置是引用。

安全点

OOPmap不可能每一条指令都有,而是每隔一段才有一个OOPMap,所以GC什么时候运行就需要在特定的点上。
而且这个时间不能太长也不能太短,所以安全点的选取是以程序“是否具有让程序长时间执行的特征”为标准
(因为每条指令执行的时间都非常短暂),“长时间执行”最明显的就是指令序列复用,
例如:方法调用,循环跳转,异常跳转等。

如何让线程稳定的停在安全点上呢???

两种方案:
    1.抢先式中断(没有虚拟机使用这个方法):让线程直接停止,如果没有到点,就让它跑到安全点上。
    2.主动式中断:设置一个标志,当线程主动轮询这个标志,发现为真就自己中断,标志轮询的点和安全点是重合的。               

如果线程并没有正在执行,该怎么停到安全点呢?

*Safe Region:安全区域:在一段代码片段中,引用关系不会发生变化。
 在线程执行到安全区域中的代码时,首先表示自己已经进入了安全区域,当那段时间里,GC就不会管这个线程。
 在线程要离开安全区域时,要先检查系统是否完成了GC过程,如果完成,线程继续执行。