什么是垃圾?
垃圾是指在运行程序中没有任何指针指向的对象,即这个对象就是要被回收的对象.
垃圾回收相关算法:
垃圾回收可分为两个阶段:
第一阶段:垃圾标记阶段(即对象是否存活):
在GC之前首先要区分出内存中哪些对象是否存活,那些对象是已经死亡的对象.只有被标记为死亡的对象,GC才会在执行垃圾回收时,释放其所占的内存空间.
判断对象存活的一般方式:引用计数器算法和可达性分析算法.
1.引用计数器算法:
对每个对象保存一个整型的引用计数器属性,用于记录对象被引用的情况.
对于一个对象A,只要有任何一个对象引用了A,则A的引用计数器就加1;当引用失效时,引用计数器就减1。只要对象A的引用计数器的值为0,即表示对象A不可能再被使用,可进行回收。
优点:实现简单,垃圾对象便于辨识;判定效率高,回收没有延迟性。
缺点:它需要单独的字段存储计数器,这样的做法增加了存储空间的开销。每次赋值都需要更新计数器,伴随着加法和减法操作,这增加了时间开销。引用计数器有一个严重的问题,即无法处理循环引用的情况。这是一条致命缺陷,导致在Java的垃圾回收器中没有使用这类算法。
而在java中并没有采用这种算法,来进行垃圾回收.
2.可达性分析(根搜索算法,追踪性垃圾收集)
优点:具备了实现简单,执行效率高效等特点,解决了引用计数算法中的循环引用问题,防止内存泄漏问题.
GC Roots根集合:一组必须活跃的引用.
实现的基本思路:
以根对象集合为起始点,按照从上到下的方式搜索被根对象集合所连接的目标对象是否是可达的,而经过算法搜索过的路径称为引用链,如果目标对象没有任何引用相连,则是不可达的,即就会被标记成为垃圾对象。
在java语言中,GC Roots 包括以下几种元素:
除了这些固定的GC Roots集合以外,根据用户所选用的垃圾收生器以及当前回收的内存区域不同,还可以有其他对象“临时性”地加入,共同构成完 整GC Roots集合。
比如:分代收集和局部回收(Partial GC)。如果只针对Java堆中的某一块区域进行垃圾回收(比如:典型的只针 对新生代),必须考虑到内存区域是虚拟机自己的实现细节,更不是孤立封闭的,这个区域的对象完全有可能被其他区域的对象所引用,这时候就需要一并将关联的区域对象也加入GC Roots集合中去考虑,才能保证可达性分析的准确性。
由于Root采用栈方式存放变量和指针,所以如果一个指针,它保存了堆内存
里面的对象,但是自己又不存放在堆内存里面,那它就是一个Root。
补充:对象的finalzation机制
垃圾回收之前总会调用这个对象的finalize()方法.
注意:不要主动地调用某个对象的finallize()方法,调用有可能会导致对象复活,或者影响GC性能.
虚拟机中对象一般可能处于三种状态:
1.可触及的:从根节点开始,可以到达这个对象.
2.可复活的: 对象的所有引用都被释放,但是对象有可能在finalize()中复活。
3.不可触及的:对象的finalize()被调用,而且没有复活,那么就会进入这种状态,不可触及得对象不可能复活的,因为finalize()只会被调用一次.所以说只有对象时不可触及的对象时才会被回收.
具体判断过程:判定一个对象objA是否可回收,至少要经历两次标记过程:
如果对象objA到 GC Roots没有引用链,则进行第一次标记。进行筛选,判断此对象是否有必要执行finalize()方法,如果对象objA没有重写finalize()方法,或者finalize()方法已经被虚拟机调用过,则虚拟机视为“没有必要执行”,obja被判定为不可触及的。 ②如果对象obja重写了finalize()方法,且还未执行过,那么objA会被插入到F-Queue队列中,由一个虚拟机自动创建的、低优先级的Finalizer线程触发其finalize()方法执行。
③finalize()方法是对象逃脱死亡的最后机会,稍后Gc会对F-Queue队列中的对象进行第二次标记。如果obja在finalize()方法中与引用链上的任何一个对象建立了联系, 那么在第二次标记时,objA会被移出“即将回收”集合。之后,对象会再次出现没有引用存在的情况。在这个情况下,finalize方法不会被再次调用,对象会直接变成不可触及的状态,也就是说,一个对象的finalize方法只会被调用一次。
第二阶段:垃圾清除阶段:
jvm目前常用的三种垃圾收集算法:标记清除(Mark-Sweep)算法,复制算法,标记压缩算法.
1.标记清除算法:
执行过程:当堆中的有效内存空间被耗尽时,就会停止整个程序,然后进行两项工作:标记和清除。
标记阶段:Collector从引用跟节点开始遍历,标记所有被引用的对象.一般是在对象的Header中记录为可达对象.清除阶段:Collector对堆内存从头到尾进行线性遍历,如果发现某个对象在其Header中没有标记为可达对象,则会进行回收.
清除阶段:并不是真正意义上的置空,而是把需要清除的对象地址保存在空闲的地址列表里.如果有新对象,就会判断内存是否够用,然后进行存放(覆盖原来的垃圾对象).
缺点:效率不高,在GC 时会停止程序.请理之后会产生内存碎片,需要维护一个空闲列表.
2. 复制(Copying)算法:
执行过程:将活着的内存空间分为两块,每次只是使用其中一块,在垃圾回收时将正在使用的内存中的存活对象复制到未被使用的内存模块中,之后清除正在使用的内存中的所有对象,交换连个内存的角色,最后完成垃圾回收.
优点:没有标记和清除过程,实现简单,运行高效,不会出现碎片问题.
缺点: 需要两倍的内存空间,内存占用大.
如果程序中垃圾对象很多,赋值算法就不会很理想。复制算法需要复制程序中的对象不能太大.
3. 标记整理(Mark-Compact)算法:
执行过程:标记阶段:从根节点开始标记所有被引用对象.整理阶段:将所有的存活对象压缩到内存的一端,按顺序排放,然后再清除边界之外的所有空间.
缺点:效率低,移动对象的同时,如果对象被其他对象引用,则还需要调整引用的地址.移动过程中需要全程暂停用户应用程序(STW).
对比:
标记清除 | 标记整理 | 复制算法 | |
速度 | 中等 | 最慢 | 最快 |
空间开销 | 少(碎片堆积) | 少(不存在碎片堆积) | 活对象的2倍大小 |
移动对象 | 否 | 是 | 是 |
目前的GC基本都采用用分代收集的算法(即具体问题具体分析):
在HotSpot中,基于分代的概念,GC所使用的内存回收算法必须结合年轻代和老年代各自的特点。年轻代(Young Gen)年轻代特点:区域相对老年代较小,对象生命周期短、存活率低,回收频繁。这种情况复制算法的回收整理,速度是最快的。复制算法的效率只和当前存活对象大小有关,因此很适用于年轻代的回收。而复制算法内存利用率不高的问题,通过hotspot中的两个survivor的设计得到缓解。老年代(Tenured Gen)老年代特点:区域较大,对象生命周期长、存活率高,回收不及年轻代频繁。这种情况存在大量存活率高的对象,复制算法明显变得不合适。一般是由标记-清除或者是标记-清除与标记整理的混合实现。
Mark阶段的开销与存活对象的数量成正比。
Sweep阶段的开销与所管理区域的大小成正相关。
Compact阶段的开销与存活对象的数据成正比。