JVM垃圾收集与垃圾收集器

垃圾收集(GC)主要作用在于内存的回收,而GC要思考的三件事情:

1,哪些内存需要回收?

2,什么时候回收?

3,怎么回收?

在java运行时的内存区域中的程序计数器、虚拟机栈、本地方法栈,这三个区域因为是与线程绑定的,线程生而生,线程灭而灭,当线程启动就分配内存,当线程销毁就回收内存,所以这三个区域的内存分配和回收都是确定的。

所以要考虑的主要是java堆和方法区:其中,方法区的回收比较困难

一、java堆内存的回收:

java堆内存的回收,其实就是对象实例的回收,那么先考虑第一步

(一)哪些对象已死?(判断什么对象已死需要内存回收)

(1)第一步:判断对象是否已死的算法

1.可达性分析算法

在java主流的虚拟机中使用的都是可达性分析算法来判断对象是否存活,这个算法的思想就是以一系列称为“GC Roots”的对象作为起始点,向下搜索,搜索走过的路径称为引用链,如果一个对象到GC Roots没有任何的引用链,那么,这个对象就会被判断为可回收的。


图中的object 5 6 7都是可回收的,没有与GC Roots相关联

可以作为GC Roots的对象有:

1.虚拟机栈中引用的对象(栈帧中的本地变量表)中引用的对象

2.方法区中类静态变量引用的对象

3.方法区中的常量引用的对象

4.本地方法栈中的JNI(由Native修饰的方法)引用的对象

PS:引用记数法,在java有一个误区就是有人会认为垃圾收集器是使用引用记数法来判断对象是否存活,引用记数法的思想是当有一个地方引用这个对象,计数器就加一,当引用失效后,计数器减一,当计数器为0时就认为这个对象是不可用的,可以回收。但是在java虚拟机中是不使用这种算法的,因为这种算法很难解决对象之间互相循环引用的问题。

(二)第二步,判断对象是否要进入回收集合(是否有自救)

当对象在可达性分析算法中判断为不可达时,这时候的对象也不是一定会被回收,虚拟机会对其进行第一次标记和筛选,筛选的条件是对象是否有必要执行finalize()方法,如果对象没有覆盖finalize()方法或者finalize()方法已经被虚拟机调用过了,那么对象将直接被判定为要回收的对象。

如果对象被判断为有必要执行finalize()方法(未满足上面的筛选条件),那么这个对象将会被放置到一个称为F-Queue的队列之中,并且在稍后虚拟机会创建一个Finalizer线程去专门执行这个对象的finalize()方法,这也是对象唯一的一次自救机会,只要在覆盖的finalize方法中将自己与任何一个对象关联起来,比如把自己(this)赋值给某个类或者成员变量,那么虚拟机将在第二次标记时将他移出要被回收的集合中。如果没有,那么这个对象就基本真的被回收了。

(三)第三步,回收的方式(垃圾收集算法)

目前主流有三种回收的算法

1.标记-清除算法

这种算法的思想很简单,就是对要回收的对象进行标记,在标记完成后,统一对标记的对象进行内存回收。这种算法有两个弊端:一个是效率问题,另一个是空间利用问题,在回收后内存中有含有大量的不连续的内存空间。空间碎片太多会导致后面如果有一个大的对象需要内存分配可能会找不到合适的连续内存空间而不得不再次触发这个算法来进行内存回收。这是一个最基础的回收算法,后续的算法都是建立在其基础之上,改进这个算法的不足之处。

2.复制算法:

这种算法的思想是,将内存分为两块,在分配内存时,只使用其中的一块,当要回收内存时,将存活的对象都复制到另一块未被使用的内存区域中,只要移动堆顶指针,按顺序分配内存空间即可,不用考虑内存碎片等情况。运行高效简单,但是要牺牲一半的内存空间。

现在的商业虚拟机都是使用这种算法来回收新生代,IBM公司的研究表明。新生代中的98%的是朝生夕死,所以使用复制算法时,并不是将内存平均的分位两块,而是分为一块较大的区域和两块较小的区域,较大的区域称为Eden(伊甸园),较小的称为Survivor(幸存者)。平时只在Eden和一块Survivor中分配内存,当回收内存时,将存活的对象复制到另一块没有使用的Survivor中。其中,Eden和Survivor的内存比例为8:1,这样的话新生代的内存使用率就有90%。当然,并不能保证每次内存回收时只有不超过内存空间10%的对象存活,所以我们需要依赖其他内存来进行分配担保,当新生代需要的内存空间超过10%时,会通过分配担保机制进入老年代,保证不会出现内存溢出的异常。

3.标记-整理算法

这种算法的思想和标记清除算法很像,但是在标记之后并不是直接进行统一的内存回收,而是将存活的对象都向一端移动,然后直接将存活对象边界以外的内存进行清除。这种算法的好处在于不用需要额外的内存控件去进行分配担保,但是移动对象则需要一定的系统开销。

因为老年代的存活率较高,不适合使用复制算法,所以在老年代适合采用标记-整理算法。

4.分代整理算法

当前的商业虚拟机的垃圾收集都是采用这个算法,这个算法只是将上面的算法综合运用起来。将java堆分为新生代和老年代,新生代因为对象存活率低,所以采用复制算法;老年代对象存活率高,所以采用标记-清除算法或者标记-整理算法。

二、各大垃圾收集器的实现原理

上面介绍了垃圾收集的理论方法,

下面来说真实应用中的垃圾收集器。目前是没有一个万能的最好的垃圾收集器,我们只能根据不同的需求去选择不同的垃圾收集器。

(1)Serial收集器

这是一个最基本的,发展历史最悠久的垃圾收集器。这是一个单线程的垃圾收集器,只会使用一个cpu,只使用一条线程去进行垃圾收集,并且Serial在进行垃圾收集时要暂停所有在运行的程序(stop the world),直到gc结束。这就会在用户不可见的情况下有一个停顿时间,降低了用户的体验。

虽然说现在有更多的垃圾收集器可以降低这个停顿时间,用户线程和垃圾收集线程的并发,但是在单核cpu限定的情况下,Serial的简单高效,在客户机模式下仍是一个不错的选择。

(2)ParNew

这个其实就是Serial的多线程版,在进行gc时,仍需要暂停运行中的线程。在单核cpu与通过超线程的双cpu的情况下,parnew的gc效率并不能确保超过Serial的效率,因为存在了线程交互的开销。但是在如今的多核cpu的服务器上,它的效率还是非常好的。默认开启与cpu数量相同的线程去进行gc。

(3)Parallel Scavenge

这个收集器的与ParNew类似,但是它多了一个重要的功能,可以控制的吞吐量(程序运行的时间/程序运行的时间+GC的时间)。可以自行设置吞吐量,也可以通过一个参数去开启GC自适应调节策略,将内存管理交给虚拟机,我们只要设置堆的最大值,然后让虚拟机选择更加关注停顿时间或者是吞吐量。这个收集器无法与老年代的CMS配合工作。

(4)Serial old

Serial old是Serial收集器的老年代版,也是单线程,采用“标记-整理”算法。

(5)Parallel old

Parallel old是Parallel Scavenge的老年代版,使用多线程和“标记-整理算法”。

(6)CMS

CMS是老年代的收集器,更关注于低停顿时间,使用的是“标记-清除”算法。它的GC分为以下几个阶段

1.初始标记,这个阶段需要“stop the world”,但是速度很快,仅仅是标记一下能与GC Roots能直接关联的对象。

2.并发标记,这个阶段就是进行可达性分析的阶段,这个阶段比较耗时,但是这个阶段可以与程序一起并发,与上一阶段的标记有关。

3.重新标记,这个阶段也需要“stop the world”,主要用于修正和标记一些因为并发标记时,程序运行时产生变动的关联标记,比较耗时,但远低于并发标记的时间。

4.并发清除,这个阶段可以与其他程序一起并发,因为经过三个阶段,要清理的对象和不需要清理的对象已经明了,所以直接清除标记的对象即可。

因为CMS从总体上是并发的,所以它的停顿时间就减少了很多。

使用“标记-清除”算法的弊端就是会产生大量的空间碎片,如果当一个大的对象要分配内存而找不到合适的连续内存空间,那么CMS就会进行一次内存整理,这会增加程序的停顿时间

另外一个弊端就是因为GC线程和Java程序是并发的,所以当CMS在GC时,可能程序还在另外一边产生新的垃圾,而这部分垃圾只能留到下一次的GC去处理,所以CMS需要预留一定的内存空间去给java程序使用,不能在老年代的空间在快没有的时候才进行GC。

(6).G1收集器(目前使用的还比较少,未来可能是主流)

G1收集器是当前收集器的最新发展前沿陈果之一,使用了G1收集器就不再需要为新生代和老年代分别设定一个收集器,在G1中java堆不在分为新生代和老年代的内存,而是多个相等大小的独立区域(Region),新生代和老年代不再是物理隔离。

它具有以下几个特点。

1.并行与并发:类似于CMS,利用多核CPU来缩短停顿时间。

2.分代收集:在G1中,分代收集的思想得以保留,它能用不同的方式去处理新创建的对象,和存活很久,熬过多次GC的对象。

3.空间整合:在G1中,从整体上来看,收集器是基于“标记整理”算法的,从局部上看(两个Region),收集器是基于“复制”算法的。所以,G1在收集过程是不会产生空间碎片的。

4.可以预测的停顿时间。G1可以预测程序的停顿时间,并根据用户的意愿来修改时间。

由于Region中的对象可以和整个java堆中的对象引用,所以为了避免在GC时的可达性分析中要扫描整个Java堆(太慢了),G1会在每一个Region中都创建一个Remembered Set,当虚拟机发现程序在对Region中的Reference类型的数据进行写操作时,会发出一个“Write Barrier”(写入屏障)来暂停程序的写操作,并检查Reference引用的对象是否在本Region中,如果不在,通过CardTable来进行记录被引用的对象所属的Region的Rememberd Set中。这样GC在根节点的枚举时,将Rememberd Set加入进去,就不用扫描整个Java堆了。

G1的运作阶段大致可以分为下面几个步骤:

1.初始标记

2.并发标记

3.最终标记

4.筛选收回

其中,前三个阶段与CMS类似,第四个阶段则是虚拟机将各个Region的回收价值和成本进行排序,根据用户定制的可停顿时间来制定回收策略。

你可能感兴趣的:(JVM垃圾收集与垃圾收集器)