首先java虚拟的堆存放着所有的对象实例,那么在进行垃圾回收的时候,如何确定有哪些对象还活着?哪些已经死去了?
引用计数算法
给对象添加一个引用计数器,每当有一个对象引用它的时候,计数器加一,失效的话计数器减一,任何时候计数器为0的对象都不能再被使用。
但是难以解决对象间循环引用的问题。假如两个对象相互引用,双方的引用计数都不为0,于是就没有办法通知GC收集器回收他们。
根搜索算法
通过一系列名为"GC Roots"的对象作为起始点,从这些节点由上往下搜索,搜索所走过的路径称为引用链。
当一个对象到GC Roots没有任何引用链相连的时候,证明此对象是不可用的。
从图论的角度,就是从GC Roots到这个对象是不可到达的
那什么可以作为GC Roots的对象呢?
- 虚拟机栈
- 方法区中类静态属性引用的对象
- 方法区常量引用的对象
- 本地方法栈jni引用(本地方法)的对象
因为5,6,7到Gc Roots是不可到达的,因此会被判定为可回收对象。
Java的引用
- 强引用
Object obj = new Object();
只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象
- 软引用
描述有用但是非必须的对象。在系统将要发生内存溢出异常之前,会把这些对象列进回收范围之中进行第二次回收
- 弱引用
描述非必须对象。只能生存到下一次垃圾收集发生之前。无论当前内存是否足够,都会回收掉被弱引用关联的对象。
- 虚引用
作用:希望能在这个对象被收集器回收的时候,收到一个系统通知。
一个对象是否有虚引用的存在,完全不会对生存时间构成影响,也无法通过虚引用来取得一个对象实例。
生存还是死亡?
- 要真正宣告一个对象的死亡,至少要经历两次标记过程
- 在根搜索算法中不可到达的对象,暂时处于死缓,当根搜索后发现没有与GC Roots相连接的引用链,那么它就会被第一次标记并且进行一次筛选。
- 筛选条件是这个对象是否有必要执行finalize()方法。
- 如果对象没有覆盖finalize(),或者finalize方法已经被虚拟机调用过,那么虚拟机将把这两种情况都视为没有必要执行。
- 如果对象被判定为有必要执行finalize(),对象会被放置在一个叫F-Queue的队列之中。
- 然后虚拟机会自动建立一个低优先级的Finalizer线程,去以此触发队列里的方法。
- GC将对F-Queue中的对象进行第二次小规模的标记,如果这个时候对象重新与引用链上的任何一个对象建立关联,那么在第二次标记的时候它会被移出即将回收的集合。
public class FinalizeEscapeGC {
public static FinalizeEscapeGC SAVE_HOOK = null;
public void isAlive() {
System.out.println("alive!");
}
@Override
protected void finalize() throws Throwable {
super.finalize();
System.out.println("finalize!");
//临死之前,让自己和引用链上的对象再次关联起来
FinalizeEscapeGC.SAVE_HOOK = this;
}
public static void main(String [] args) throws InterruptedException {
SAVE_HOOK = new FinalizeEscapeGC();
//对象第一次成功真就自己
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no I am dead!");
}
//第二次会失败
SAVE_HOOK = null;
System.gc();
Thread.sleep(500);
if (SAVE_HOOK != null) {
SAVE_HOOK.isAlive();
} else {
System.out.println("no I am dead!");
}
}
}
运行结果:
finalize!
alive!
no I am dead!
- 为什么第一次成功了,第二次失败了?
任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行。
新生代与老生代
- Java中堆分为新生代与老生代
- 新生代又分为三个区域:Eden,From Survivor,To Survivor
方法区的回收
方法区与堆一样,都是多个线程共享的,存储着加载的类信息,常量,静态变量以及JIT生成的代码
- 方法区(HotSpot永久代)垃圾回收的效率远比堆中的新生代要低。
- 永久代垃圾收集主要:废弃常量与无用的类
判断废弃常量:没有其他地方引用了这个常量,例如一个字符串"abc"进入了常量池中,但是系统中没有一个String对象叫做"abc".那么如果发生内存回收,常量池中的"abc"就会被"请出常量池"
判断无用的类: 需要满足以下三个条件
- 这个类所有的实例都已经被回收,也就是Java堆中不存在该类的任何实例
- 加载这个类的ClassLoader已经被回收
- 对应的java.lang.Class对象没有在任何地方被引用,不能在任何地方通过反射访问该类的方法
垃圾收集算法
- 标记清除算法
首先要标记出所有需要回收的对象,标记完成后统一回收掉所有被标记的对象.
缺点:
1.效率问题
标记和清除过程效率低
2.空间问题
产生大量不连续内存碎片
复制算法
为了解决效率问题,可以把可用内存按照容量划分为大小相同的两块,每次只使用其中一块.当这一块的内存用完之后,就把还存活着的对象复制到另外一块上面,然后再把已经使用过的内存空间一次清理掉.
优点:
每次都只是对其中一块进行内存回收,内存分配的时候不用考虑内存碎片等复杂情况.只需要移动堆顶指针,按顺序分配内存即可.
缺点:
内存大小将会减半
现在的虚拟机都是采用复制算法来回收新生代,因为新生代大部分对象都会被回收,那么复制的成本会比较低.
因此可以把内存划分为一块比较大的Eden空间和两块比较小的Survivor空间,每次只使用Eden和其中一块Survivor.
回收的时候,把Eden和Survivor还存活的对象一次性拷贝到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间.
哎,那如果另外一块Survivor没有足够空间存放上一次新生代收集下来的存活对象,怎么办?
可以直接通过分配担保机制进入老生代
标记整理算法
让所有的存活对象都向一端移动,然后直接清理掉端边界以外的内存.
比较适合老生代
分代收集算法
- 根据对象存活周期不同把内存划分为几块
- 把Java的堆分为新生代和老生代,新生代使用复制算法,
- 老生代使用标记清楚或者标记整理算法.
- 因为新生代只有少量的存活对象,复制成本比较低
- 而老生代因为对象存活率高,而且没有额外空间提供担保机制
上图的GC收集器,如果两个收集器之间存在连线,说明可以搭配使用.
- Serial 收集器
一个单线程的收集器,在进行垃圾收集的时候,必须先暂停其他所有的工作线程.
是运行在Client模式下的默认新生代收集器
对于单核场景来说,Serial收集器没有线程交互的开销,可以专心做垃圾收集.
- ParNew收集器
是Serial收集器的多线程版本.对于新生代来说,可以用多线程采取复制算法,暂停所有用户线程.对于老生代来说,可以采用标记整理算法暂停所有用户线程.
是运行在Server模式下的默认新生代收集器
但是在单CPU的环境中,不会有比Serial收集器更好的效果,因为存在着线程相互切换的开销.
Parallel Scavenge 收集器
一个新生代的收集器,也是使用复制算法的的收集器,同时也是并行的多线程收集器.
关注点在于达到一个可运行的吞吐量(运行用户代码时间/(运行用户代码时间+垃圾收集时间))
停顿时间越短,就越适合需要与用户交互的程序
而吞吐量大的GC可以高效率地利用CPU时间,尽快完成程序的运算任务.主要适用于后台运算而不需要太多交互的任务.
吞吐量优先的垃圾收集器
CMS收集器
- Concurrent Mark Sweep
以获得最短回收停顿时间为目标的收集器.
比较重视服务的响应速度,希望系统的停顿时间最短
基于标记–清除算法实现
1.初始标记
2.并发标记
3.重新标记
4.并发清除
初始标记,重新标记这两个步骤仍然需要停止其他所有的用户线程.
初始标记仅仅只是标记一下GC Roots能直接关联到的对象.
并发标记:进程GC Roots Tracing的过程
重新标记:为了修正并发标记期间,因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录
并发清除:多线程清除内存
处理并发标记和并发清理这两个耗时最长的过程的时候让gc线程,用户线程交替运行,这样会尽量减少GC线程独占资源的时间.
CMS无法处理浮动垃圾:
由于CMS并发清理阶段用户线程还在运行,伴随着程序的运行还会有新的垃圾不断产生.但是本次垃圾收集是无法处理掉这些垃圾的,只好留到下一次GC把其清理掉.
如果CMS运行期间预留的内存无法满足程序需要,就会出现一次"Concurrent Mode Failure",导致一次Full GC的发生.
而且标记清楚会导致收集结束的时候会产生大量的空间碎片.
G1收集器
- G1收集器是基于"标记整理"算法实现的收集器,不会产生空间碎片
- 可以精确控制停顿
- 能够实现不牺牲吞吐量的前提下完成低停顿的内存回收.之前收集器进行收集的范围都是整个新生代或者老生代,而G1会把整个Java堆划分为多个大小固定的独立区域,然后跟踪这些区域里面的垃圾堆积程度,在后台维护一个priority queue,每次根据允许的收集时间,优先回收垃圾最多的区域