关于Java垃圾回收器(GC)的知识整理

这两天整理了一下Java中有关垃圾回收器(GC)的知识,在这里写篇blog总结一下。

在进入垃圾回收器的内容之前,我们先来回顾一下Java的内存模型。JVM将内存区域区分成下述几种类型:程序计数器区,栈,堆,方法区,常量池和直接内存区:
1.程序计数器区:可以简单理解成存放指向当前执行代码行的指针的区 域。
2.栈:被划分为虚拟机栈、本地方法栈和操作数栈三部分,不过需要注意的是HotSpot将虚拟机栈和本地方法栈合并实现。在函数中定义的一些基本类型的变量数据和对象的引用变量都在函数的栈内存中分配。
3.堆:堆内存用来存放由new创建的对象和数组,并且被所有线程共享。在堆中内存大小是在运行时动态分配的,生存期也不必事先告诉编译器。在堆中分配的内存,由Java虚拟机的自动垃圾回收器来管理,因而堆是这篇文章重点关注的对象。
4.方法区:方法区跟堆一样,被所有的线程共享。用于存储虚拟机加载的类信息、常量、静态变量、即时编译器(JIT)编译后的代码等数据。
5.常量池:常量池指的是在编译期被确定,并被保存在已编译的.class文件中的一些数据。除了包含代码中所定义的各种基本类型(如int、long等等)和对象型(如String及数组)的常量值(final)还包含一些以文本形式出现的符号引用,比如:类和接口的全限定名; 字段的名称和描述符; 方法和名称和描述符。虚拟机必须为每个被装载的类型维护一个常量池。值得注意的是,在jdk7(可能有误)开始,在程序执行时,常量池会被放置于堆中,这与之前放置在方法区中不同。

好的,聊完了Java的内存模型,下面进入正餐——垃圾回收器。众所周知,内存管理一直是令众多C、C++程序员们为之头疼烦恼的问题,为了解决这一问题,Java中引入了垃圾回收机制,它对堆中已分配出去的内存进行管理,能够自动回收无用的垃圾,避免内存泄露的发生。那么实现垃圾回收的原理是什么呢?其实,对任何一种垃圾回收算法来说,其主要完成的工作有两个,一是发现无用信息对象,二是回收被无用对象占用的内存空间,使该空间可被程序再次使用。接下来开始揭开垃圾回收算法的神秘面纱~~~
1.引用计数法(Reference Counting Collector):这是一种简单但速度很慢的垃圾回收技术。每个对象都含有一个引用计数器,当有引用连接至对象时,引用计数+1,当引用离开作用域或被置为null时,引用计数-1,垃圾回收器会在含有全部对象的列表上遍历,当发现某个对象的引用计数为0时便释放其占用的空间。这种方法有个缺陷,如果对象之间存在循环引用,可能会出现“对象应该被回收但是引用计数不等于0”的情况,例如

public class Main{
        public static void main(String[] args){
                MyObject obj1 = new MyObject();
                MyObject obj2 = new MyObject();
                obj1.object = obj2;
                obj2.object = obj1;
                obj1 = null;
                obj2 = null;
    }
}

这里在最后将obj1和obj2赋值为null,也就是说obj1和obj2指向的对象已经不可能再被访问,应该被回收,但是由于它们互相引用对方,导致它们的引用计数器都不为0,所以垃圾收集器就不会回收它们,这样便发生了内存泄露。
2.标记-清除算法(mark and sweep):采用从根集合进行扫描,对存活的对象标记,标记完毕后,再扫描整个空间中未被标记的对象,进行回收,如下图所示,
标记-清除算法示意图
这种算法在存活对象比较多的情况下更为高效,不过它只负责清理无用内存,并不负责对存活对象进行移动整理,故有可能造成内存碎片。
3.标记-整理算法:其实就是在标记-清除算法的基础上,对未被清理的存活对象进行移动整理,详细见下图,
标记-整理算法示意图
由于多了对内存的整理工作,所以这种算法的成本高,但是解决了内存碎片化的问题。
4.copying算法(Compacting Collector):copying算法把堆分成一个对象面和多个空闲面,程序从对象面为对象分配空间,当对象满了,垃圾收集器就从根集中扫描活动对象,并将每个活动对象复制到空闲面,这样空闲面变成了对象面,原来的对象面变成了空闲面,程序会在新的对象面中分配内存。
copying算法示意图
该算法克服了句柄的开销和解决了堆碎片的垃圾回收,一种最典型的实现便是停止-复制(stop-and-copy)算法,它将堆分成对象面和空闲区域面,在对象面与空闲区域面的切换过程中,程序暂停执行。
5.generation算法(Generational Collector):分代的垃圾回收策略,是基于这样一个事实:不同的对象的生命周期是不一样的。因此,针对不同生命周期的对象可以采取不同的回收算法,以便提高回收效率。该算法将堆内存区域划分为年轻代,年老代和持久代。
分代算法
年轻代(Young Generation)
(1)所有新生成的对象首先都是放在年轻代的。
(2)新生代内存按照8:1:1的比例分为一个eden区和两个survivor(survivor0,survivor1)区。大部分对象在Eden区中生成。回收时先将eden区存活对象复制到一个survivor0区,然后清空eden区,当这个survivor0区也存放满了时,则将eden区和survivor0区存活对象复制到另一个survivor1区,然后清空eden和这个survivor0区,此时survivor0区是空的,然后将survivor0区和survivor1区交换,即保持survivor1区为空, 如此往复。
(3)当survivor1区不足以存放 eden和survivor0的存活对象时,就将存活对象直接存放到老年代。若是老年代也满了就会触发一次Full GC,也就是新生代、老年代都进行回收。
(4)新生代发生的GC也叫做Minor GC,MinorGC发生频率比较高(不一定等Eden区满了才触发)。
年老代(Old Generation)
(1)在年轻代中经历了N次垃圾回收后仍然存活的对象,就会被放到年老代中。因此,可以认为年老代中存放的都是一些生命周期较长的对象。
(2)内存比新生代也大很多(大概比例是1:2),当老年代内存满时触发Major GC即Full GC,Full GC发生频率比较低,老年代对象存活时间比较长,存活率标记高。
持久代(Permanent Generation):用于存放静态文件,如Java类、方法等。持久代对垃圾回收没有显著影响,但是有些应用可能动态生成或者调用一些class,例如Hibernate 等,在这种时候需要设置一个比较大的持久代空间来存放这些运行过程中新增的类。值得注意的是Java8中删除了持久代,有关持久代的操作现在全权交付给JVM了。
在这里插一下GC的执行机制,由于将对象进行了分代处理,因此垃圾回收区域、时间也不一样,GC有两种类型:Scavenge GC和Full GC。
Scavenge GC:一般情况下,当新对象生成,并且在Eden申请空间失败时,就会触发Scavenge GC,对Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。这种方式的GC是对年轻代的Eden区进行,不会影响到年老代。因为大部分对象都是从Eden区开始的,同时Eden区不会分配的很大,所以Eden区的GC会频繁进行。因而,一般在这里需要使用速度快、效率高的算法,使Eden区能尽快空闲出来。
Full GC:对整个堆进行整理,包括Young、Tenured和Perm。Full GC因为需要对整个堆进行回收,所以比Scavenge GC要慢,因此应该尽可能减少Full GC的次数。

基于上述回收算法思想产生了下述四款垃圾回收器,但选择哪一款并不是JVM自动选择的,它还没有那么智能,所以只能我们根据实际需求进行选择:
1.串行回收器:串行回收器是最简单的一个,主要面对单线程环境和比较小的堆,这个回收器工作的时候会将所有应用线程全部冻结,就这一点而言就注定它不适合应用于服务端。(可以打开-XX:+UseSerialGC这个JVM参数来启用它。)
2.并行回收器:这是JVM的默认回收器,它使用多个线程来扫描及压缩堆,但缺点在于不管执行的是minor GC还是full GC它都会暂停应用线程。并行回收器最适合那些可以容许暂停的应用,它试图减少由回收器所引起的CPU开销。
3.CMS回收器:这个算法在两种情况下会进入一个”stop the world”的模式:当进行根对象的初始标记的时候 (老生代中线程入口点或静态变量可达的那些对象)以及当这个算法在并发运行的时候应用程序改变了堆的状态使得它不得不回去再次确认自己标记的对象都是正确的。这个算法的弊端在于,如果回收器需要将年轻的对象提升到年老代中,而刚好这个时候年老代已经没有多余的空间了,它就会进行一次“stop the world”模式下的full GC,为避免这种情况发生,一个方法是增加年轻代和年老代的大小(或者增加整个堆的大小),另一个方法是给回收器分配一些后台线程以便与对象分配的速度进行赛跑。(可以打开-XX:+UseConcMarkSweepGC来启用它)
4.G1回收器:G1( Garbage first)回收器在JDK 7update 4中首次引入,它的设计目标是能更好地支持大于4GB的堆。G1回收器将堆分为多个区域,大小从1MB到32MB不等,并使用多个后台线程来扫描它们。G1回收器会优先扫描那些包含垃圾最多的区域,这正是它的名字的由来(Garbage first)。这一策略减少了后台线程还未扫描完无用对象前堆就已经用光的可能性,而那种情况回收器就必须得暂停应用,这就会导致STW回收。G1的另一个好处就是它总是会进行堆的压缩,而CMS回收器只有在full GC的时候才会干这事。(可以通过-XX:UseG1GC来启用它。)
介绍到这里,估计有人就有疑问了,既然每一次只能使用一个回收器,那么选择哪个的效率更高呢?有个人做了个测试实验(点击可进入原文),对于测试结果他贴了一张图:
多款GC性能表现
图中内容显示Java8默认的Parallel GC是性能最佳的。不过该测试实验的条件具有一定的局限性,并不能说明所有情况,详情可进入原文链接查看这里不再赘述。

好了到这里有关GC的整理也暂告一段落了,零零散散的凑了不少内容。看下时间一点半,恩该睡觉了。。。

阅读更多
文章标签: jvm GC
个人分类: Java学习
想对作者说点什么? 我来说一句

没有更多推荐了,返回首页

关闭
关闭
关闭