垃圾收集器

图片看不清楚,能够下载或在页面中单独查看图片

1. 概述

Garbage Collection, GC:1960年诞生于MIT的Lisp是第一门真正使用内存动态分配和垃圾收集器技术的语言。java

程序计数器,虚拟机栈,本地方法栈3个区域随线程而生,随线程而灭;栈中的栈帧随着方法的进入和退出而有条不紊地执行着出栈和入栈操做。每个栈帧中分配多少内存基本上是在类结构肯定下来时就已知的(尽管在运行期会由JIT编译器进行一些优化),所以这几个区域的内存分配和回收都具有肯定性,在这几个区域内就不须要过多考虑回收的问题。git

Java堆和方法区则不同:一个接口中的多个实现类须要的内存可能不同,一个方法中的多个分支须要的内存也可能不同,只能在运行期才能知道会建立哪些对象,这部份内存的分配和回收都是动态的,垃圾收集器所关注的是这部份内存。github

2. 对象已死吗?

堆里面存放着Java中几乎全部的对象实例,垃圾收集器在对堆进行回收前,须要肯定那些还“存活”,哪些已经“死去”,即不可能再被任何途径使用的对象。算法

2.1 引用计数算法

给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任什么时候刻计数器为0的对象就是不可能再被使用的。Java虚拟机里面没有选用引用计数算法来管理内存,其中最主要的缘由是它很难解决对象之间相互循环引用的问题。数组

应用:微软的COM计数,AS3,Python语言等。安全

2.2 可达性分析算法

Reachability Analysis:经过一系列称为“GC Roots”的对象做为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证实此对象是不可用的。服务器

 

 对象object5,object6,object7虽然互相有关联,可是它们到GC Roots是不可达的,因此它们将会被断定为可回收的对象。数据结构

 在Java中,可做为GC Roots的对象包括下面几种:多线程

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象
  • 方法区中类静态属性引用的对象
  • 方法区中常量引用的对象
  • 本地方法栈中JNI(即通常的Native方法)引用的对象

2.3 强引用,软引用,弱引用,虚引用

jdk1.2以前,Java中引用的定义:若是reference类型的数据中的数值表明的是另外一块内存的起始地址,就称这块内存表明着一个引用。并发

在jdk1.2以后,Java对引用的概念进行了扩充,将引用分为强引用,软引用,弱引用,虚引用。强度依次逐渐减弱。

(1)强引用:程序中广泛存在的,相似Object obj = new Object(),这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象;

(2)软引用:描述一些还有用但并不是必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出溢出异常以前,将会把这些对象列进回收范围之中进行第二次回收。若是此次回收尚未足够的内存,才会抛出内存溢出异常。在jdk1.2以后,提供了SoftReference类来实现软引用;

(3)弱引用:当垃圾收集器工做时,不管当前内存是否足够,都会回收掉只被引用关联的对象。在jdk1.2以后,提供了WeakReference类来实现弱引用;

(4)虚引用:一个对象是否有虚引用的存在,彻底不会对其生存时间构成影响,也没法经过虚引用来取得一个对象实例。为一个对象设置虚引用关联的惟一目的就是能在这个对象被收集器回收时收到一个系统通知,在jdk1.2以后,提供了PhantomReference类来实现虚引用。

2.4 finalize()方法

即便在可达性分析算法中不可达的对象,也并不是是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程

若是对象在进行可达性分析后发现没有与GC Roots相链接的引用链,那它将会被第一次标记而且进行一次筛选,筛选的条件是此对象是否有必要执行finalize()方法。(1)当对象没有覆盖finalize()方法;(2)finalize方法已经被虚拟机调用过;虚拟机这两种状况都视为“没有必要执行”。

若是对象被断定为有必要执行finalize方法,那么对象将会放置在一个叫作F-Queue的队列中,虚拟机自动创建一个优先级低的Finalizer线程去执行它(这里“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束,这样作的缘由是,若是一个对象的finlize方法执行缓慢或发生死循环,将极可能致使F-Queue队列中其余对象永久处于等待)。稍后GC将对F-Queue中的对象进行第二次小规模的标记,若是对象从新与引用链上的任何对象创建关联便可,那个第二次标记时它将被移除“即将回收”的集合;若是对象这时候尚未逃脱,那基本上它就真的被回收了。

实例:

package org.github.oom;

public class FinalizeEscape {
    public static FinalizeEscape fe = null;
    public void alive() {
        System.out.println("yes, i am still alive...");
    }
    public static void dead() {
        System.out.println("no, i am dead...");
    }
    
    
    @Override
    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!!!");
        fe = this;
    }
    
    public static void main(String[] args) throws InterruptedException {
        fe = new FinalizeEscape();
        // 对象第一次成功拯救本身
        fe = null;
        System.gc();
        // 由于finalize方法优先级很低,因此暂停1秒,等待它
        Thread.sleep(1000);
        if (fe != null) {
            fe.alive();
        } else {
            dead();
        }
        
        // 拯救失败
        fe = null;
        System.gc();
        // 由于finalize方法优先级很低,因此暂停1秒,等待它
        Thread.sleep(1000);
        if (fe != null) {
            fe.alive();
        } else {
            dead();
        }
    }

}  

运行结果:

 

注意,任何一个对象的finalize方法都只会被系统自动调用一次,若是对象面临下一次回收,它的finalize方法不会被再次执行。

2.5 回收方法区

HotSpot虚拟机中的实现是永久代,主要回收两个部分:废弃常量和无用的类。

断定一个常量是不是“废弃常量”比较简单,而要断定一个类是不是“无用的类”的条件则相对苛刻许多。类须要同时知足3个条件才能算是“无用的类”:

  1. 该类全部的实例都已经被回收,也就是Java堆中不存在该类的任何实例;
  2. 加载该类的ClassLoader已经被回收;
  3. 该类对应的java.lang.Class对象没有在任何地方被引用,没法再任何地方经过反射访问该类的方法。

虚拟机能够对知足上述3个条件的无用类进行回收,这里说的仅仅是“能够”,而并非和对象同样,不是了就必然回收。是否对类进行回收,HotSpot虚拟机提供了-Xnoclassgc参数进行控制,还可使用-verbose:class,-XX:+TraceClassLoading,-XX:+TraceClassUnLoading查看类加载和卸载信息。

注:在大量使用反射,动态代理,CGLib等ByteCode框架,动态生成JSP以及OSGi这类频繁自定义ClassLoader的场景都须要虚拟机具有类卸载的功能,以保证永久代不会溢出。

3. 垃圾收集算法

因为垃圾收集算法的实现涉及大量的程序细节,并且各个平台的虚拟机操做内存的方法又各不相同,所以不打算过多讨论算法的实现。

3.1 标记 - 清除算法

Mark-Sweep算法分为“标记”和“清除”两个阶段,首先标记出全部须要回收的对象,在标记完成后统一回收全部被标记的对象。

它是最基础的收集算法,是由于后续的收集算法都是基于这种思路并对其不足进行改进而获得的。

不足:(1)效率问题,标记和清除的过程的效率都不高;(2)空间问题,标记清除以后会产生大量的不连续的内存碎片,空间碎片太多可能致使在程序运行过程当中须要分配较大对象时,没法找到足够的连续内存而不得不提早触发另外一次垃圾收集动做。

 

3.2 复制算法

为了解决效率问题,一种称为“复制”的收集算法,它将可用内存按容量划分为大小相等的两块。每次只使用其中的一块。当一块的内存用完了,就将还存活的对象复制到另一块上面,而后再把已使用过的内存空间一次清理掉。每次都是对整个半区进行内存回收,内存分配时就不用考虑内存碎片等复杂状况。

 

如今的商业虚拟机都采用这种收集算法来回收新生代,将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活的对象一次性复制到另一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(Eden + 一个Survivor),只有10%会被浪费。当存活对象大于10%,另外一Survivor空间不够时,须要依赖其余内存(老年代)进行分配担保。

分配担保:若是另一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接经过分配担保机制进入老年代。

3.3 标记 - 整理算法

复制收集算法在对象存活率较高时就要进行较多的复制操做,效率就会变低。根据老年代的特色,有人提出了另一种“标记 - 整理”算法,标记过程与“标记 - 清除”算法一致。但后续步骤不是直接对可回收对象进行清理,而是让全部存活的对象都向一端移动,而后直接清理掉端边界之外的内存。

3.4 分代收集算法

当前商业虚拟机的垃圾收集都采用“分代收集”算法(Generational Collection)。根据对象存活周期的不一样将内存划分为几块,Java堆分为新生代和老年代,根据各个年代的特色采用最适当的收集算法。

新生代中,每次垃圾收集时都发现有大批对象死去,只有少许存活,那就选用复制算法,只须要付出少许存活对象的复制成本就能够完成收集。

老年代中由于对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记 - 清理”或“标记 - 整理”算法进行回收。

3.5 HotSpot的算法实现

上面介绍了对象存活断定算法和垃圾收集算法,而在HotSpot虚拟机上实现这些算法时,必须对算法的执行效率有严格的考量,才能保证虚拟机高效运行。

3.5.1 枚举根节点

从可达性分析中从GC Roots节点找引用链这个操做为例,可做为GC Roots的节点主要在全局性的引用(常量或类静态属性等)与执行上下文(栈帧中的本地变量表)中。

如今不少应用仅仅方法区都有数百兆,若是要逐个检查这里面的引用,那么必然会消耗不少时间。

另外,可达性分析对执行时间的敏感还体如今GC停顿上,由于这项分析工做必须在一个能确保一致性的快照中进行。

目前的虚拟机使用的都是准确式GC,因此当执行系统停顿下来后,并不须要一个不漏地检查完全部执行上下文和全局的引用位置,HotSpot虚拟机中使用一组称为OopMap的数据结构来达到这个目的。

3.5.2 安全点

在OopMap的协助下,HotSpot能够快速且准确地完成GC Roots枚举,但一个很现实的问题随之而来:可能致使引用关系变化,或者说OopMap内容变化的指令很是多,若是为每一条指令都生成对应的OopMap,将会须要大量的额外空间,这样GC的空间成本将会变得很高。

实际上,HotSpot也的确没有为每条指令都生成OopMap,只有在特定的位置记录了这些信息,这些位置称为安全点(Safepoint),即程序执行时并不是在全部地方都能停顿下来开始GC,只有在达到安全点时才能暂停。Safepoint的选定既不能太少以至于让GC等待时间太长,也不能过于频繁以至于过度增大运行时的负荷。因此,安全点的选定基本上是以程序“是否具备让程序长时间执行的特征”为标准进行选定的。

“长时间执行”的最明显特征就是指令序列复用,例如方法调用、循环跳转、异常跳转等,因此具备这些功能的指令才会产生Safepoint。

对于安全点,另外一个须要考虑的问题是如何在GC发生时让全部线程都“跑”到最近的安全点上再停顿下来。有2种方案可供选择:抢先式中断和主动式中断。

(1)抢先式中断不须要线程的执行代码主动去配合,在GC发生时,首先把全部线程所有中断,若是发现有线程中断的地方不在安全点上,就恢复线程,让它“跑”到安全点上。如今JVM没有采用这种方法。

(2)主动式中断的思想是当GC须要中断线程的时候,不直接对线程操做,仅仅简单地设置一个标志,各个线程执行时主动去轮询这个标志,发现中断标志为真时就本身中断挂起,轮询标志的方法和安全点是重合的,另外再加上建立对象须要分配内存的地方。

3.5.3 安全区域

使用Safepoint彷佛已经完美解决了如何进入GC的问题,但实际状况却并不必定。Safepoint机制保障了程序执行时,在不太长的时间内就会遇到可进入GC的Safepoint。可是,程序“不执行”的时候呢?

所谓的程序不执行就是没有分配CPU时间,典型的例子就是线程处于sleep状态或Blocked状态,这时候程序没法响应JVM的中断请求,“走”到安全的地方去中断挂起,JVM也显然不太可能等待线程从新被分配CPU时间,对于这种状况,就须要安全区域(Safe Region)来解决。

安全区域是指在一段代码片断之中,引用关系不会发生变化。在这个区域中的任意地方开始GC都是安全的,也能够把Safe Region看作是被扩展了的Safepoint。

在线程执行到Safe Region中的代码时,首先标示本身已经进入了Safe Region,那样,当在这段时间里JVM要发起GC时,就不会管标识本身为Safe Region状态的线程了。

在线程要离开Safe Region时,它要检测系统是否已经完成了根节点枚举(或整个GC过程),若是完成了,那线程就继续执行,不然它必须等待知道收到能够安全离开Safe Region的信号为止。

4. 垃圾收集器

收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。

Java虚拟机规范中对垃圾收集器应该如何实现并无任何规定,所以不一样的厂商、不一样版本的虚拟机所提供的垃圾收集器均可能会有很大差异,而且通常都会提供参数供用户根据本身的应用特征和要求组合出各个年代所使用的收集器。

目前主要有7种做用于不一样分代的收集器,若是两个收集器之间存在连线,就说明它们能够搭配使用。收集器所处的区域,则表示它是属于新生代收集器仍是老年代收集器。

注:对各个收集器进行比较,但并不是为了挑选一个最好的收集器,由于直到如今为止尚未最好的收集器出现,更加没有万能的收集器,因此咱们选择的只是对具体应用最合适的收集器

4.1 Serial收集器

虚拟机运行在Client模式下的默认新生代收集器。只会使用一个CPU或一条收集线程去完成垃圾收集工做。

简单高效,对于限定单个CPU的环境来讲,Serial收集器因为没有先交互额开销,专心作垃圾收集天然能够得到最高的单线程收集效率。

 

4.2 ParNew收集器

ParNew收集器其实就是Serial收集器的多线程版本,除了使用多线程进行垃圾收集以外,其他行为包括Serial收集器可用的全部控制参数,收集算法,Stop The World,对象分配规则,回收策略等都与Serial收集器彻底同样。

 

ParNew是许多运行在Server模式下的虚拟机中首选的新生代收集器,其中一个很重要的缘由是除了Serial收集器外,只有它能与CMS收集器配合工做。

备注:此处解释并行与并发的区别

并行:指多条垃圾收集器线程并行工做,但此时用户线程仍然处于等待状态;

并发:指用户线程与垃圾收集器线程同时执行(但不必定是并行的,可能会交替执行),用户程序在继续运行,而垃圾收集程序运行于另外一个CPU上。

4.3 Parallel Scavenge收集器

Parallel Scavenge收集器是一个新生代收集器,它也是使用复制算法的并行收集器。

Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量:CPU用于运行用户代码的时间与CPU总消耗时间的比值,即吞吐量 = 运行用户代码时间 / (运行用户代码时间 + 垃圾收集时间) 

Parallel Scavenge收集器提供了两个参数用于精确控制吞吐量:

(1)-XX:MaxGCPauseMillis最大垃圾收集停顿时间,参数容许的值是一个大于0的毫秒数。

不要觉得把这个参数的值设置得小一点就能使得系统的垃圾收集速度变得更快。GC停顿时间缩短是以牺牲吞吐量和新生代空间来换取的:系统把新生代调小一点,收集300MB新生代确定比收集500MB快,这也直接致使垃圾收集发生得更频繁一些,原来10秒收集一次,每次停顿100毫秒,如今变成5秒收集一次,每次停顿70毫秒。停顿时间的确实降低了,可是次数多了,吞吐量也就降下来了。

(2)-XX:GCTimeRatio设置吞吐量大小,值应该是一个0 < xx < 100的整数,默认值为99,就是容许最大1%(即1 /(1 + 99))的垃圾收集时间。

Parallel Scavenge收集器还有一个参数-XX:+UseAdaptiveSizePolicy这是一个开关参数,打开就不须要手动指定新生代的大小(-Xmn),Eden与Survivor区的比例(-XX:SurvivorRatio),晋升老年代对象年龄(-XX:PretenureSizeThreshold)等细节参数。虚拟机会根据当前系统的运行状况收集性能监控信息,动态调整这些参数以提供最合适的停顿时间或最大吞吐量,这种调节方式称为GC自适应的调节策略。

4.4 Serial Old收集器

Serial Old是Serial收集器的老年代版本,单线程,标记 - 整理算法, 这个收集器主要用在Client模式下。若是是Server模式下,有两个用途(1)与Parallel Scavenge收集器搭配使用;(2)在CMS并发收集发生Concurrent Mode Failure时做为CMS收集器的后备预案。

注:Concurrent Mode Failure后面会介绍。

4.5 Parallel Old收集器

Parallel Old是Parallel Scavenge收集器的老年代版本,多线程,标记 - 整理算法。

4.6 CMS收集器

Concurrent Mark Sweep是一种以获取最短回收停顿时间为目标的收集器,多线程,标记 - 清除算法(碎片问题)。目前很大一部分的Java应用集中在互联网或者B/S系统的服务器上,这类应用尤为重视服务的响应速度,但愿系统停顿时间最短,给用户带来较好的体验。CMS收集器就很是符合这类应用的需求。

它的运行过程分为4个步骤:

(1)初始标记(CMS initial mark),须要STW,只是标记GC Roots能直接关联到的对象,速度很快;

(2)并发标记(CMS concurrent mark),进行GC Roots Tracing的过程;

(3)从新标记(CMS remark),须要STW,为了修正并发标记阶段因用户程序继续运做而致使标记产生变更的那一部分对象的标记记录,这个阶段停顿时间比初始标记阶段稍长一些,可是远比并发标记的时间短;

(4)并发清除(CMS concurrent sweep)

因为整个过程当中耗时最长的并发标记和并发清除过程收集器线程均可以与用户线程一块儿工做,因此从整体上来讲,CMS收集器的内存回收过程是与用户线程一块儿并发执行的。

CMS是一款优秀的收集器,它的主要优势:并发收集、低停顿。

可是也有以下3个明显的缺点:

(1)CMS收集器对CPU资源很是敏感:其实,面向并发设计的程序都对CPU资源比较敏感。在并发阶段,它虽然不会致使用户线程停顿,可是会由于占用了一部分线程而致使应用程序变慢,总吞吐量下降。CMS默认启动的回收线程数是(sizeof(Cpu) + 3)/ 4。

(2)CMS收集器没法处理浮动垃圾,可能出现Concurrent Mode Failure失败而致使另外一次Full GC的产生。因为CMS并清理阶段用户线程还在运行着,伴随程序运行天然就还会有新的垃圾不断产生,这一部分垃圾出如今标记过程以后,CMS没法再当次收集中处理掉它们,只好留待下一次GC时再清理掉。这部分垃圾就称为“浮动垃圾”。也是因为在垃圾收集阶段用户线程还须要运行,那也就须要预留有足够的内存空间给用户线程使用,所以CMS不能像其余收集器那样等到老年代几乎彻底填满了再进行收集,须要预留一部分空间提供并发收集时的程序运做使用,CMS在老年代占用到92%时运行CMS,能够经过-XX:CMSInitiatingCccupancyFraction的值来改变。若是CMS运行期间预留的空间没法知足程序须要,就会出现一次“Concurrent Mode Failure”失败,这时虚拟机将启动后备预案:临时启动Serial Old收集器来从新进行老年代的垃圾收集,这样停顿时间就很长了。

(3)CMS是一款基于“标记 - 清除”算法实现的收集器(由于并发清除阶段,用户线程不停顿,无法使用标记 - 整理算法)。意味着收集结束时会有大量空间碎片产生。空间碎片过多时,将会给大对象分配带来很大麻烦,每每会出现老年代还有很大空间剩余,可是没法找到足够大的连续空间来分配当前对象,不得不提早触发一次Full GC。为了解决这个问题,CMS收集器提供了一个-XX:+UseCMSCOmpactAtFullCollection开关参数(默认是开启的),用于在CMS收集器顶不住要进行Full GC时开启内存碎片的合并整理过程,内存整理的过程是没法并发的,空间碎片问题没有了,但停顿时间不得不变长。虚拟机还提供了另一个参数-XX:CMSFullGCsBeforeCompaction,这个参数是用于设置执行多少次不压缩的Full GC后,跟着来一次带压缩的(默认值为0,标示每次进入Full  GC时都进行碎片整理)

4.7 G1

Garbage-First:一款面向服务器端应用的垃圾收集器。HotSpot开发团队赋予它的使命是(在比较长期的)将来能够替换掉CMS收集器。

G1收集器的运行大体可划分为如下几个步骤:

(1)初始标记(Initial marking):标记如下GC Roots能直接关联到的对象,而且修改TAMS(Next Top at Mark Start)的值,让下一个阶段用户程序并发运行时,能在正确可用的Region中建立新对象,这阶段须要停顿线程,但耗时很短。

(2)并发标记(Concurrent marking):从GC Roots开始对堆中对象进行可达性分析,找出存活对象,这阶段耗时较长,但可与用户程序并发执行。

(3)最终标记(Final marking):为了修正在并发标记期间因用户程序继续运做而致使标记产生变更的那一部分标记记录,须要停顿线程,可是可并行执行。

(4)筛选回收(Live data couting and evacuation):首先对各个Region的回收价值和成本记性排序,根据用户所指望的GC停顿时间来指定回收计划。须要停顿,只回收一部分Region,时间上是用户可控制的,并且停顿用户线程将大幅提升收集效率。

与其余收集器相比,G1具有以下特色:

(1)并行与并发:能充分利用多CPU,多核环境下的硬件优点。

(2)分代收集:分代概念在G1中依然得以保留。G1能够不须要其余收集配合就能独立管理整个GC堆,但它可以采用不一样的方式去处理新建立的对象和已经存活了一段时间、熬过屡次GC的旧对象以获取更好的收集效果。

(3)空间整合:G1从总体上看是基于“标记 - 整理”算法实现的收集器,从局部(两个Region之间)上来看是基于“复制”算法。都不会产生内存空间碎片。

(4)可预测的停顿:下降停顿时间是G1和CMS共同的关注点,但G1除了追求低停顿外,还能创建可预测的停顿时间模型。能让使用者明确指定在一个长度为M毫秒的时间片断内,消耗在垃圾收集上的时间不得超过N毫秒。

在G1以前的其余收集器进行收集的范围都是整个新生代或老年代,而G1再也不是这样。使用G1收集器时,Java堆的内存布局就与其余收集器有很大差异,它将整个Java堆划分为多个大小相等的独立区域(Region),虽然还保留新生代和老年代的概念,但新生代和老年代再也不是物理隔离的了,他们都是一部分Region的集合。

G1之因此可以创建可预测的停顿时间模型,是由于它能够有计划地避免在整个Java堆中进行全区域的垃圾收集。G1跟踪各个Region里面的垃圾堆积的价值大小(回收所得到的空间大小以及回收所须要时间的经验值),在后天惟一一个优先列表,每次根据容许的收集时间,优先回收价值最大的Region(G1名称的由来)。这种使用Region划份内存空间以及有优先级的区域回收方式,保证了G1收集器在有限的时间内能够获取尽量高的收集效率。

5. GC日志

每一种收集器的日志形式都是由它们自身的实现所决定的,即每一个收集器的日志格式均可以不同,可是各个收集器的日志也有必定的共性。

 

 

6. 配置垃圾收集器及参数

6.1 配置垃圾收集器

从上面图中能够看出新生代与老年代之间的收集器一共有6种组合,下面经过实例验证收集器:

6.1.1 UseSerialGC(client模式下的默认值)

打开此开关后,使用Serial + Serial Old的收集器组合进行内存回收。

如何设置Serial + CMS + Serial Old ?

实例:

package com.huawei.jvm;

public class Test00 {
    
    private static final int _1MB = 1024 * 1024;
    
    public static void main(String[] args) {
        byte[] b1 = new byte[6 * _1MB];
byte[] b2 = new byte[4 * _1MB]; } } 

设置jvm的参数为:

-Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails  -XX:+UseSerialGC

-Xms20M -Xmx20M参数限制Java堆大小为20MB,不可扩展。

-Xmn10M其中10MB分配给新生代,剩下的10MB分配给老年代。

-XX:SurvivorRatio=8设置了新生代中Eden区与一个Survivor区的空间比例为8:1,即Eden为8MB,to Survivor与from Survivor为1MB,即新生代可用空间为9MB(Eden区加一个Survivor区)。

-XX:+PrintGCDetails打印GC详细信息。

-XX:+UseSerialGC使用Serial + Serial Old的收集器组合进行内存回收

 运行上面的程序,控制台打印以下的GC日志和堆信息:

过程以下:

6.1.2 UseParNewGC【deprecated】

使用ParNew + Serial Old的收集器组合。

实例:

将上面实例中的收集器修改成-XX:+UseParNewGC。

GC日志与使用Serial + Serial Old的收集器相似,只是有个提示:未来会移除这种收集器的组合,主要缘由是这种组合方式的收集器不多使用,可是却花费了很大的开发,维护和测试。具体能够参考:【http://openjdk.java.net/jeps/173】,【http://openjdk.java.net/jeps/214】

6.1.3 UseConcMarkSweepGC

使用ParNew + CMS + Serial Old收集器组合,其中Serial Old收集器做为CMS出现Concurrent Mode Failure失败后的后备收集器。

将上面实例中的收集器修改成-XX:+UseConcMarkSweepGC。

GC日志也是相似的,能够看出不一样的收集器对新生代和老年代的命名是有部分区别的

6.1.4 UseParallelGC(server模式下的默认值)

使用Parallel Scavenge + Serial Old(PS MarkSweep)

将上面实例中的收集器修改成-XX:+UseParallelGC。

奇怪,Eden不足,却没有发生GC,直接将4MB存入了老年代。

修改代码:

package com.huawei.jvm;

public class Test00 {
    
    private static final int _1MB = 1024 * 1024;
    
    public static void main(String[] args) {
        byte[] b1 = new byte[6 * _1MB];
        byte[] b2 = new byte[3 * _1MB];
    }

}

经过对比发现,当整个新生代剩余的空间(Eden加一个Survivor)没法存放某个对象时,Parallel Scavenge/Parallel Old中该对象会直接进入老年代;

而若是整个新生代剩余的空间能够存放但只是Eden区空间不足,则会尝试一次Minor GC;

而对于Serial/Serial Old当发现Eden区不足以存放对象时,就进行一次Minor GC。

此外,为何触发了一次新生代GC,而后又触发了一次Full GC呢?

其实,Parallel Scavenge(-XX:+UseParallelGC)框架下,默认是在要触发full GC前先执行一次young GC,而且两次GC之间能让应用程序稍微运行一小下,以期下降full GC的暂停时间(由于young GC会尽可能清理了young gen的死对象,减小了full GC的工做量)

6.1.5 UseParallelOldGC

使用Parallel Scavenge + Parallel Old

修改使用-XX:+UseParallelOldGC

也是没有触发GC的,第二次分配改为3MB就会触发GC,原理同(4)UseParallelGC。

 

6.2 参数

(1)SurvivorRatio: 新生代中Eden区域与Survivor区域的容量比值,默认为8,表明Eden:Survivor = 8:1

(2)PretenureSizeThreshold: 直接晋升到老年代的对象大小,设置这个参数后,大于这个参数时对象将直接在老年代分配。该参数只对新生代的Serial和ParNew收集器才起做用。实例见下面章节【大对象直接进入老年代】

(3)MaxTenuringThreshold: 晋升到老年代的对象年龄,每一个对象在坚持过一次Minor GC以后,年龄就增长1,当超过这个参数值时进入老年代

(4)UseAdaptiveSizePoliy: 动态调整Java堆中个区域的大小以及进入老年代的年龄

(5)HandlePromotionFailure: 是否容许分配担保失败,即老年代的剩余空间不足以应付新生代的整个Eden和Survivor区的全部对象都存活的极端状况

(6)ParallelGCThreads: 设置并行GC时进行内存回收的线程数

(7)GCTimeRatio: GC时间占总时间的比率,默认值为99,即容许1%的GC时间。仅在使用Parallel Scavenge收集器时生效

(8)MaxGCPauseMillis: 设置GC的最大停顿时间,仅在使用Parallel Scavenge收集器时生效。

(9)CMSInitiatingOccupancyFraction: 设置CMS收集器在老年代空间被使用多少后触发垃圾收集。默认值为68%,仅在使用CMS收集器时生效

(10)UseCMSCompactAdFullCollection: 设置CMS收集器在完成垃圾收集后是否要进行一次内存碎片整理,仅在使用CMS收集器时生效

(11)CMSFullCsBeforeCompaction: 设置CMS收集器在进行若干次垃圾收集器再启动一次内存碎片整理,仅在使用CMS收集器时生效 

7. 内存分配与回收策略

对象主要分配在新生代的Eden区上,若是启动了本地线程分配缓冲,将按照线程优先在TLAB上分配,少数状况下也可能会直接分配在老年代中。分配规则并非固定的,其细节取决于当前使用的哪种垃圾收集器组合,还有虚拟机中与内存相关的参数设置。

7.1 对象优先在Eden分配

7.2 大对象直接进入老年代

大对象:须要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串和数组。

虚拟机提供了一个-XX:PretenureSizeThread参数,令大于这个设置值的对象直接在老年代分配,这样能够避免在Eden区和两个Survivor区直接发生大量的内存复制。

测试代码:

package com.huawei.jvm;

public class Test00 {
    
    private static final int _1MB = 1024 * 1024;
    
    public static void main(String[] args) {
        byte[] b1 = new byte[4 * _1MB];
    }

}

VM参数

-server -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8 -XX:+PrintGCDetails
-XX:+UseSerialGC -XX:PretenureSizeThreshold=5242880

即大于等于5MB的对象才会直接分配到老年代中,分别测试分配4MB和6MB的对象,而后堆的状况:

4MB的堆:

6MB的堆:

7.3 长期存活的对象将进入老年代

虚拟机给每一个对象定义了一个对象年龄计数器。若是对象在Eden出生并通过第一次Minor GC后仍然存活,而且能被Survivor容纳的话,将被移动到Survivor空间中,而且对象年龄设置为1,。对象在Survivor中每熬过一次Minor GC,年龄就增长1,当它的年龄增长到必定程度(默认15),就会晋升到老年代中。

对象晋升老年代的年龄阈值,能够经过参数-XX:MaxTenuringThreshold设置。

7.4 动态对象年龄断定

虚拟机并非永远要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,若是在Survivor空间中相同年龄全部对象大小的综合大于Survivor空间的一半,年龄大于或等于该年龄的对象就能够直接进入老年代,无需等到MaxTenuringThreshold中要求的年龄。

7.5 空间分配担保

  1. 在发生Minor以前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代全部对象总空间;
  2. 若是这个条件成立,那么Minor GC能够确保是安全的,则执行Minor GC;
  3. 若是不成立,虚拟机会接着查看HandlePromotionFailure设置值是否容许担保失败;
  4. 若是容许,会继续检查老年代最大可用的连续空间是否大于历次晋升到老年代对象的平均大小;
  5. 若是大于,将尝试一次Minor GC,若是小于或HandlePromotionFailure设置不容许,则进行一次Full GC

 

从JDK6 Update 24以后,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略。

从JDK 6 Update 24以后的规则变为只要老年代的连续空间大于新生代对象总大小或者历次晋升的平均大小就会进行Minor GC ,不然将进行Full GC。

  

x. 参考资料

http://blog.csdn.net/canot/article/details/51069424

http://blog.csdn.net/z69183787/article/details/51606410

http://openjdk.java.net/jeps/173

http://openjdk.java.net/jeps/214

相关文章
相关标签/搜索