程序员人生 网站导航

关于GC

栏目:php教程时间:2015-03-20 08:50:18

在介绍GC之前有必要先了解1下JVM的内存划分,这样在后面介绍GC和各种不同的GC collector的时候更容易理解。

下面这张图是“偷”的他人的,很经典的描写了jvm的体系结构,我们只需要关注最大的那1块――运行时数据区域。


运行时区顾名思义是jvm在运行时的内存结构,主要有以下5种。

1.方法区

方法区是各个线程同享的1块内存区域,当虚拟机装载1个class文件时,它会从2进制数据中解析类型的信息,这些信息便是存储在方法区,包括类的静态变量也会存储到该区域。虚拟机规范把该区域划分为堆的1部份,但是实际上它还有个别名Non-Heap,很明显是用来和堆做辨别的。在讨论GC时我们习惯把这个区域叫做永久代,本质上它俩不是1个概念,对HotSpot来讲,永久代仅仅是实现方法区的1种方式。并且HotSpot后续的版本计划移除永久代,如果该区域内存不足时,会产生OOM。

2.堆

堆和方法区1样也是各个线程同享的1块内存区域,所有的对象实例包括数组都在这里分配内存。比如当我们new Object()的时候便是在这里分配的内存,java堆可以说是jvm管理的最大的1块内存区域,需要注意的1点是和方法区1样jvm规范并没有要求堆是连续的,jvm可以在运行时动态的扩大和收缩堆。为了更好的实现GC,现代的jvm针对堆又做了细化,将1整块堆分成不同的区域。下面这张图来自oracle官方网站,详细的画出了堆的详细情况。


图倒是蛮大的- -,还是横着的,凑合着看吧,全部堆分为3个区域,Young区、Tenured区(也就是Old区)、Perm区,习惯上我们称为年轻代,年老代,永久代(实际上GC就是依照这3个代进行分代搜集的)。仔细的朋友可能会注意到每一个区域都有1块virtual,有必要说明1下virtual是做甚么,我们知道堆可以在运行时扩充,比如在配置虚拟机参数的时候通常会指定-Xmx,-Xms最大堆和初始堆,这里的virtual就是预留的内存区域,其值为最大堆减去初始堆的值,实际上操作系统1开始就会划分-Xmx大小的内存给jvm,只不过jvm1开始可能不需要那末大的空间,因此jvm将1部份内存标记为virtual区,留着后面扩大用。3种内存区域中Young区略微复杂些,这里又分为3个区域分别为1个Eden和两个Survivor区(1个to survivor1个from survivor),名字很成心思,1个是伊甸园1个是幸存区,关于这几个区具体寄存的是甚么后面做进1步解释。

3.虚拟机栈

虚拟机栈为线程私有,因此处于这个区域的值不需要斟酌并发的问题。jvm为每一个java线程都分配1个栈,为该线程私有,栈内存随着线程的烧毁而释放。jvm为每一个方法都会生成1个栈帧用于保存局部变量表,操作数栈(这些概念在我之前的博客中也提到过)等信息。基本类型和对象的援用都可以在栈中存储。该区域有可能抛出StackOverflowerror和OOM异常。

4.本地方法栈

本地方法栈类似虚拟机栈,只不过虚拟机栈是为java方法服务,本地方法栈为本地的Native方法服务。

5.程序计数器

程序计数器是1块非常小的内存区域,它是当前线程履行的字节码的行号唆使器,总是指向下1个要履行的指令,该区域是唯1不会产生OOM的内存区。

上面说过虚拟机栈,本地方法栈和程序计数器它们的内存分配在编译期基本就能够肯定,并且内存随着线程的方法结束或线程结束而烧毁,因此这部份内存不需要斟酌回收的问题。堆和方法区的内存分配相比来讲就具有了不肯定性,而且这部份的内存分配和回收都是动态的,因此jvm需要针对这两块内存做GC。

趁热打铁,刚刚讲完堆的分代,正好来看下为何要分代和各个代中存储的对象有何不同。

之所以要分代很明显的1个缘由是方便做垃圾搜集,由于垃圾搜集只是针对那些没有被援用的孤对象进行的,而研究表明java中大多数的对象都是短命的,但也有1些对象存活的时间比较长。因此为了针对这些生命不1的对象做搜集,将堆划分为不同的代来寄存这些对象,也就是说Young区中的对象都是比较“年轻的”,同理可理解Old区。这就是为何叫做Young和Old的缘由。

实际上GC其实不是java独有的,GC的历史要比java悠久。关于GC的基本原理比如援用计数法,可达性分析法等等就不作介绍了。GC主要有两种,1种是minor gc另外一种是major gc也有称为young gc和old gc的。minor gc产生在Young区,并且时间通常非常短,major gc产生在Old区,时间较长,需要控制major gc的次数和GC时间。

jvm依照对象存活的时间,给对象1个类似我们人类“年龄的概念”,实际上每经过1次GC,存活下来的对象年龄便+1,大多数情况下对象优先分配到Eden区,这就是为何这里叫Eden区的缘由,当Eden区的没有足够内存分配的时候,便会触发1次Minor GC。此时存活下来的对象年龄+1,当到达1定年龄的时候,表明该对象存活比较稳定,会把该部份对象移到年老代(我们可以通过参数-XX:MaxTenuringThreshold 控制经过量少次Minor gc后便进入old区,默许为15),如果在Minor GC的时候发现to survivor寄存不下这些对象,则会直接寄存到年老代。需要注意的1点是当要分配大对象(比如数组和长字符串)的时候,也是直接将他们分配到年老代的,因此我们尽可能避免大对象特别是短命大对象的使用,由于这很容易引发old区的内存不够分配,从而提早触发full gc。当年老代没有足够内存的时候便会触发Major GC,通常minor gc的时间非常短对程序影响可以疏忽,但是major gc的进程则要比minor gc长很多,因此要尽可能避免major gc的产生(当产生gc的时候会暂停所有的利用线程履行,官方称为stop the world,这里的时间指的就是stop the world的时间)。

来看1下目前几种主要的GC算法,之所以这些算法并存是由于针对不同的区域通常需要使用特定的算法。

1.标记-清除

很明显该算法分为两步,第1步是标记出需要回收的对象,第2步针对这些对象做清算动作。该算法的缺点主要有两个:1是效力问题,标记和清除的效力都不高,2是清除以后会产生大量不连续的内存碎片,这会致使运行进程中如果需要分配较大的对象,会由于找不到足够的连续内存而提早触发1次垃圾搜集。

2.复制算法

复制算法将内存分为大小相等的两块,每次使用1块,当1块使用完时,将还存活的对象全部复制到另外一块区域中,然后清算第1块的内存,该算法的好处不言而喻,不会产生内存碎片,但是只使用1块内存未免代价太大,并且在存活对象非常多的时候,效力肯定会低下。实际上在java中绝大多数的对象都是“短命的”,因此不需要依照1:1来划份内存,HotSpot默许将Eden区和两个survivor区依照8:2的比例进行划分,也就是Eden:survivor=8:1(固然我们可以通过-XX:SurvivorRatio设置Survivor的大小,该值如果为8,则表示Eden区占Young的10分之8,两个Survivor占Young区的10分之2),该算法非常合适在minor gc时使用。

3.标记-整理

复制算法针对有大量存活的对象时效力低下,因此不合适对年老代的回收,但是标记清除又有碎片的问题,因此产生了标记整理算法。标记整理算法和标记清除算法类似,第1步都是标记,而第2步整理阶段是将存活的对象移向1端,然后直接清算边界之外的内存,这样不会产生碎片效力也不算太差。

针对上面几种算法,HotSpot主要提供了以下几种实现:


图中黄色背景是年轻代的搜集器,浅灰色背景是年老代的搜集器,蓝色背景表示垃圾搜集器,两两直线相连表示两种搜集器可以共用。下面分别介绍1下这6种搜集器:

1."Serial"会引发stop the world,基于拷贝的单线程搜集器。

2."ParNew"即是serial的多线程版本,不同于 "Parallel Scavenge" ,ParNew可以和CMS配合1起使用威力更大。

3. "Parallel Scavenge"会引发stop the world,基于拷贝的多线程搜集器。

4."Serial Old"会引发stop the world,基于标记-清除-整理的单线程搜集器。

5."CMS"是1种并发短暂停的搜集器,其中的某些步会引发stw,后面详细讲授。

6."Parallel Old"1种并发的基于标记-整理的搜集器,Parallel Scavenge的年老代版本。

以上6种最复杂的是CMS搜集器,后面会详细讲授。

ParNew是多线程并行搜集器,CMS是并发搜集。这里不能不提1句,并行指的是多个垃圾搜集线程1起进行回收工作,此时利用线程是停止,但是并发指的是搜集线程和利用线程同时履行,也就是垃圾搜集工作的时候其实不影响利用(这里的不影响只是相对的,实际CMS的工作进程分为好多步,有些步骤也会产生stop the world)。

既然ParNew和Parallel Scavenge都是针对新生代的并行搜集,那末他们两个有甚么不同呢?

像CMS和ParNew等搜集器主要关注的是减少因搜集而引发的利用停顿时间,而Parallel Scavenge主要关注的是利用的吞吐量,所谓的吞吐量就是CPU用于运行利用程序时间和CPU总消耗时间的比值,比如虚拟机总共运行100分钟,而垃圾搜集用掉了1分钟,吞吐量即为99/100=99%。而对Parallel Scavenge有个特殊的参数 -XX:+UseAdaptiveSizePolicy利用该参数JVM可以在运行时自动调剂堆内存各个区的大小,不需要人为的配置。

针对虚拟机参数使用-XX配置不同的搜集器,主要有以下几种:

UseSerialGC 是"Serial" + "Serial Old"
UseParNewGC 是 "ParNew" + "Serial Old"
UseConcMarkSweepGC 是"ParNew" + "CMS" + "Serial Old"。 年老代的回收绝大多数时间使用"CMS"。但是当产生concurrent mode failure毛病的时候会切换到"Serial Old" 。
UseParallelGC是"Parallel Scavenge" + "Serial Old"
UseParallelOldGC是"Parallel Scavenge" + "Parallel Old" 


上面这张图是CMS搜集器的几个工作阶段分别是:初始标记,并发标记,重新标记,并发清除。其中的1,3两个步骤需要暂停所有的利用程序线程的。第1次暂停从root对象开始标记存活的对象,这个阶段称为初始标记;第2次暂停是在并发标记以后, 暂停所有利用程序线程,重新标记并发标记阶段遗漏的对象(在并发标记阶段结束后对象状态的更新致使)。第1次暂停会比较短,第2次暂停通常会比较长,并且 remark这个阶段可以并行标记。1个CMS会产生两次STW。因此在使用CMS的垃圾搜集器的时候,通常我们使用jstat查看的fullgc(有1种说法是fullgc的次数就是STW的次数)次数和cms产生的次数为2:1的关系。关于CMS的参数有很多需要关注的点:


其他的1些关于GC的参数还有下面1些:

-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 日志文件的输前途径

上面个的参数主要触及GC日志的打印,jvm还有很多其他的参数不逐一描写了,网上有很多详细的讲授。

讲了那末多GC,下面来分析1段GC日志

519.514: [GC 519.514: [ParNew: 5149852K->83183K(5662336K), 0.0831770 secs] 6955196K->1905793K(9856640K), 0.0833560 secs] [Times: user=0.57 sys=0.03, real=0.08 secs ] 

前面的519.514表示了自虚拟机启动到该GC产生的秒数,[GC表示本次是普通的GC固然还有[Full GC,[ParNew表示使用的是ParNew搜集器对年轻代做搜集, 5149852K->83183K(5662336K)分别表示GC前该区域已使用的内存大小,GC后该区域使用的内存大小,该区域的总大小。 0.0831770 secs表示GC所占用的时间单位为秒,后面更详细的时间user=0.57 sys=0.03, real=0.08 secs与Linux的time命令所输出的时间含义1致。

------分隔线----------------------------
------分隔线----------------------------

最新技术推荐