一、概述
前面我们已经知道,栈的内存是固定的,栈帧多大都是已知。而堆就不一样了。
二、判断对象存活状态
垃圾回收的第一件事就是判断对象是否还活着,是否可以回收。
1.引用计数法
- 给对象添加一个引用计数器,被引用时加一,失效时减一,计数器为0就不能被使用了。这种方法无法解决相互引用问题。
public class ReferenceCountingGC {
public Object instance = null;
private static final int _1M = 1024 * 1024;
//很大的内存
private byte[] bigSize = new byte[2 * _1M];
public static void main(String[] args) {
testGC();
}
public static void testGC() {
ReferenceCountingGC obj1 = new ReferenceCountingGC();
ReferenceCountingGC obj2 = new ReferenceCountingGC();
obj1.instance = obj2;
obj2.instance = obj1;
obj1 = null;
obj2 = null;
System.gc();
}
}
这里的日志信息: TODO 日志分析,gc文件查看
4603k -> 210k
意味着并没有因为相互引用就不回收,说明虚拟机并不是通过引用计数实现的。
2. 可达性分析算法
- 主流语言都是通过可达性分析实现垃圾回收。就是通过一系列GCRoots作为七点,判断有没有被引用。如果一个对象到GCRoots没有引用链,用图论的语言就是不可达,那么可以回收。
- java的GCRoots包括 虚拟机栈(栈帧的本地变量表)引用的对象。、方法区中类静态属性引用的对象、方法区中常量引用的对象,本地方法(native方法)引用的对象。
3.理解引用
- 引用实际上很重要,上面的两张方法都是通过引用来判断。如果reference类型的数据中存储的数值代表另一个内存的起始地址,就称这块内存代表一个引用。
- 一个对象在这种定义下只有引用和被引用两种关系。所以这个定义太过狭隘。
- jdk1.2后java对引用的概念进行了扩充。分为强引用、软引用、弱引用、虚引用。
- 四种引用相关的只是可以查询资料。
4.最后的判断
- 即使判断为不可达对象,也只是处于“缓刑”阶段。要彻底宣告死亡还得经过两次判断。第一次标记后进行筛选,筛选条件是该对象是否有必要执行finalize方法,当对象没有覆盖finalize方法或者已经被调用过,将会被标记为没必要执行。
- 如果被标记为有必要执行,会被放在一个F-Queue中,稍后放在一个虚拟机级别的线程中执行这个finalize,但是不会等待它执行。
- finalize是对象拯救自己最后的机会,只要把自己引用到某个对象即可。但是每个对象的finalize只会调用一次,所以下面的代码拯救自己一次。第二次失败了。
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive(){
System.out.println("yes , i am 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 Exception {
SAVE_HOOK = new FinalizeEscapeGC();
// 对象第一次成功拯救自己
SAVE_HOOK = null;
System.gc();
//finalize 优先级低,等待执行
Thread.sleep(1000);
if (null != SAVE_HOOK) {
SAVE_HOOK.isAlive();
}else {
System.out.println("no, i am dead");
}
//对象第二次拯救自己
SAVE_HOOK = null;
System.gc();
//finalize 优先级低,等待执行
Thread.sleep(1000);
if (null != SAVE_HOOK) {
SAVE_HOOK.isAlive();
}else {
System.out.println("no, i am dead");
}
}
}
这种方式太不推荐了。
5.回收方法区
- 很多人认为方法去是没有垃圾回收的,其实只是方法区垃圾回收效率低。
- 永久代的垃圾回收主要是两部分,一部分是常量,另一部分是无用的类。废弃常量和堆的回收很类似,但是判断一个类是否是无用的类,就比较麻烦。
- 无用的类判断条件是:所有实例都被回收了,ClassLoader被回收了,无法在任何地方通过反射得到该类的方法。
- 在大量使用反射、动态代理、CGlib等技术的地方,频繁自定义classLoader的地方都要虚拟机具备类卸载的功能,保证永久代不溢出。
三、垃圾回收算法
1.标记-清楚
对需要回收的对象标记,然后回收。标记和清除效率都不高,而且容易产生碎片。
2.复制算法
将空间分为相同的两块,每次回收后移动到另一边。存活率较高时效率低,另外浪费一半空间
3.标记-整理
标记然后移动。
4.分代回收
对不同对象用不同方法。
四、算法实现
TODO
五、常见垃圾收集器
1.TODO
2.阅读GC日志
gc 参数:
JVM的GC日志的主要参数包括如下几个:
-XX:+PrintGC 输出GC日志
-XX:+PrintGCDetails 输出GC的详细日志
-XX:+PrintGCTimeStamps 输出GC的时间戳(以基准时间的形式)
-XX:+PrintGCDateStamps 输出GC的时间戳(以日期的形式,如 2013-05-04T21:53:59.234+0800)
-XX:+PrintHeapAtGC 在进行GC的前后打印出堆的信息
-Xloggc:../logs/gc.log 日志文件的输出路径
日志:
0.756: [Full GC (System) 0.756: [CMS: 0K->1696K(204800K), 0.0347096 secs] 11488K->1696K(252608K), [CMS Perm : 10328K->10320K(131072K)], 0.0347949 secs] [Times: user=0.06 sys=0.00, real=0.05 secs]
5.617: [GC 5.617: [ParNew: 43296K->7006K(47808K), 0.0136826 secs] 44992K->8702K(252608K), 0.0137904 secs] [Times: user=0.03 sys=0.00, real=0.02 secs]
解释如下:
5.617(时间戳): [GC(Young GC) 5.617(时间戳): [ParNew(使用ParNew作为年轻代的垃圾回收器): 43296K(年轻代垃圾回收前的大小)->7006K(年轻代垃圾回收以后的大小)(47808K)(年轻代的总大小), 0.0136826 secs(回收时间)] 44992K(堆区垃圾回收前的大小)->8702K(堆区垃圾回收后的大小)(252608K)(堆区总大小), 0.0137904 secs(回收时间)] [Times: user=0.03(Young GC用户耗时) sys=0.00(Young GC系统耗时), real=0.02 secs(Young GC实际耗时)]
第一个是时间戳,然后是GC或者FullGC代表垃圾回收类型。然后中括号括起来的是年轻代垃圾回收,第一个是垃圾回收器,例如:DefNew,PSYoungGen等,然后是年轻代的容量变化和总用量。中括号外的是堆的总容量。 后面三个是时间,分别是user,sys,real。分别是用户态CPU时间、内核态CPU时间、墙钟时间,墙钟时间包含各种非运算的等待耗时,例如IO阻塞,CPU时间则不包含这些世界,当计算机是多核这些世界会累加,所以看到real或者sys超过real也正常。