介绍
垃圾回收需要做的三件事:1.那些内存需要回收;2.什么时候需要回收;3.如何回收。
如何判断对象可以回收?
引用计数法
即对象在被创建时,添加一个引用计数器,每当有一个地方引用它时,计数器+1,引用失效时,计数器-1,任何时刻计数器为0的对象就是不可能再被使用的(是可以被回收还是不可能在被使用,不确定)。优点:算法实现简单,判定效率高,在大部分情况下是个不错的算法,但是当前主流的虚拟机都没有选用引用算法来管理内存,最主要的缺点就是很难解决循环引用的问题。如下面的代码,两个对象循环依赖了,相当于出于一种死锁的状态,计数器永远不可能为0.
class ObjectClass{ public Object instance = null; } ObjectClass o1 = new ObjectClass(); ObjectClass o2 = new ObjectClass(); o1.instance=o2; o2.instance=o1;
可达性分析算法
在如今主流的商业程序语言中(c#,java)的主流实现中,都是通过可达性分析来判断对象是否存活。通过一系列被成为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索过的路径称为引用链(Refence Chain),当一个对象到GC Roots没有任何的引用链相连接(在图论中称为GC Roots 到这个对象不可达),则证明此对象是不可用的,可以回收。(入下图,object5和object6两个对象都是不可用的)
在Java中,可作为GC Root的对象包括以下几种:
- 虚拟机栈(栈帧中的本地变量表)中引用的对象
- 方法区中类静态属性引用的对象
- 方法区中常量引用的对象
- 本地方法栈中JNI(即一般说的Native方法)引用的对象
注**************:
即使是通过可达性分析算法中不可达的对象,也不是一定被回收。对象被回收,还需要经历两次标记过程。
1.如果对象通过可达性分析算法判断对象为不可达,那将被第一次标记,并第一次筛选,筛选的条件是是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者已经被虚拟机调用过,都视为没有必要执行。反之,则判定有必要执行finalize()方法,并将该对象放置到F-Queue队列中。
2.虚拟机创建的,低优先级的Finalizer线程执行(触发对象的finalize方法),但是不会承诺等到它运行结束,原因是如果对象在finalize方法中执行缓慢,或者发生了死循环,又可能导致F-Queue队列中其他对象出于永久等待。稍后GC就会对F-Queue中的对象进行第二次小规模的标记,这一部分才会被正在的回收。(在这个过程中,对象时可以进行自救的,主要重新与引用链上任何的一个对象建立关联即可,如果吧自己(this关键字)赋值给某个类变量或者对象的成员变量,在第二次标记时,他就会被清除,避免被回收)
垃圾回收的算法
标记-清除算法
算法分为标记和清除两个过程,第一个过程在上面介绍的可达性分析中介绍,标记需要回收的对象,在标记完成后,统一回收别标记的所有对象。但是有两个极大的确定1.效率问题:标记和清除的过程效率都不高 2:清除过后,由于内存空间的不连续会产生大量的内存碎片,导致的后果是在之后分配较大对象的时候,无法找到足够的连续内存从而导致提前触发另一次的垃圾回收。
复制算法
将内存安装容量划分为大小两块的内存空间,每次有且仅适用其中的一块,当这一块使用完之后,将还存活的对象复制到另外的一块上面,然后将已使用的内存空间一次清理,这样每次回收之后都不会产生内存碎片,缺点就是可用内存只有分配的一半。
注:如今商业虚拟机都采用这种收集算法。主要用于青年代的堆内存回收,将青年代分为Eden和两块较小的Survivor区域,每次使用Eden和其中一块Survivor,当回收时,将Eden和Survivor还存活的对象一次性复制到另外一块Survivor中,最后清理掉Eden和之前的Survivor区。
标记-整理算法
标记过程同上面一样,但是后续步骤不是直接对可回收的对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存,主要用于老年代的回收
注:这里不介绍分代收集算法,分代收集主要是青年代采用复制算法,老年代主要采用标记-整理算法
垃圾收集器
如图展示了7种作用于不同分代的收集器,如果两个收集器之间存在连线,就说明他们可以搭配使用。上面表示青年代,下面表示老年代。(没有最好的收集器,只有最适合的收集器)
Serial收集器
Serial收集器是单线程收集器,但是这里的“单线程”的意思不仅仅说明他只会使用一个CPU或者一条收集线程去完成垃圾手机工作。更加重要的是它在垃圾回收的时候,必须要停止其他所有的工作线程(Stop The World)。但是简单高效,对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾回收。
Serial Old收集器
Serial Old 就是Serial收集器的老年代版本,唯一的区别就是一个垃圾回收算法采用复制算法,一个标记-整理算法。
ParNew 收集器
ParNew收集器就是Serial收集器的多线程版本。除了使用多线程进行垃圾收集之外,其他都和Serial一样
ParNew Old收集器
略。。。。。。
Parallel Scavenge收集器
Parallel Scavenge收集器是一个新生带收集器,也是采用复制算法,也是多线程并行,这些和ParNew一暗影。它的特点在于,该收集器的目标是达到一个可控制的吞吐量。所谓吞吐量 = 运行用户代码的时间 / (垃圾回收时间 + 运行用户代码时间),比如虚拟机运行总共花了100分钟,垃圾回收消耗1分钟,吞吐量 = 99%。
该收集器提供两个参数: -XX:MaxGCPauseMillis 控制最大垃圾收集时间 和 -XXGCTimeRatio 直接设置吞吐量的大小。GC停顿时间越短是通过牺牲吞吐量和新生代空间换来的(比如新时代空间越小,肯定回收时间就越短,或者全部10分钟出发一次停顿10S ,变成了1分钟触发一次停顿5S),所以需要合理的设置GC停顿时间。
CMS收集器
在介绍CMS收集器之前介绍两个概念:
并行:指多条垃圾收集线程并行工作,但是用户线仍然处于等待状态
并发:只用户线程和垃圾收集线程同时执行(但是不一定是并行的,可能是相互交替运行)
CMS收集器是一种以获取最短回路停顿时间为目标的收集器。是目前老年代中比较优秀的垃圾收集器。
从图中可以看出,CMS收集器的工作过程可以分为4个阶段:
- 初始标记(CMS initial mark)阶段
- 并发标记(CMS concurrent mark)阶段
- 重新标记(CMS remark)阶段
- 并发清除(CMS concurrent sweep)阶段
从图中可以看到,在初始标记和重新标记过程中,用户线程是处于等待状态的。初始标记阶段的工作是标记GC Roots可以直接关联到的对象,速度很快。并发标记阶段,会从GC Roots 出发,标记处所有可达的对象,这个过程可能会花费相对比较长的时间,但是由于在这个阶段,GC线程和用户线程是可以一起运行的,所以即使标记过程比较耗时,也不会影响到系统的运行。重新标记阶段,是对并发标记期间因用户程序运行而导致标记变动的那部分记录进行修正,重新标记阶段耗时一般比初始标记稍长,但是远小于并发标记阶段。最终,会进行并发清理阶段,和并发标记阶段类似,并发清理阶段不会停止系统的运行,所以即使相对耗时,也不会对系统运行产生大的影响。
缺点:1.对于CPU资源敏感,对于并发实现的收集器而言,虽然可以利用多核优势提高垃圾收集的效率,但是由于收集器在运行过程中会占用一部分的线程,这些线程会占用CPU资源,所以会影响到应用系统的运行,会导致系统总的吞吐量降低。CMS默认开始的回收线程数是(Ncpu + 3) / 4,其中Ncpu是机器的CPU数。所以,当机器的CPU数量为4个以上的时候,垃圾回收线程将占用不少于%25的CPU资源,并且随着CPU数量的增加,垃圾回收线程占用的CPU资源会减少。
2.CMS收集器在处理垃圾收集的过程中,可能会产生浮动垃圾,由于它无法处理浮动垃圾,所以可能会出现Concurrent Mode Failure问题而导致触发一次Full GC。所谓的浮动垃圾,是由于CMS收集器的并发清理阶段,清理线程是和用户线程一起运行,如果在清理过程中,用户线程产生了垃圾对象,由于过了标记阶段,所以这些垃圾对象就成为了浮动垃圾,CMS无法在当前垃圾收集过程中集中处理这些垃圾对象。
G1收集器
垃圾回收时间
一个对象实例化时 先去看伊甸园有没有足够的空间
如果有 不进行垃圾回收 ,对象直接在伊甸园存储.
如果伊甸园内存已满,会进行一次minor gc
然后再进行判断伊甸园中的内存是否足够
如果不足 则去看存活区的内存是否足够.
如果内存足够,把伊甸园部分活跃对象保存在存活区,然后把对象保存在伊甸园.
如果内存不足,向老年代发送请求,查询老年代的内存是否足够
如果老年代内存足够,将部分存活区的活跃对象存入老年代.然后把伊甸园的活跃对象放入存活区,对象依旧保存在伊甸园.
如果老年代内存不足,会进行一次full gc,之后老年代会再进行判断 内存是否足够,如果足够 同上.
如果不足 会抛出OutOfMemoryError.