JVM调优的垃圾定位、垃圾回收算法、垃圾处理器比较实现,了解前面的前置知识有利于对垃圾回收器的理解。什么是垃圾?垃圾主要是指堆上的对象,那么如何判断这些对象是否可以被回收呢?大体的思路是,如果一个对象永远不能被访问,那么它就是垃圾,如何判断是否可以回收该对象永远不会被使用呢?引用计数方法为对象添加一个引用计数器。每当有对它的引用时,计数器值就加一;当引用无效时,计数器值减一;计数器随时为零对象不再可用。但是在Java领域,至少主流的Java虚拟机并没有使用引用计数算法来管理内存。比如简单的引用计数很难解决对象间循环引用的问题。如图所示,每个对象的引用都是1,构成循环引用,但不能被其他对象访问。这两个对象没有任何引用,引用计数算法无法回收它们。代码验证:packagecom.courage;publicclassReferenceCountingGC{publicObjectinstance=null;privatestaticfinalint_1MB=1024*1024;/***这个成员属性唯一的意义就是占用一些内存,这样可以在GC日志中清楚的看到是否被回收了*/privatebyte[]bigSize=newbyte[5*_1MB];publicstaticvoidtestGC(){//5MReferenceCountingGCobjA=newReferenceCountingGC();//5MReferenceCountingGCobjB=newReferenceCountingGC();objA.instance=objB;objB.instance=objA;objA=null;objB=null;//假设GC发生在这一行,objA和objB是否可以回收?System.gc();}publicstaticvoidmain(String[]args){testGC();}}执行结果:[0.004s][warning][gc]-XX:+PrintGCDetailsisdeprecated.Willuse-Xlog:gc*instead.[0.012s][info][gc,heap]Heapregionsize:1M[0.015s][info][gc]UsingG1[0.015s][info][gc,heap,coops]Heapaddress:0x0000000701000000,size:4080MB,CompressedOopsmode:Zerobased,Oopshiftamount:3......[0.119s][info][gc,metaspace]GC(0)Metaspace:805K->805K(1056768K)??[0.119s][info][gc]GC(0)PauseFull(System.gc())14M->0M(8M)2.886ms[0.119s][info][gc,cpu]GC(0)User=0.03sSys=0.00sReal=0.00s[0.120s][info][gc,heap,exit]他ap...为了篇幅,我省略了一些打印的内容。可以看到System.gc()后的内存使用量从14M->0M,释放了对象的10M,说明JVM没有使用引用计数方式。标记垃圾。该算法的基本思想是使用一系列称为“GCRoots”的根对象作为起始节点集。从这些节点开始,按照引用关系向下查找。搜索过程所经过的路径称为“GCRoots”。引用链”(ReferenceChain),如果一个对象和GCRoots之间没有引用链,或者在图论中,从GCRoots到这个对象是不可达的,就证明这个对象是不可能被再次使用的。在Java技术体系中,可以固定为GCRoots的对象包括:虚拟机栈中引用的对象(栈帧中的局部变量表),比如各个线程调用的方法栈中使用的参数,Objects被方法区中的类静态属性引用,比如局部变量和临时变量,比如Java类的引用类型静态变量。方法区常量引用的对象,如字符串常量池(StringTable)中的引用。本地方法栈中JNI引用的对象(俗称Native方法)。Java虚拟机内部的引用,如基本数据类型对应的Class对象,一些常驻异常对象(如NullPointExcepiton、OutOfMemoryError)等,以及系统类加载器。同步锁(synchronized关键字)持有的所有对象。反映Java虚拟机内部情况的JMXBean,JVMTI中注册的回调,本地代码缓存等垃圾收集算法本文介绍了三种常见的垃圾收集算法(mark-sweep,mark-compact,mark-copy),分别是java虚拟机各种垃圾收集器的算法基础。垃圾收集算法思路目前商业化的虚拟机垃圾收集器大多是根据“分代收集”理论设计的。它基于两个世代假设:1)弱世代假设:大多数对象都是短暂的。2)强分代假设(StrongGenerationalHypothesis):在垃圾回收过程中幸存下来的对象越多,就越难消亡。这两个分代假设共同为许多常用的垃圾收集器建立了一个一致的设计原则:收集器应该将Java堆划分为不同的区域,然后根据对象的年龄(年龄意味着对象在垃圾收集过程中存活的数量)进行回收次)分配到不同的区域进行存储。显然,如果一个区域中的大多数对象都在死亡,很难在垃圾回收过程中存活下来,那么把它们放在一起,只关注每次回收时如何保留少量存活的对象,而不是标记大量要回收的对象可以以低成本回收大量空间;如果剩下的是难以消亡的对象,就把它们放在一起,虚拟机就可以使用这块区域,回收频率较低,既兼顾了垃圾回收的时间开销,又兼顾了内存空间的有效利用。Mark-Sweep算法Mark-Sweep算法分为两个阶段:“标记”和“清除”。首先对所有需要回收的对象进行标记,标记完成后统一回收所有标记的对象。对于幸存的对象,统一收集所有未标记的对象。它的缺点主要有两个:第一是执行效率不稳定。如果Java堆中包含了大量的对象,而且其中大部分都需要被回收,此时就必须进行大量的标记和清除动作,导致标记和清除。两个进程的执行效率随着对象数量的增加而降低;二是内存空间的碎片化。标记和清除后,会产生大量不连续的内存碎片。太多的空间碎片可能会导致以后程序运行时出现问题。当进程需要分配更大的对象时,找不到足够的连续内存,不得不提前触发另一个垃圾收集动作。Mark-CopyMark-CopyMark-Copy算法通常简称为复制算法。它将可用内存按容量分成大小相等的两块,一次只使用其中一块。当这块内存用完后,将存活的对象复制到另一块中,然后一次性清理掉已使用的内存空间。如果内存中的大部分对象都是存活的,这种算法会产生大量的内存到内存的复制开销,但是对于大部分对象都是可回收的情况,算法只需要复制少量存活的对象,而且每次都是回收整个半个区域的内存。分配内存时,无需考虑空间碎片的复杂情况。只需移动堆顶指针并按顺序分配即可。这种实现简单,运行高效,但其缺陷也很明显。这种副本恢复算法的代价是将可用内存减少到原来的一半,空间浪费太大。Mark-CompactMark-CompactMark-Copy算法在对象存活率高的时候会执行更多的复制操作,效率会降低。更重要的是,如果不想浪费50%的空间,就需要有额外的空间分配保证来应对已用内存中所有对象都是100%存活的极端情况,所以一般不能直接在老年代算法中选择这个。mark-compression算法的标记过程还是和“mark-clear”算法一样,只是后面的步骤不是直接清理可回收对象,而是将所有存活的对象移动到内存空间的一端,然后直接清理边界外的内存:mark-clear算法和mark-compact算法的本质区别在于前者是非移动回收算法,而后者是移动回收算法。回收后是否移动存活对象是一个冒险的决定,有利也有弊:如果移动存活对象,尤其是老年代每次回收都有大量的对象存活区,移动存活对象并更新所有引用这些对象的地方都将是一个极其繁重的操作,这个对象移动操作必须挂起用户应用程序(STW问题)才能进行。垃圾处理器基于以上三种垃圾收集算法,衍生出七种垃圾收集器:收集线程将用于完成垃圾收集工作。更重要的是,需要强调的是,当它在收集垃圾时,必须暂停所有其他工作线程,直到收集结束。到目前为止,它仍然是客户端模式下运行的HotSpot虚拟机默认的新生代收集器。它优于其他收集器,即简单高效(与其他收集器的单线程相比),并且在内存资源受限的环境下,它具有最小的额外内存消耗(MemoryFootprint)[1]在所有收藏家中;对于单核处理器或者处理器核数较少的环境,Serial收集器没有线程交互,专注于垃圾收集自然可以获得最高的单线程收集效率。对于以客户端模式运行的虚拟机,Serial收集器是一个不错的选择。ParNew收集器ParNew收集器本质上是Serial收集器的多线程并行版本。除了同时使用多个线程进行垃圾回收外,其余行为包括Serial收集器可用的所有控制参数(例如:-XX:SurvivorRatio、-XX:PretenureSizeThreshold、-XX:HandlePromotionFailure等)、collection算法、StopTheWorld、对象分配规则、回收策略等都与Serial收集器完全一致,两个收集器在实现上也共享了相当多的代码。ParNew收集器除了支持多线程并行收集外,与Serial收集器相比并没有太多创新之处,但它是很多运行在服务器模式下的HotSpot虚拟机,尤其是JDK7之前的首选新生代收集器遗留系统,一个与功能和性能无关但其实很重要的原因是:除了Serial收集器之外,目前只有CMS收集器可以工作。另一方面,CMS的出现巩固了ParNew收集器在单核处理器环境下永远不会比Serial收集器更好的效果。即使由于线程交互的开销,收集器也是通过超线程技术实现的。在伪双核处理器环境下,并不能100%保证超越Serialcollector。当然,随着可以使用的处理器核心数量的增加,ParNew对于垃圾回收时系统资源的高效利用还是很有好处的。ParallelScavenge收集器ParallelScavenge收集器也是新一代收集器。它也是一个基于mark-copy算法的收集器,也是一个多线程的收集器,可以并行收集...ParallelScavenge的很多特性表面上看和ParNew非常相似,那么它有什么特别的呢?ParallelScavenge收集器的特点是它的侧重点不同于其他收集器。CMS等收集器的重点是尽可能缩短垃圾收集时用户线程的停顿时间,而ParallelScavenge收集器的目标是实现可持续的受控吞吐量(Throughput)。所谓吞吐量就是处理器用来运行用户代码的时间与处理器消耗的总时间的比值,即如果虚拟机完成某项任务,用户代码加上垃圾回收一共需要100分钟,其中垃圾回收耗时1分钟,吞吐量为99%。停顿时间越短,越适合需要与用户交互或保证服务响应质量的程序。良好的响应速度可以提升用户体验;而高吞吐量可以最有效地使用处理器资源,尽快完成程序的计算任务。主要适用于后台运行,交互不多的分析任务。由于与吞吐量的密切关系,ParallelScavenge收集器常被称为“吞吐量优先收集器”。SerialOld收集器SerialOld是Serial收集器的老版本,也是单线程收集器,使用标记-排序算法。这个收集器的主要意义也是客户端模式下的HotSpot虚拟机使用的。如果是服务器模式,它可能还有两个用途:一个是和JDK5及更早版本中的ParallelScavenge收集器一起使用,另一个是作为CMS收集器出现故障时的备份计划。在并发收集中发生ConcurrentModeFailure时使用。ParallelOld收集器ParallelOld是ParallelScavenge收集器的老版本,支持多线程并发收集,基于mark-sort算法实现。这个收集器直到JDK6才提供。在此之前,新一代的ParallelScavenge收集器一直处于比较尴尬的状态。原因是如果新生代选择Para??llelScavenge收集器,老年代除了SerialOld(PSMarkSweep)收集器外,其他性能良好的老年代收集器如CMS无法与之协同工作。由于老年代SerialOld收集器对服务器应用性能的“拖累”,使用ParallelScavenge收集器可能无法最大化整体吞吐量。同样,由于单线程老年代收集无法充分利用服务器多处理器的并行处理能力,在老年代内存空间大、硬件规格相对先进的运行环境下,这种组合的总吞吐量可能甚至不高于ParNew加上CMS的组合非常出色。直到ParallelOld收集器的出现,“吞吐量优先”的收集器终于有了更名副其实的组合。当强调吞吐量或处理器资源紧缺时,可以优先考虑ParallelScavenge和ParallelOldcollector的组合。.CMS收集器CMS(ConcurrentMarkSweep)收集器是一种旨在获得最短恢复停顿时间的收集器。目前,很大一部分Java应用都集中在互联网网站的服务器端或基于浏览器的B/S系统中。这些应用通常更注重服务的响应速度,希望系统停顿时间尽可能短,为用户带来良好的交互体验。CMS收集器非常适合这类应用的需求。从名字(包括“MarkSweep”)可以看出CMS收集器是基于mark-clear算法实现的。它的操作过程比以前的收集器更复杂。整个过程分为四个步骤,包括:1)初始标记(CMSinitialmark)2)并发标记(CMSconcurrentmark)3)重新标记(CMSremark)4)并发清除(CMSconcurrentsweep)两个步骤初始标记和重新标记仍然需要“停止世界”。初始标记只是标记GCRoots可以直接关联的对象,速度很快;并发标记阶段是从GCRoots的直接关联对象开始遍历整个对象图的过程。这个过程需要很长时间,但不需要停止用户线程。它可以与垃圾收集线程并发运行;重标记阶段是对并发标记期间由于用户程序的继续运行而改变的部分对象的标记记录进行修正。该阶段的停顿时间通常比初始标记阶段的停顿时间长。比并发标记阶段稍长,但也远短;最后是并发清除阶段,对标记阶段判断出的死对象进行清理和删除。由于不需要移动幸存对象,这个阶段也可以与用户线程并发。由于垃圾收集器线程可以在整个进程最长的并发标记和并发清除阶段与用户线程一起工作,所以一般情况下,CMS收集器的内存回收过程是与用户线程并发执行的。优点:并发收集,低暂停缺点:1.对处理器资源非常敏感2.无法处理“浮动垃圾”(FloatingGarbage)3.空间碎片GarbageFirst收集器GarbageFirst(简称G1)收集器是垃圾收集的一个里程碑集热器技术发展史上的成就。首创了收集器针对partialcollection的设计思想和基于Region的内存布局形式。G1是一个主要针对服务器端应用程序的垃圾收集器。对于G1收集器之前的所有其他收集器,包括CMS,垃圾收集的目标范围要么是整个新生代(MinorGC),要么是整个老年代(MajorGC),要么是整个Java堆(FullGC)。GC)。但是G1已经跳出了这个牢笼。它可以对堆内存的任意部分组成一个CollectionSet(一般简称为CSet)进行回收。衡量标准不再是它属于哪一代,而是存放在哪块内存中的垃圾量最多,回收收益最大,这就是G1收集器的MixedGC模式。G1首创的Region-based堆内存布局是其能够实现这一目标的关键。虽然G1依然是按照分代收集的理论来设计的,但是其堆内存的布局与其他收集器有很大不同:G1不再坚持固定大小和固定数量的分代区域划分,而是将连续的Java堆划分为分成多个大小相等的独立区域(Region),每个Region可以根据需要充当新生代的Eden空间、Survivor空间或老年代空间。收集器可以采用不同的策略来处理扮演不同角色的Region,这样无论是新创建的对象还是存活了一段时间并存活了多次收集的旧对象,都可以获得很好的收集结果。Region中还有一个特殊的Humongous区域,专门用来存放大对象。G1认为只要大小超过一个Region容量的一半,就可以判断为大对象。每个Region的大小可以通过参数-XX:G1HeapRegionSize来设置,取值范围是1MB到32MB,应该是2的N次方。对于那些超过整个Region容量的超大对象,会存储在N个连续的HumongousRegion中。G1的大部分行为都将HumongousRegion视为老年代的一部分,如图3-12所示。虽然G1仍然保留了新生代和老年代的概念,但是新生代和老年代已经不再固定。它们是一系列区域(不一定是连续的)的动态集合。G1收集器之所以能够建立可预测的停顿时间模型,是因为它把Region看作是单次收集的最小单位,即每次收集的内存空间是Region大小的整数倍,这样就可以计划以避免在整个Java堆上执行区域范围内的垃圾收集。更具体的处理思路是让G1收集器跟踪每个Region垃圾堆积的“值”。该值是回收获得的空间大小和回收所需时间的经验值,然后在后台维护一个优先级列表。每次根据用户设置的允许收集暂停时间(使用参数-XX:MaxGCPauseMillis指定,默认值为200毫秒),回收值最大的Region会被优先处理,这就是命名为“垃圾优先”。这种使用Region划分内存空间和优先region回收的方式保证了G1收集器能够在有限的时间内获得尽可能高的收集效率。垃圾处理器总结目前是新生代和老年代垃圾收集器的结合:
