垃圾收集(Garbage Collection ,GC )
前沿:
为什么我们还要去了解GC 和内存分配 Why: ---> 当需要排查内存溢出,内存泄漏问题,垃圾收集成为系统达到更高并发的瓶颈,我们就要对于自动化
的技术实施必要的监控。
GC算法分析(一) 如何判断对象已死
- 引用计数法(很可惜java 不是这种算法)
- 这种算法判断就是对象有无引用问题
/**
* testGC()方法执行后,objA和objB会不会被GC呢?
* @author zzm
*/
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1MB = 1024 * 1024;
/**
* 这个成员属性的唯一意义就是占点内存,以便在能在GC日志中看清楚是否有回收过
*/
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,objA和objB是否能被回收?
System.gc();
}
}
out:
运行结果 发生GC 代表 两个对象被回收 说明jvm 不是采用这种算法
-
可达性分析算法
个算法的基本思路就是通过一系列的称为“GC
Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC
Roots没有任何引用链相连(用图论的话来说,就是从GC
Roots到这个对象不可达)时,则证明此对象是不可用的。如图3-1所示,对象object 5、object 6、object
7虽然互相有关联,但是它们到GC Roots是不可达的,所以它们将会被判定为是可回收的对象。
- 可以作为GCRoots 的对象包括下面几种
- 虚拟机栈(栈帧中的本地变量表)中引用的对象。
- 方法区中类静态属性引用的对象。
- 方法区中常量引用的对象。
- 本地方法栈中JNI(即一般说的Native方法)引用的对象。
GC算法分析(二) 垃圾收集算法
-
标记-清除算法(Mark-Sweep)
- 最基础的垃圾回收算法,分为两个阶段,标注和清除。标记阶段标记出所有需要回收的对象,清除阶段回收被标记的对象所占用的空间。如图:
- 从上图可以看出 标记后会产生大量不连续的内存碎片,这样碎片太多之后需要分配大对象的时候无法找到足够的空间 导致再次GC 。
- 还有一个问题:标记和清除 的效率都不是很高。
-
复制算法
- 为了解决Mark-Sweep算法内存碎片化的缺陷而被提出的算法。按内存容量将内存划分为等大小的两块。每次只使用其中一块,当这一块内存满后将尚存活的对象复制到另一块上去,把已使用的内存清掉,如图:
- 将原有的内存空间分为两块,每次只使用其中一块,在垃圾回收时,将正在使用的内存中的存活对象复制到未使用的内存块中,之后,清除正在使用的内存块中的所有对象,交换两个内存的角色,完成垃圾回收
- 缺点 空间浪费一般 空间 不够时 向 老年代申请空间(贷款)
-
标记-整理算法(Mark-Compact)
- 为了避免缺陷而提出。标记阶段和Mark-Sweep算法相同,标记后不是清理对象,而是将存活对象移向内存的一端。然后清除端边界外的对象。如图:
-
分代收集算法(Generational Collection)
一般情况下将GC堆划分为老生代(Tenured/Old Generation)和新生代(Young Generation)。老生代的特点是每次垃圾回收时只有少量对象需要被回收,新生代的特点是每次垃圾回收时都有大量垃圾需要被回收,因此可以根据不同区域选择不同的算法。
目前大部分JVM的GC对于新生代都采取Copying算法,因为新生代中每次垃
圾回收都要回收大部分对象,即要复制的操作比较少,但通常并不是按照1:
1来划分新生代。一般将新生代划分为一块较大的Eden空间和两个较小的Surv
ivor空间(From Space,ToSpace),每次使用Eden空间和其中的一块Survivor空间,当进行回收时,
将该两块空间中还存活的对象复制到另一块Survivor空间中。
- 一般 finalize 在 对象进行中 可能会执行一次 一般对象只会进行一次执行finalize() 方法 所以可以再finalize()进行对象自救 但是由于 只会执行一次 第二次GC 来临就不执行了 有点类似
标记算法
第一次标记可能会执行 flialize()方法(为什么可能? 因为JVM 有个 F-Queue队列 他会触发这个执行但是不会保证这个执行结束 防止 某个进入死循环 卡死整个垃圾回收,当没有覆盖过finalize()方法或者已经被调用过 JVM 也不会执行第二遍 )
例子:
/**
* 此代码演示了两点:
* 1.对象可以在被GC时自我拯救。
* 2.这种自救的机会只有一次,因为一个对象的finalize()方法最多只会被系统自动调用一次
* @author zzm
*/
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 mehtod executed!");
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String[] args) throws Throwable {
SAVE_HOOK = new FinalizeEscapeGC();
//对象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
// 下面这段代码与上面的完全相同,但是这次自救却失败了
SAVE_HOOK = null;
System.gc();
// 因为Finalizer方法优先级很低,暂停0.5秒,以等待它
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no, i am dead :(");
}
}
}
out:
finalize mehtod executed
yes, i am still alive :)
no, i am dead :(
-
经验:避免使用finalize(),操作不慎可能导致错误。优先级低,何时被调用, 不确定何时发生GC不确定可以使用try-catch-finally来替代它
典型的垃圾收集器
垃圾收集器名字 | 介绍 |
---|---|
Serial(串行GC)收集器 | Serial收集器是一个新生代收集器,单线程执行,使用复制算法。它在进行垃圾收集时,必须暂停其他所有的工作线程(用户线程)。是Jvm client模式下默认的新生代收集器。对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。 |
ParNew(并行GC)收集器 | ParNew收集器其实就是serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为与Serial收集器一样。 |
Parallel Scavenge(并行回收GC)收集器 | Parallel Scavenge收集器也是一个新生代收集器,它也是使用复制算法的收集器,又是并行多线程收集器。parallel Scavenge收集器的特点是它的关注点与其他收集器不同,CMS等收集器的关注点是尽可能地缩短垃圾收集时用户线程的停顿时间,而parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。吞吐量= 程序运行时间/(程序运行时间 + 垃圾收集时间),虚拟机总共运行了100分钟。其中垃圾收集花掉1分钟,那吞吐量就是99%。 |
Serial Old(串行GC)收集器 | Serial Old是Serial收集器的老年代版本,它同样使用一个单线程执行收集,使用“标记-整理”算法。主要使用在Client模式下的虚拟机。 |
Parallel Old(并行GC)收集器 | Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。 |
CMS(并发GC)收集器 | CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记-清除”算法实现的,整个收集过程大致分为4个步骤:①.初始标记(CMS initial mark)②.并发标记(CMS concurrenr mark)③.重新标记(CMS remark)④.并发清除(CMS concurrent sweep) |
G1收集器 | G1(Garbage First)收集器是JDK1.7提供的一个新收集器,G1收集器基于“标记-整理”算法实现,也就是说不会产生内存碎片。还有一个特点之前的收集器进行收集的范围都是整个新生代或老年代,而G1将整个Java堆(包括新生代,老年代)。 |
- 根据不同代的特点,选取合适的收集算法
- 少量对象存活,适合复制算法
- 大量对象存活,适合标记清理或者标记压缩
Stop-The-World
- Java中一种全局暂停的现象
- 全局停顿,所有Java代码停止,native代码可以执行,但不能和JVM交互
- 多半由于GC引起
- Dump线程
- 死锁检查
- 堆Dump
- GC时为什么会有全局停顿?
- 类比在聚会时打扫房间,聚会时很乱,又有新的垃圾产生,房间永远打扫不干净,只有让大家停止活动了,才能将房间打扫干净。
- 危害
长时间服务停止,没有响应
遇到HA系统,可能引起主备切换,严重危害生产环境。
其他知识点
- 大对象直接进入老年代
- 长期存活对象进入老年代(对象从 eden 区 到 survivor(幸存者区) 每次GC 如果躲过一次GC的话,其对象年龄便会加1,默认情况下,如果对象年龄达到15岁,就会移动到老年代中。)
- 空间担保 之前说过 因为新生代使用复制算法 (由于新生代很多都是朝生夕死的 百分之98 都是, 所有是这种算法 ,但是 如果 Minor GC 后 大量对象 还继续存活 需要进行老年代担保 借用空间 如果老年代不足够 就会 启动Full GC 让老年代 腾出空间)
参考
- 《深入理解JAAVA虚拟机》、
- Java虚拟机学习 - 垃圾收集器