对Java程序员而言不须要显式地管理对象的生命周期:咱们能够在须要时建立对象,对象再也不被使用时,会被JVM在后台自动进行回收。那为何咱们还要去了解GC和内存分配?java
答案很简单:当须要排查各类内存溢出、内存泄露问题时或者当垃圾收集成为系统达到更高并发量的瓶颈时,就须要对这些“自动化”的技术实施必要的监控和调节。程序员
垃圾收集器所关注的内存分配和回收的区域为Java堆和方法区。一个接口中的多个实现类须要的内存可能不同,一个方法中的多个分支须要的内存也可能不同,咱们只有在程序处于运行期间时才能知道会建立那些对象。算法
简单来讲,垃圾收集由两步构成:查找再也不使用的对象(垃圾对象),以及释放这些对象所管理的内存。服务器
给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任什么时候刻计数器为0的对象就是不可能再被使用的。多线程
客观地说,引用计数算法的实现简单,断定效率也很高。可是,至少主流的Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的缘由是它很难解决对象之间相互循环引用的问题。并发
在主流的商用程序语言(Java、C#,甚至包括前面提到的古老的Lisp)的主流实现中,都是称经过可达性分析来断定对象是否存活的。这个算法的基本思路就是 经过一系列的称为“GC Roots”的对象做为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证实此对象是不可用的,将会被断定为可回收的对象。框架
在Java语言中,可做为GC Roots的对象包括下面几种:高并发
不管经过哪一种算法查找再也不使用的对象,断定对象是否存活都与“引用”有关。在JDK 1.2之前,Java中的引用的定义很传统:若是reference类型的数据中存储的数值表明的是另一块内存的起始地址,就称这块内存表明着一个引用。这种定义很纯粹,可是太过狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态。布局
在JDK 1.2以后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)4钟,这4钟引用强度依次逐渐减弱。性能
若是对象在进行可达性分析后发现没有与GC Roots相链接的引用链,那它将会被第一次标记而且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。当对象没有覆盖finalize()方法,或者finalize()方法已经被虚拟机调用过,虚拟机将这两种状况都视为“没有必要执行”。
若是这个对象被断定为有必要执行finalize()方法,那么这个对象将会放置在一个叫作F-Queue的队列之中,并在稍后由一个虚拟机自动创建的、低优先级的Finalizer线程去执行它。finalize()方法是对象逃脱死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模的标记,若是对象要在finalize()中成功拯救本身,只要从新与引用链上的任何一个对象创建关联便可,那在第二次标记时它将被移除出“即将回收”的集合;若是对象这时候尚未逃脱,那基本上它就真的被回收了。
任何一个对象的finalize()方法都只会被系统自动调用一次,若是对象面临下一次回收,它的finalize()不会被再次执行。
不少人认为方法区(或者Hotspot虚拟机中的永久代)是没有垃圾收集的,Java虚拟机规范中不要求虚拟机在方法区实现垃圾收集,并且在方法区中进行垃圾收集的“性价比”通常比较低:在堆中,尤为是在新生代中,常规应用进行一次垃圾收集通常能够回收70%~95%的空间,而永久代的垃圾收集效率远低于此。
永久代的垃圾收集主要回收两部份内容:废弃常量和无用的类。回收废弃常量与回收Java堆中的对象很是相似,而要断定一个类是不是“无用的类”的条件则相对苛刻许多。类须要同时知足3个条件才能算是“无用的类”:
虚拟机能够对知足上述3个条件的无用类进行回收,这里说的仅仅是“能够”,而并非和对象同样,不使用了就必然回收。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制。
在大量使用反射、动态代理、CGLib等ByteCode框架、动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都须要虚拟机具有类卸载的功能,以保证永久代不会溢出。
最基础的收集算法是“标记-清除”算法,该算法分为“标记”和“清除”两个阶段:首先标记出全部须要回收的对象,在标记完成后统一回收全部被标记的对象。它的主要不足有两个:
效率问题:标记和清除两个过程的效率都不高
空间问题:标记清除以后会产生大量不连续的内存碎片,空间碎片太多可能会致使之后在程序运行过程当中须要分配较大对象时,没法找到足够的连续内存而不得不提早触发另外一次垃圾收集动做。标记—清除算法的执行过程以下图所示:
为了解决效率问题,一种称为“复制(Copying)”的收集算法出现了,它将可用空间按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另一块上面,而后把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂状况,只要移动堆顶指针,按顺序分配内存便可,实现简单,运行高效。这种算法的代价是将内存缩小为原来的一半,未免过高了一点。复制算的执行过程以下图所示:
如今的商业虚拟机都采用这种收集算法来回收新生代,IBM公司的专门研究代表,新生代的对象98%是“朝生夕死”的,因此不须要按照1:1的比例来划份内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另一块Survivor空间上,最后清理掉Eden和用过的Survivor空间。当Survivor空间不够用时,须要依赖其余内存进行分配担保。
复制收集算法在对象存活率较高时就要进行较多的复制操做,效率将会变低。因此在老年代通常不能直接选用这种算法。
根据老年代的特色,提出了“标记-整理”(Mark-Compat)算法,该算法的标记过程仍然与“标记-清除”算法同样,但后续步骤不是直接对可回收对象进行清理,而是让全部存活的对象都向一端移动,而后直接清理端边界之外的内存。"标记 - 整理"算法的示意图以下图:
当前商业虚拟机的垃圾收集都采用“分代收集”算法,该算法根据对象存活周期的不一样将内存划分为几块,这样就能够根据各个年代的特色采用最适当的收集算法。通常是把Java堆分为"新生代(Young Generation)和"老年代(Old Generation或Tenured Generation)",新生代又被进一步划分为不一样区域,分别称为Eden空间和Survivor空间。
在新生代中,每次垃圾收集时都发现有大批对象死去,只有少许存活,那就选用复制算法,只须要付出少许存活对象的复制成本就能够完成收集。而老年代中由于对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清理”或者“标记-整理”算法来进行回收。
全部的垃圾收集算法在新生代进行垃圾回收时都存在“时空停顿”现象。全部应用线程都中止运行所产生的停顿称为时空停顿(stop-the-world)。一般这些停顿对应用的性能影响最大,调优垃圾收集时,尽可能减小这种停顿是最为关键的考量因素。
新生代是堆的一部分,对象首先在新生代中分配。新生代填满时,垃圾收集器会暂停全部的应用程序,回收新生代空间。再也不使用的对象会被回收,仍然在使用的对象会被移动到其余地方。这种操做被称为Minor GC。
对象不断地被移动到老年代,最终老年代也会被填满,JVM须要找到老年代中再也不使用的对象,并对它们进行回收。这个过程被称为Full GC,一般致使应用程序长时间的停顿。
Java虚拟机规范中对垃圾收集器应该如何实现并无任何规定,所以不一样的厂商、不一样版本的虚拟机所提供的垃圾收集器均可能会有很大差异,而且通常都会提供参数供用户根据本身的应用特色和要求组合出各个年代所使用的收集器。
Serial收集器是最基本、发展历史最悠久的收集器,在JDK 1.3以前是虚拟机新生代收集的惟一选择。该收集器使用单线程清理堆的内容。不管是进行Minor GC仍是Full GC,在进行清理堆空间时,全部的应用线程都会被暂停(Stop The World),直到它收集结束。进行Full GC时,它还会对老年代空间的对象进行压缩整理。
Serial收集器是虚拟机运行在Client模式下的默认新生代收集器。也有着优于其余收集器的地方:
ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集以外,其他行为包括Serial收集器可用的全部控制参数、收集算法、“Stop The World”、对象分配规则、回收策略等都与Serial收集器彻底同样,在实现上,这两种收集器也共用了至关多的代码。
ParNew收集器是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中有一个与性能无关但很重要的缘由是,除了Serial收集器外,目前只有它能与CMS收集器配合工做。CMS做为老年代的收集器,新生代只能选择ParNew或者Serial收集器中的一个。ParNew收集器也是使用-XX:+UseConcMarkSweepGC选项后的默认新生代收集器,也可使用-XX:+UseParNewGC选项来强制指定它。
ParNew收集器在单CPU的环境中绝对不会有比Serial收集器更好的效果,甚至因为存在线程交互的开销,该收集器在经过超线程技术实现的两个CPU的环境中都不能百分之百地保证能够超越Serial收集器。固然,随着可使用的CPU的数量的增长,它对于GC时系统资源的有效利用仍是颇有好处的。它默认开启的收集线程数与CPU的数量相同,在CPU很是多的环境下,可使用-XX:ParallelGCThreads参数来限制垃圾收集的线程数。
并行(Parallel):指多条垃圾收集线程并行工做,但此时用户线程仍然处于等待状态。
并发(Concurrent):指用户线程与垃圾收集线程同时执行,用户程序在继续运行,而垃圾收集程序运行于另外一个CPU上。
Parallel Scavenge收集器是一个新生代收集器,也是使用复制算法的收集器,又是并行的多线程收集器。可是Parallel Scavenge收集器的关注点与其余收集器不一样,CMS等收集器的关注点是尽量地缩短垃圾收集时用户线程的停顿时间,而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量(Throughput)。所谓吞吐量就是CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间),虚拟机总共运行了100分钟,其中垃圾收集花掉1分钟,那吞吐量就是99%。
停顿时间越短就越适合须要与用户交互的程序,良好的响应速度能提高用户体验,而高吞吐量则能够高效率地利用CPU时间,尽快完成程序的运算任务,主要适合在后台运算任务,主要适合在后台运算而不须要太多交互的任务。
Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量,分别是最大垃圾收集停顿时间的-XX:MaxGCPauseMillis参数以及直接设置吞吐量大小的-XX:GCTimeRatio参数。
除了上述两个参数以外,Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy值得关注。这是一个开关参数,当这个参数打开以后,就不须要手工指定新生代的大小(-Xmn)、Eden与Survivor区的比例(-XX:SurvivorRatio)、晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数,虚拟机会根据当前系统的运行状况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或者最大的吞吐量,这种调节方式称为GC自适应的调节策略。
Serial Old是Serial收集器的老年代版本,一样是一个单线程收集器,使用“标记-整理”算法。这个收集器的主要意义也是在于给Client模式下的虚拟机使用。若是在Server模式下 ,那么它主要还有两大用途:
Parallel Old是Parallel Scavenge收集器的老年代版本,使用多线程和“标记-整理”算法。这个收集器是在JDK 1.6中才开始提供的。在此以前,新生代的Parallel Scavenge收集器一直处于比较尴尬的状态。缘由是若是新生代选择了Parallel Scavenge收集器,老年代除了Serial Old(PS MarkSweep)收集器外别无选择。
Parallel Old收集器出现后,“吞吐量优先”收集器终于有了比较名副其实的应用组合,在注重吞吐量以及CPU资源敏感的场合,均可以优先考虑Parallel Scavenge加Parallel Old收集器。
CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。CMS收集器是基于“标记-清除”算法实现的,它的运做过程相对于前面几种收集器来讲更复杂一些,整个过程分为4个步骤:
其中,初始标记、从新标记这两个步骤仍然须要“Stop The World”。整个过程当中耗时最长的并发标记和并发清楚过程收集器线程均可以与用户线程一块儿工做,因此,从整体上来讲,CMS收集器的内存回收过程是与用户线程一块儿并发执行的。
CMS收集器也存在以下3个明显的缺点:
对CPU资源很是敏感
面向并发设计的程序都对CPU资源比较敏感。CMS收集器在并发阶段,虽然不会致使用户线程停顿,但会由于占用了一部分线程(或者说CPU资源)而致使应用程序变慢,总吞吐量会下降。CMS默认启动的回收线程数是(CPU数量 + 3)/ 4,也就是当CPU在4个以上时,并发回收时垃圾收集线程不小于25%的CPU资源,而且随着CPU数量的增长而降低。当CPU不足4个(譬如2个)时,CMS对用户程序的影响就可能变得很大,若是原本CPU负载就比较大,还分出一半的运算能力去执行收集器线程,就可能致使用户程序的执行速度突然下降了50%,其实也让人没法接受。
没法处理浮动垃圾(Floating Garbage),可能出现“Concurrent Mode Failure”失败而致使另外一次Full GC的产生。
CMS在并发清理阶段用户线程还须要运行着,伴随程序运行天然就会有新的垃圾不断产生,这一部分垃圾出如今标记过程以后,CMS没法在当次收集中处理掉它们,只好留待下一次GC时再清理掉。这一部分垃圾就称为“浮动垃圾”。因为在垃圾收集阶段用户线程还须要运行,那就还须要预留有足够的内存空间给用户线程使用,所以CMS收集器不能其余收集器那样等到老年代几乎彻底被填满了在进行收集,须要预留一部分空间提供并发收集时的程序运做使用。
若是CMS运行期间预留的内存没法知足程序须要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启用Serial Old收集器来从新进行老年代的垃圾收集,这样停顿时间就很长了。
老年代使用了多少内存时才会触发CMS收集器,是由参数:-XX:CMSInitiatingOccupancyFraction的值决定的。若是参数CMSInitiatingOccupancyFraction的值设置太高很容易致使大量“Concurrent Mode Failure”失败,性能反而下降。
产生空间碎片
CMS是一款基于“标记-清除”算法实现的收集器,这意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,每每会出现老年代还有很大空间剩余,但没法找到足够大的连续空间来分配当前对象,不得不提早触发一次Full GC。
为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCompactAtFullCollection开发参数(默认开启),用于在CMS收集器顶不住要进行FullGC时开启内存碎片的合并整理过程,内存整理的过程是没法并发的,空间碎片问题没有了,但停顿时间变长了。另一个参数-XX:CMSFullGCsBeforeCompaction,这个参数用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,表示每次进入Full GC时都进行碎片整理)。
G1 GC,全称Garbage-First Garbage Collector,经过-XX:+UseG1GC参数来启用,在JDK 7u4版本发行时被正式推出。在JDK 9中,G1被提议设置为默认垃圾收集器(JEP 248)。
G1是一种服务器端的垃圾收集器,G1收集器的设计目标是取代CMS收集器。与CMS相比,在如下方面表现的更出色:
使用G1收集器时,Java堆的内存布局就与其余收集器有很大差异,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留有新生代和老年代的概念,但新生代和老年代再也不是物理隔离的,它们都是一部分Region(不须要连续)的集合。
在上图中,注意到有一些Region标明了H,它表明Humongous,这表示这些Region存储的是巨大对象(humongous object,H-obj),即大小大于等于region一半的对象。
G1跟踪各个Region里面的垃圾堆积的价值大小(回收所得到的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据容许的收集时间,优先回收价值最大Region(这也就是Garbage-First名称的来由)。
这种使用Region划份内存空间以及有优先级的区域回收方式,保证了G1收集器在有限时间内能够得到尽量高的收集效率。
Java堆分为多个Region后,垃圾收集是否就真的能以Region为单位进行了?Region不多是孤立的,一个对象分配在某个Region中,它并不是只能被本Region中的其余对象引用,而是能够与整个Java堆任意的对象发生引用关系。那在作可达性断定肯定对象是否存活的时候,岂不是还得扫描整个Java堆才能保证准备性。这个问题其实并不是在G1中才有,只是在G1中更加突出而已。
在G1收集器中,Region之间的对象引用以及其余收集器中的新生代与老年代之间的对象引用,虚拟机都是使用Remembered Set来避免全堆扫描的。G1中每一个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操做时,会产生一个Write Barrier暂时中断写操做,检查Reference引用的对象是否处于不一样的Region之中,若是是,便经过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即保证不对全堆扫描也不会有遗漏。