说起垃圾回收,我们大概关注三个问题:
- 哪写内存需要回收
- 什么时候回收
- 如何回收
下面我们来一一解释:
- 回收的对象是已经不需要的,也就是没有引用指向的对象,那么如何确定对象是否已死,有如下几种算法:
1. 引用计数法
给对象添加一个引用计数器, 有引用就加一,失效就减一。但是它没办法解决对象之间循环引用的问题。
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024*1024;
private byte[] bigSize = new byte[2*_1MB];
public static void testGC() {
ReferenceCountingGC objA = new ReferenceCountingGC();
ReferenceCountingGC objB = new ReferenceCountingGC();
objA.instance = objB;
objB.instance = objA;
objA = null;
objB = null;
// 假定发生GC
System.gc();
}
public static void main(String[] args) {
testGC();
}
}
上述代码演示了Java虚拟机并没有因为循环引用而不回收,所以Java虚拟机不是通过引用计数来判断对象是否存活的。
-
可达性分析
通过一系列的称为“GC Roots"的对象作为起始点,从这些节点开始向下搜索,搜索走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的。
在Java中,可作为GC Roots的对象包括下面几种:- 虚拟机栈中引用的对象
- 方法区中静态属性引用的对象
- 方法区中常量引用的对象。
- 本地方法栈中JNI引用的对象。
引用类型: - 强引用:类似于Object obj = new Object(), 只要强引用还在,就不会被回收
- 软引用:用来描述一些有用但是并非必需的对象,使用SoftReference来实现软引用。
软引用会在内存要发生溢出之前回收,看回收了这些之后还会不会发生内存溢出 - 弱引用:强度比软引用更弱,使用WeakReference来实现弱引用。
再下一次垃圾回收前一定会被回收 - 虚引用,又称幽灵引用,它的存在可以用形同虚设来说明,使用PahantomReference来实现。
它存在的唯一目的是能在这个对象被回收时收到一个系统通知。
那么对于不可达的对象就一定会被回收吗?其实对象还有一次自我拯救的机会。
因为如果要真正宣告一个对象死亡,至少要经过两次标记。 在第一次标记中,会进行一次筛选,筛选的条件是此对象是否有必要执行finallize方法。若对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,就没有必要执行。 如果某个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在F-Queue队列中,并稍后由虚拟机触发执行。因为这里会执行finalize方法,所以可以在这个方法中将对象重新赋予某个引用,则拯救自己。
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive(){
System.out.println("yes, i am still alive: ");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("Finalize method executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws InterruptedException {
SAVE_HOOK = new FinalizeEscapeGC();
// 对象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
// 因为finalize方法优先级低,所以暂停0.5s以等待它
Thread.sleep(500);
if(SAVE_HOOK != null)
SAVE_HOOK.isAlive();
else
System.out.println("no, i am dead :(");
// 第二次拯救失败
SAVE_HOOK = null;
System.gc();
// 因为finalize方法优先级低,所以暂停0.5s以等待它
Thread.sleep(500);
if(SAVE_HOOK != null)
SAVE_HOOK.isAlive();
else
System.out.println("no, i am dead :(");
}
}
现在,已经明确了要回收的内存。
-
垃圾回收算法
- 标记-清除算法
这个算法分为“标记“和”清除“两个阶段,首先对要回收的内存进行标记,然后统一回收。
这个算法清除后会留下大量的空间碎片,导致之后无法分配大块内存。 - 复制算法
它将可用内存按容量分为大小相等的两块,每次只使用其中的一块。当这一块内存用完了,就将还存活的对象复制到另一个对象上去,然后再把已使用的空间一次性回收 - 标记-整理算法
先标记,然后不是直接清除,而是让存活的对象都向一端移动,然后直接清理掉端边界以外的内存。
- 标记-清除算法
-
HotSpot的算法实现
使用一组成为OopMap的数据结构来标记那些地方存放着对象引用,以便在寻找GC Roots的时候不用全部检查。
在OopMap的协助下,HotShot可以快速且准确的完成GC Roots的枚举。但还需保证一致性,就是在算法期间不能再有引用关系变化,HotShot中靠安全点来实现。程序执行时只能在到达安全点时才能暂停,称作safepoint。 -
HotSpot虚拟机所包含的垃圾收集器
-
Serial收集器
它在JDK1.3.1之前,是虚拟机新生代的唯一选择。这是一个单线程收集器,它在收集的时候,必须暂停其他所有线程。虽然它有这么大的缺点,但到现在1.7为止,他还是虚拟机运行在Client模式下的默认新生代收集器,因为它简单高效,没有线程交互的开销。
-
ParNew收集器
这个收集器是Serial的多线程版本,它是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关的主要原因是因为除了Serial收集器之外,目前只有它能够与CMS收集器配合工作。
-
Parallel Scavenge收集器是一个新生代收集器,使用复制算法,并且是一种并行的多线程收集器。
这个收集器的目标是达到一个可控制的吞吐量,而不是像其他收集器一样是为了尽可能地缩短垃圾收集时用户线程的停顿时间。这个收集器提供了两个参数用于精确控制吞吐量,分别是控制最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。示意图与2类似 -
Serial Old收集器
这个收集器是Serial收集器的老生代版本,主要用于Client模式下的虚拟机。如果用在Server模式下,它有两个作用:1. 在JDK1.5一起与Parallel Scavenge收集器搭配使用。 2. 作为CMS收集器的后备预案,在并发集发生Concurrent Mode Failure使用。示意图见1。 -
Parallel Old 收集器
于1.6后开始提供,是Parallel Scavenge收集器的老年版本,应用于吞吐量优先的场合。
-
CMS收集器
这是一种以获取最短回收停顿时间为目标的收集器。基于“标记-清除”算法,整个过程分为四个步骤:-
初始标记
-
并发标记
-
重新标记
-
并发清除
其中,初始标记和重新标记这两个步骤仍然需要“stop the world"。
初始标记只是标记一下GC Roots能直接关联到的对象,速度很快。
并发标记就是进行GC Roots Tracing的过程。
重新标记是为了修正在并发标记期间因用户程序继续运作而导致标记变动的对象
从整体上说,CMS收集器的内存回收过程是与用户线程一起并发执行的,具体如图:
优点- 并发收集
- 低停顿
缺点
- 对cpu资源非常敏感
- 会有大量空间碎片产生
- 无法处理浮动垃圾
-
-
G1 收集器
这个收集器伴随jdk1.7诞生,是一款面向服务端应用的垃圾收集器。
特点:- 并发与并行
使用多个CPU来缩短Stop-The-world停顿的时间。 - 分代收集
对不同代的对象采用不同的回收方式。 - 空间整合
G1从整体上来看是基于”标记-整理“算法,从局部上来看是基于复制,但是都不会产生内存空间碎片。 - 可预测的停顿
这是G1相对于CMS的另一大优势,G1除了追求低停顿外,还能建立可预测的停顿时间模型,能让使用者明确指定一个在长度为M毫秒的时间片段外,消耗在垃圾收集上的时间不得超过N毫秒。
G1收集器将Java堆划分成多个大小相等的独立区域(Region),并且采用回收价值优先的策咯进行回收。
问题:和其他收集器老生代和新生代中的对象可能相互引用一样,G1收集器中的不同Region之间也可能相互引用,那么是不是在判断某个Region的存活对象的时候还需要遍历整个Java堆才能保证准确性?
在G1收集器中,采用Remembered Set 来避免遍历整个Java堆。
步骤如图:
- 并发与并行
-
-
内存分配与回收策略
- 对象优先在(新生代的)Eden分配
- 大对象直接进入老年代
- 长期存活的对象将进入老年代
- 动态对象年龄判断
- 空间分配担保