JVM垃圾收集器与内存分配策略

本文是基于周志明的《深入理解Java虚拟机》 
  堆中几乎存放着Java世界中所有的对象实例,垃圾收集器在对堆回收之前,第一件事情就是要确定这些对象哪些还“存活”着,哪些对象已经“死去”(即不可能再被任何途径使用的对象)

一、判断对象是否存活

1、引用计数算法(Reference Counting)

  很多教科书判断对象是否存活的算法是这样的:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值加1;当引用失效时,计数器减1;任何时刻计数器都为0的对象就是不可能再被使用的。 
  引用计数算法的实现简单,判断效率也很高,在大部分情况下它都是一个不错的算法。但是Java语言中没有选用引用计数算法来管理内存,其中最主要的一个原因是它很难解决对象之间相互循环引用的问题。 
  例如: 
  在testGC()方法中,对象objA和objB都有字段instance,赋值令objA.instance=objB及objB.instance=objA,除此之外这两个对象再无任何引用,实际上这两个对象都已经不能再被访问,但是它们因为相互引用着对象方,异常它们的引用计数都不为0,于是引用计数算法无法通知GC收集器回收它们。

/**
 * 执行后,objA和objB会不会被GC呢?
 */
public class ReferenceCountingGC {
    public Object instance = null;

    private static final int _1MB = 1024 * 1024;

    /**
     * 这个成员属性的唯一意义就是占点内存,以便能在GC日志中看清楚是否被回收过
     */
    private byte[] bigSize = new byte[2 * _1MB];

    public static void main(String[] args) {
        ReferenceCountingGC objA = new ReferenceCountingGC();
        ReferenceCountingGC objB = new ReferenceCountingGC();
        objA.instance = objB;
        objB.instance = objA;

        objA = null;
        objB = null;

        //假设在这行发生了GC,objA和ojbB是否被回收
        System.gc();
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26

0.193: [GC 4418K->256K(61504K), 0.0046018 secs] 
0.198: [Full GC 256K->160K(61504K), 0.0125962 secs] 
  在运行结果中可以看到GC日志中包含”4418K->256K”,老年代从4418K(大约4M,其实就是objA与objB)变为了141K,意味着虚拟并没有因为这两个对象相互引用就不回收它们,这也证明虚拟并不是通过通过引用计数算法来判断对象是否存活的。大家可以看到对象进入了老年代,但是大家都知道,对象刚创建的时候是分配在新生代中的,要进入老年代默认年龄要到了15才行,但这里objA与objB却进入了老年代。这是因为Java堆区会动态增长,刚开始时堆区较小,对象进入老年代还有一规则,当Survior空间中同一代的对象大小之和超过Survior空间的一半时,对象将直接进行老年代。


2、可达性分析算法(GC Roots Analysis)

  主流用这个判断 
  在主流的商用程序语言中(Java和C#),都是使用可达性分析算法判断对象是否存活的。这个算法的基本思路就是通过一系列名为”GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链(Reference Chain),当一个对象到GC Roots没有任何引用链相连时,则证明此对象是不可用的,下图对象object5, object6, object7虽然有互相判断,但它们到GC Roots是不可达的,所以它们将会判定为是可回收对象。 
   
在Java语言里,可作为GC Roots对象的包括如下几种: 
  a.虚拟机栈(栈桢中的本地变量表)中的引用的对象 
  b.方法区中的类静态属性引用的对象 
  c.方法区中的常量引用的对象 
  d.本地方法栈中JNI的引用的对象


3、finalize()方法最终判定对象是否存活

  即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历再次标记过程。 
  标记的前提是对象在进行可达性分析后发现没有与GC Roots相连接的引用链。 
1).第一次标记并进行一次筛选。 
  筛选的条件是此对象是否有必要执行finalize()方法。 
  当对象没有覆盖finalize方法,或者finzlize方法已经被虚拟机调用过,虚拟机将这两种情况都视为“没有必要执行”,对象被回收。

2).第二次标记 
  如果这个对象被判定为有必要执行finalize()方法,那么这个对象将会被放置在一个名为:F-Queue的队列之中,并在稍后由一条虚拟机自动建立的、低优先级的Finalizer线程去执行。这里所谓的“执行”是指虚拟机会触发这个方法,但并不承诺会等待它运行结束。这样做的原因是,如果一个对象finalize()方法中执行缓慢,或者发生死循环(更极端的情况),将很可能会导致F-Queue队列中的其他对象永久处于等待状态,甚至导致整个内存回收系统崩溃。 
  Finalize()方法是对象脱逃死亡命运的最后一次机会,稍后GC将对F-Queue中的对象进行第二次小规模标记,如果对象要在finalize()中成功拯救自己—-只要重新与引用链上的任何的一个对象建立关联即可,譬如把自己赋值给某个类变量或对象的成员变量,那在第二次标记时它将移除出“即将回收”的集合。如果对象这时候还没逃脱,那基本上它就真的被回收了。不过大家尽量避免使用这个方法 
流程图如下: 
   
  

/**
 * 此代码演示了两点
 * 1、对象可以在被GC时自我拯救
 * 2、这种自救的机会只有一次,因为一个对象的finalize()方法最多只能被系统自动调用一次。
 */
public class FinalizeEscapeGC {
    public static FinalizeEscapeGC SAVE_HOOK = null;

    public void isAlive() {
        System.out.println("yes, I am still alive");
    }

    protected void finalize() throws Throwable {
        super.finalize();
        System.out.println("finalize method executed!");
        FinalizeEscapeGC.SAVE_HOOK = this;
    }

    public static void main(String[] args) throws InterruptedException {
        SAVE_HOOK = new FinalizeEscapeGC();

        //对象第一次成功拯救自己
        SAVE_HOOK = null;
        System.gc();

        //因为finalize方法优先级很低,所有暂停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no ,I am dead QAQ!");
        }

        //-----------------------
        //以上代码与上面的完全相同,但这次自救却失败了!!!
        SAVE_HOOK = null;
        System.gc();

        //因为finalize方法优先级很低,所有暂停0.5秒以等待它
        Thread.sleep(500);
        if (SAVE_HOOK != null) {
            SAVE_HOOK.isAlive();
        } else {
            System.out.println("no ,I am dead QAQ!");
        }
    }
}
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
  • 47

finalize method executed! 
yew, I am still alive 
no ,I am dead QAQ! 
  从结果可以看出,SAVE_HOOK对象的finalize()方法确实被GC收集器触发过,并且在被收集前成功逃脱了。 
  注意:任何一个对象的finalize()方法都只会被系统自动调用一次,如果对象面临下一次回收,它的finalize()方法不会被再次执行,因此第二段代码的自救行动失败了。 并且建议大家尽量避免使用它

Eclipse设置GC日志输出: 
  1.右键项目,选择properties。 
  2.选择run/debug setting, 在要执行的类点Edit,并如下图设置 
这个方法是测试main方法用的 

1、在eclipse根目录下的eclipse.ini配置文件中添加以下参数: 
  -verbose:gc (开启打印垃圾回收日志) 
  -Xloggc:eclipse_gc.log (设置垃圾回收日志打印的文件,文件名称可以自定义) 
  -XX:+PrintGCTimeStamps (打印垃圾回收时间信息时的时间格式) 
  -XX:+PrintGCDetails (打印垃圾回收详情) 
  添加完以上参数后当启动Eclipse后就能在Eclipse根目录看到一个eclipse_gc.log的gc日志文件 
2、设置eclipse初始堆、非堆内存大小以及年轻代 
  -Xms50m –Xmx200m -XX:PermSize=30m -XX:MaxPermSize=60m 
3、添加JVM监控参数 
  -Djava.rmi.server.hostname=127.0.0.1 -Dcom.sun.management.jmxremote.port=6688 -Dcom.sun.management.jmxremote.ssl=false -Dcom.sun.management.jmxremote.authenticate=false

gc日志:

Java HotSpot(TM) Client VM (25.25-b02) for windows-x86 JRE (1.8.0_25-b18), built on Oct  7 2014 14:31:05 by "java_re" with MS VC++ 10.0 (VS2010)
Memory: 4k page, physical 8340720k(3545144k free), swap 8787248k(2501780k free)
CommandLine flags: -XX:InitialHeapSize=268435456 -XX:MaxHeapSize=943718400 -XX:+PrintGC -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -XX:-UseLargePagesIndividualAllocation
4.458: [GC (Allocation Failure) 4.458: [DefNew: 69952K->8704K(78656K), 0.0681410 secs] 69952K->22643K(253440K), 0.0690407 secs] [Times: user=0.06 sys=0.02, real=0.08 secs]
5.741: [Full GC (Metadata GC Threshold) 5.741: [Tenured: 13939K->33208K(174784K), 0.1036054 secs] 48618K->33208K(253440K), [Metaspace: 11442K->11442K(12672K)], 0.1041130 secs] [Times: user=0.11 sys=0.00, real=0.11 secs]
12.427: [GC (Allocation Failure) 12.427: [DefNew: 70016K->8704K(78720K), 0.0821340 secs] 103224K->55838K(253504K), 0.0822684 secs] [Times: user=0.08 sys=0.00, real=0.09 secs]
12.787: [Full GC (Metadata GC Threshold) 12.787: [Tenured: 47134K->56307K(174784K), 0.1662610 secs] 59785K->56307K(253504K), [Metaspace: 19215K->19215K(20864K)], 0.1663899 secs] [Times: user=0.16 sys=0.00, real=0.16 secs]
17.018: [GC (Allocation Failure) 17.018: [DefNew: 70016K->8704K(78720K), 0.0475891 secs] 126323K->68567K(253504K), 0.0477196 secs] [Times: user=0.03 sys=0.02, real=0.06 secs]
21.047: [GC (Allocation Failure) 21.047: [DefNew: 78720K->8704K(78720K), 0.0752255 secs] 138583K->83739K(253504K), 0.0753766 secs] [Times: user=0.08 sys=0.00, real=0.06 secs]
21.320: [Full GC (Metadata GC Threshold) 21.320: [Tenured: 75035K->61015K(174784K), 0.2800589 secs] 87423K->61015K(253504K), [Metaspace: 31969K->31969K(34176K)], 0.2802057 secs] [Times: user=0.28 sys=0.00, real=0.29 secs]
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

GC日志说明: 
  GC打印时间: [垃圾回收类型回收时间: [收集器名称: 年轻代回收前占用大小->年轻代回收后占用大小(年轻代当前容量), 年轻代局部GC时JVM暂停处理的时间] 堆空间GC前占用的空间->堆空间GC后占用的空间(堆空间当前容量),GC过程中JVM暂停处理的时间]。 
  垃圾回收类型:分为GC和Full GC. 
  GC一般为堆空间某个区发生了垃圾回收, 
  Full GC基本都是整个堆空间及持久代发生了垃圾回收,通常优化的目标之一是尽量减少GC和Full GC的频率。 
  收集器名称:一般都为收集器的简称或别名,通过收集器名称基本都能判断出那个区发生了GC。 
  DefNew:年轻代(新生代)发生了GC (若为DefNew可知当前JVM年轻代使用的串行收集器) 
  ParNew:年轻代(新生代)发生了GC (若为ParNew可知当前JVM年轻代使用了并行收集器) 
  Tenured:老年代发生了GC 
  Perm:持久代发生了GC


4、引用

  无论是通过引用计数算法判断对象的引用数量,还是通过根搜索算法判断对象的引用链是否可达,判定对象是否存活都与“引用”有关。在JDK 1.2之前,Java中的引用的定义很传统:如果reference类型的数据中存储的数值代表的是另外一块内存的起始地址,就称这块内存代表着一个引用。这种定义很纯粹,但是太过狭隘,一个对象在这种定义下只有被引用或者没有被引用两种状态,对于如何描述一些“食之无味,弃之可惜”的对象就显得无能为力。我们希望能描述这样一类对象:当内存空间还足够时,则能保留在内存之中;如果内存在进行垃圾收集后还是非常紧张,则可以抛弃这些对象。很多系统的缓存功能都符合这样的应用场景。 
  在JDK 1.2之后,Java对引用的概念进行了扩充,将引用分为强引用(Strong Reference)、软引用(Soft Reference)、弱引用(Weak Reference)、虚引用(Phantom Reference)四种,这四种引用强度依次逐渐减弱。 
  强引用:就是指在程序代码之中普遍存在的,类似“Object obj = new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象。 
  软引用:用来描述一些还有用,但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象列进回收范围之中并进行第二次回收。如果这次回收还是没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。 
  弱引用:也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象。在JDK 1.2之后,提供了WeakReference类来实现弱引用。 
  虚引用:也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是希望能在这个对象被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。


5、回收方法区

  Java虚拟机规范中确实说过可以不要求虚拟机在方法区实现垃圾收集,而且在方法区中进行垃圾收集的“性价表”一般比较低。 
  方法区中的垃圾回收主要是:废弃常量及无用类。判断常量是否废弃与判断堆中对象十分相似。例如,若常量池中存在字符串“abc”,而系统中并没有任何String对象的值为“abc”的,也就是没有任何对象引用它,那么它就可以被回收了。无用类的判定稍微复杂点,需要满足: 
  1).该类的所有对象实例已经被回收,也就是Java堆中不存在该类的任何实例; 
  2).加载该类的ClassLoader已经被回收; 
  3).该类的类对象Class没有在任何地方被引用,无法使用反射来访问该类的方法。 
  当方法区中的类满足以上条件时,就可以对无用类进行回收了,这里说的仅仅是“可以”,而并不是和对象一样,不使用了就必然会回收。是否对类进行回收,HotSpot虚拟机提供了各种配置,这里不多讲。 
  在大量使用反射、动态代理、CGLIB等ByteCode框架、动态生成JSP以及OSGI这类频繁自定义ClassLoader的场景都需要虚拟机具备类卸载的功能,以保存永久代不会溢出。


二、垃圾收集算法

  Java 语言的一大特点就是可以进行自动垃圾回收处理,而无需开发人员过于关注系统资源,例如内存资源的释放情况。自动垃圾收集虽然大大减轻了开发人员的工作量,但是也增加了软件系统的负担。 
  由于垃圾收集算法的实现涉及大量的程序细节,而且各个平台的虚拟机操作内存的方法各不相同,因此下面只讨论几种算法的思想。

1、标记-清除算法 (Mark-Sweep)

  是最基础的收集算法,之所以这么说,是因为下面的其它算法都是基于这种思路并对其不足进行改进而得到的。

标记-清除算法将垃圾回收分为两个阶段: 
  ①.标记阶段:首先标记出所有需要回收的对象。 
  如何标记,在上面的“判断对象是否存活”里有讲过 
  ②.清除阶段:标记完成后,统一回收被标记的对象

缺点: 
  ①.效率问题:标记清除过程效率都不高。 
  ②.空间问题:标记清除之后会产生大量的不连续的内存碎片(空间碎片太多可能会导致以后在程序运行过程中需要分配较大的对象时,无法找到足够的连续的内存空间而不得不提前触发另一次垃圾收集动作。)


2、复制算法 (Copying)

为了解决mark-sweep算法的效率问题 
2.1.算法思想: 
  1).将现有的内存空间分为两快,每次只使用其中一块. 
  2).当其中一块时候完的时候,就将还存活的对象复制到另外一块上去。 
  3).再把已使用过的内存空间一次清理掉。

2.2.优点: 
  1).由于是每次都对整个半区进行内存回收,内存分配时不必考虑内存碎片问题。 
  2).只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。

2.3.缺点: 
  1).内存减少为原来的一半,太浪费了。 
  2).对象存活率较高的时候就要执行较多的复制操作,效率变低。 
  3).如果不使用50%的对分策略,老年代需要考虑的空间担保策略。

2.4.演进 
  并不需要根据1:1划分内存空间,而是将内存划分为一块较大的EdenSpace和两块较小的SurvivorSpace 
  JavaHeap内存回收模型(当前商业虚拟机大多使用此算法回收新生代) 
   
  图中的内存分配担保在下面会有介绍。


3、标记-整理算法 (Mark-Compact)

  由于复制算法的缺点,及老年代的特点(存活率高,没有额外内存对其进行空间担保),老年代一般不使用复制算法。

3.1.算法思想 
  1).标记阶段:首先标记出所有需要回收的对象。与“标记-清除”一样 
  2).让存活的对象向内存的一段移动。而不跟“标记-清除”直接对可回收对象进行清理 
  3).再清理掉边界以外的内存。 
  由于老年代存活率高,没有额外内存对老年代进行空间担保,那么老年代只能采用标记-清理算法或者标记整理算法。


4、分代收集算法 (Generational Collecting)

  当前的商业虚拟机的垃圾收集都采用,把Java堆分为新生代和老年代。根据各个年代的特点采用最适当的收集算法。 
  在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,选用:复制算法 
  在老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记-清除”或者“标记-整理”算法来进行回收。


三、垃圾收集器

  如果说收集算法是内存回收的方法论,垃圾收集器就是内存回收的具体实现。Java虚拟机规范中对垃圾收集器应该如何实现并没有任何规定,因此不同的厂商、不同版本的虚拟机所提供的垃圾收集器都可能会有很大的差别,并且一般都会提供参数供用户根据自己的应用特点和要求组合出各个年代所使用的收集器。 
   
  图展示了7种作用于不同分代的收集器(包括JDK 1.6_Update14后引入的Early Access版G1收集器),如果两个收集器之间存在连线,就说明它们可以搭配使用。 
  在介绍这些收集器各自的特性之前,我们先来明确一个观点:虽然我们是在对各个收集器进行比较,但并非为了挑选一个最好的收集器出来。因为直到现在为止还没有最好的收集器出现,更加没有万能的收集器,所以我们选择的只是对具体应用最合适的收集器。这点不需要多加解释就能证明:如果有一种放之四海皆准、任何场景下都适用的完美收集器存在,那HotSpot虚拟机就没必要实现那么多不同的收集器了。 
   
————————–新生代收集器————————

1、Serial收集器

最基本、发展历史最悠久的收集器 
适用:看上去没什么用,但实际上到现在为止,它依然是虚拟机运行在Client模式下的默认新生代收集器

特点: 
  1).单线程的收集器,说明它只会使用一个CPU或一条收集线程去完成垃圾收集工作 
  2).在它进行垃圾收集时,必须暂停其他所有的工作线程(Sun将这件事情称之为“Stop The World”),直到它收集结束。这项工作实际上是由虚拟机在后台自动发起和自动完成的,在用户不可见的情况下把用户的正常工作的线程全部停掉,这对很多应用来说都是难以接受的。 
收集算法:采用复制算法

 优点:简单而高效(与其他收集器的单线程比),对于限定单个CPU的环境来说,Serial收集器由于没有线程交互的开销,专心做垃圾收集自然可以获得最高的单线程收集效率。在用户的桌面应用场景中,分配给虚拟机管理的内存一般来说不会很大,收集几十兆甚至一两百兆的新生代(仅仅是新生代使用的内存,桌面应用基本上不会再大了),停顿时间完全可以控制在几十毫秒最多一百多毫秒以内,只要不是频繁发生,这点停顿是可以接受的。所以,Serial收集器对于运行在Client模式下的虚拟机来说是一个很好的选择。 
 缺点:GC时暂停线程带给用户不良体验 
 搭配:CMS 或Serial Old(MSC)


2 ParNew收集器

  ParNew收集器其实就是Serial收集器的多线程版本,除了使用多条线程进行垃圾收集之外,其余行为都与Serial收集器完全一样,实现上这两种收集器也共用了相当多的代码。

适用:运行在Server模式下的虚拟机中的新生代 
特点: 
  1).多线程GC(并行):ParNew是Serial的多线程版本,两者共用了许多代码。 
  2).在GC时暂停所有用户线程 
算法:采用复制算法 
优点:高效 
缺点:GC时暂停线程带给用户不良体验,单线程下效果不一定优于Serial 
搭配:CMS 或Serial Old(MSC) 
  ParNew收集器有一个与性能无关但很重要的原因是,除了Serial收集器外,目前只有它能与CMS收集器配合工作。在JDK 1.5以后使用CMS来收集老年代的时候,新生代只能选择ParNew或Serial收集器中的一个。


3、Parallel Scavenge收集器

  适用:新生代收集器,在后台运算而不需要太多交互的任务。 
  特点: 1.多线程GC(并行
  2.在GC时暂停所有用户线程

与其他收集器的不同: 
  1).ParNew,CMS等收集器的关注点在于尽可能缩短垃圾收集时用户线程的停顿时间;而Parallel Scavenge收集器的目标则是达到一个可控制的吞吐量。 
  吞吐量:运行代码时间/(运行用户代码时间+垃圾收集时间) 
  停顿时间越短就越适合需要与用户交互的程序,良好的响应速度能提升用户的体验;而高吞吐量则可以最高效率地利用CPU时间,尽快地完成程序的运算任务,主要适合在后台运算而不需要太多交互的任务。 
  2).Parallel Scavenge可采用GC自适应的调节策略(这是与另外两一个重要的区别)

参数:用于精确控制吞吐量 
  -XX:MaxGCPauseMillis 最大垃圾收集停顿时间 
  -XX:GCTimeRatio 垃圾收集时间与运行用户代码时间的比例=垃圾收集时间/运行用户代码时间,相当于是吞吐量的倒数。

  实现:降低GC停顿时间:牺牲吞吐量和新生代空间(减小新生代空间,GC频率变大,吞吐量降低)

GC自适应的调节策略 
  -XX:+UseAdaptiveSizePolicy 使用自适应的调节策略 即不需要指定新生代的大小,Eden与Surivior的比例,晋升老年代的年龄等细节参数,虚拟机自动根据根据当前系统的状态动态调整这些参数以提供最合适的停顿时间或最大的吞吐量。

  算法:采用复制算法 
  优点:高效 
  搭配:Parallel Old或Serial Old(MSC)


————————-老年代收集器—————————-

4、Serial Old收集器

适用 
  1).运行在Client模式下的虚拟机中的老年代 
  2).在Server模式下,它主要还有两大用途 
  ①.与Parallel Scavenge搭配 
  ②.作为CMS收集器的后备预案,在并发收集发生Concurrent Mode Failure的时候使用

特点: 1.单线程GC,Serial收集器的老年代版本 
  2.在GC时暂停所有用户线程 
算法:采用标记-整理算法 
优点:简单,高效 
缺点:GC时暂停线程带给用户不良体验 
搭配:Serial Old(MSC)或ParNew


5、Parallel Old收集器

适用:运行在Server模式下的虚拟机中的新生代.在注重吞吐量及CPU资源敏感的场合,都可以优先考虑Parallel Scavenge加Parallel Old收集器。 
特点 
  1).多线程GC(并行):Parallel Scavenge的老年代版本 
  2).在GC时暂停所有用户线程 
  3).这个收集器是在JDK 1.6中才开始提供的

算法:采用标记-整理算法 
优点:高效 
缺点:GC时暂停线程带给用户不良体验,单线程下效果不一定优于Serial 
搭配:Parallel Scavenge


6、CMS(Concurrent Mark Sweep)收集器

  Hotspot上第一个真正意义上的并发收集器。 
  CMS(Concurrent Mark Sweep)收集器是一种以获取最短回收停顿时间为目标的收集器。目前很大一部分的Java应用都集中在互联网站或B/S系统的服务端上,这类应用尤其重视服务的响应速度,希望系统停顿时间最短,以给用户带来较好的体验。

适用:运行在Server模式下的虚拟机中的老年代,适合对响应时间要求高的应用。 
算法:采用“标记-清除”算法 
特点: 多线程 并发

过程: 
  1).初始标记:暂停用户线程,标记GC Roots能直接关联的对象,速度很快 
  2).并发标记:用户线程与标记线程并发,进行GC Roots Tracing的过程 
  3).重新标记:为了修正并发标记期间,因用户程序继续运作而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段稍长一些,但远比并发标记的时间短。 
  4).并发清除:用户线程与清除线程并发。 
  其中初始标记、重新标记这两个步骤仍然需要“Stop The World”。 
  由于整个过程中耗时最长的并发标记和并发清除过程中,收集器线程都可以与用户线程一起工作,所以总体上来说,CMS收集器的内存回收过程是与用户线程一起并发地执行的。 
  通过过图3-10可以比较清楚地看到CMS收集器的运作步骤中并发和需要停顿的时间。 
  

优点:并发收集、低停顿–由于耗时最长的并发标记和并发清除阶段都与用户线程并行工作,故系统停顿时间极短。 
缺点: 
  1).对CPU资源非常敏感。 
  原因:面向并发设计的程序都对CPU资源比较敏感。并发时,因为占用了一部分线程(或者说CPU资源)而导致应用程序变慢,总吞吐量会降低,应用程序会变慢,当CPU数不足时,尤其明显。 
  解决:增量式并发收集器(i-CMS):在并发标记、清除时让GC线程与用户线程交替运行,以降低GC线程独占CPU的时间。当GC时间将变长时,效果一般,被丢弃使用。

  2).无法处理浮动垃圾,可能出现“Concurrent Mode “Failure”失败而导致另一次Full GC的产生。 
  浮动垃圾:在并发清除阶段,用户线程仍在运行,此时产生的垃圾无法在该次收集中处理。 
  同时由于要保证并发,就必须预留内存给用户线程使用,因此CMS无法等到老年代几乎完全填满时再进行收集。JDK 1.5中CMS默认当老年代被使用68%时被激发。1.6中为92%。 
  当CMS运行期间预留的内存无法满足程序需要,就会出现一次“Concurrent Mode “Failure”失败,这时虚拟机将启动后备预案:临时使用Serial Old收集器来重新进行老年代垃圾收集,这样停顿时间就会很长。

  3).产生空间碎片,影响大对象的分配。 
  这是由于该收集器是由“标记-清除”算法实现的所引起的。所以往往存在有很大空间剩余,当无法找到足够大的连续空间来分配当前对象,不得不提前出发一次Full GC。 
  解决: 
  1.-XX:+UseCMSCompactFullCollection 开关参数(默认开启)用于当CMS要进行Full GC时开启内存碎片的合并整理过程,该过程不能并发,故停顿时间变长。 
  2.-XX:CMSFullGCsBeforeCompaction 用于设置执行多少次不压缩的Full GC后跟着来一次带压缩的Full GC。默认为0,表示每次进入Full GC时都进行碎片整理。

  搭配:Serial或ParNew


—————————新生代和老年代均适用———————

7、G1收集器

适用:面向服务端应用,适用于新生代和老年代。当前收集器技术发展的最前沿成果 
特点: 
  1.并行+并发。可充分利用CPU资源 
  2.分代收集。 
  3.空间整合。 G1从整体看是”标记-整理“算法,从局部(两个Region之间)看,是”复制“算法。 不会产生空间碎片。 
  4.可预测的停顿。建立可预测的态度时间模型,能让使用者明确指定在一个长度为M毫秒的时间内,消耗在垃圾收集的时间不得超过N毫秒,这几乎已经是实时Java(RTSJ)的垃圾收集器的特征了。

Garbage First名称的由来 
  G1收集器可以实现在基本不牺牲吞吐量的前提下完成低停顿的内存回收,这是由于它能够极力地避免全区域的垃圾收集。G1将内存划分为Region,跟踪各个Region里面的垃圾堆积的价值大小(回收所获得的空间大小以及回收所需时间的经验值),在后台维护一个优先列表,每次根据允许的收集时间,优先回收价值最大的Region。

  难点:虽然内存分为Region,但垃圾收集不能真的以Region为单位进行,因为Region不可能是孤立的,存在某个对象被多个Region的引用,那在做可达性判断确定对象是否存活时,是否需要扫描整个堆空间呢?注意:此问题在所有的收集器中都存在(如存在新生代与老年代之间的引用)。 
  解决:1.使用Remembered Set来避免圈堆扫描。 
  过程:G1中每个Region都有一个与之对应的Remembered Set,虚拟机发现程序在对Reference类型的数据进行写操作是,会产生一个Write Barrier暂时中断操作,检查Reference类型引用的对象是否处于不同的Region(在分代的例子中就是检查是否老年代的对象引用了新生代中的对象),如果是,便通过CardTable把相关引用信息记录到被引用对象所属的Region的Remembered Set中。当进行内存回收时,在GC根节点的枚举范围中加入Remembered Set即可保证不对全堆扫描也不会有遗漏。

  内存布局:G1的堆内存布局与其他收集器不同,G1将整个堆内存空间划分为多个大小相等的Region,虽然仍然有新生代和老年代的概念,但是新生代和老年代不再是物理隔离的,他们都是一部分Region(不需要连续)的集合。

过程(与CMS相似) 
  1.初始标记:暂停用户线程,标记GC Roots能直接关联的对象 
  2.并发标记:用户线程与标记线程并发,进行GC Roots的Trace 
  3.最终标记修正并发标记阶段,因用户线程继续运行而导致标记产生变动的那一部分对象的标记记录。 
  4.筛选回收:

  算法: 全局标记-整理+局部复制算法 
  优点:高效,停顿时间可控、可预测 
   
8、GC 相关参数总结 
1. 与串行回收器相关的参数
 
  -XX:+UseSerialGC:在新生代和老年代使用串行回收器。 
  -XX:+SuivivorRatio:设置 eden 区大小和 survivor 区大小的比例。 
  -XX:+PretenureSizeThreshold:设置大对象直接进入老年代的阈值。当对象的大小超过这个值时,将直接在老年代分配。 
  -XX:MaxTenuringThreshold:设置对象进入老年代的年龄的最大值。每一次 Minor GC 后,对象年龄就加 1。任何大于这个年龄的对象,一定会进入老年代。 
   
2. 与并行 GC 相关的参数 
  -XX:+UseParNewGC: 在新生代使用并行收集器。 
  -XX:+UseParallelOldGC: 老年代使用并行回收收集器。 
  -XX:ParallelGCThreads:设置用于垃圾回收的线程数。通常情况下可以和 CPU 数量相等。但在 CPU 数量比较多的情况下,设置相对较小的数值也是合理的。 
  -XX:MaxGCPauseMills:设置最大垃圾收集停顿时间。它的值是一个大于 0 的整数。收集器在工作时,会调整 Java 堆大小或者其他一些参数,尽可能地把停顿时间控制在 MaxGCPauseMills 以内。 
  -XX:GCTimeRatio:设置吞吐量大小,它的值是一个 0-100 之间的整数。假设 GCTimeRatio 的值为 n,那么系统将花费不超过 1/(1+n) 的时间用于垃圾收集。 
  -XX:+UseAdaptiveSizePolicy:打开自适应 GC 策略。在这种模式下,新生代的大小,eden 和 survivor 的比例、晋升老年代的对象年龄等参数会被自动调整,以达到在堆大小、吞吐量和停顿时间之间的平衡点。

3. 与 CMS 回收器相关的参数 
  -XX:+UseConcMarkSweepGC: 新生代使用并行收集器,老年代使用 CMS+串行收集器。 
  -XX:+ParallelCMSThreads: 设定 CMS 的线程数量。 
  -XX:+CMSInitiatingOccupancyFraction:设置 CMS 收集器在老年代空间被使用多少后触发,默认为 68%。 
  -XX:+UseFullGCsBeforeCompaction:设定进行多少次 CMS 垃圾回收后,进行一次内存压缩。 
  -XX:+CMSClassUnloadingEnabled:允许对类元数据进行回收。 
  -XX:+CMSParallelRemarkEndable:启用并行重标记。 
  -XX:CMSInitatingPermOccupancyFraction:当永久区占用率达到这一百分比后,启动 CMS 回收 (前提是-XX:+CMSClassUnloadingEnabled 激活了)。 
  -XX:UseCMSInitatingOccupancyOnly:表示只在到达阈值的时候,才进行 CMS 回收。 
  -XX:+CMSIncrementalMode:使用增量模式,比较适合单 CPU。

4. 与 G1 回收器相关的参数 
  -XX:+UseG1GC:使用 G1 回收器。 
  -XX:+UnlockExperimentalVMOptions:允许使用实验性参数。 
  -XX:+MaxGCPauseMills:设置最大垃圾收集停顿时间。 
  -XX:+GCPauseIntervalMills:设置停顿间隔时间。

5. 其他参数 
  -XX:+DisableExplicitGC: 禁用显示 GC。


四、内存分配与回收策略

  Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题: 
  1).给对象分配内存; 
  2).回收分配给对象的内存。关于回收内存这一点请参考如下文章 
  内存泄露是指该内存空间使用完毕之后未回收,在不涉及复杂数据结构的一般情况下,Java 的内存泄露表现为一个内存对象的生命周期超出了程序需要它的时间长度,我们有时也将其称为“对象游离”。 
  

Jvm怎么判断对象可以回收了? 
  1).对象没有引用,被判定为 “死亡”(并不是这个对象被赋值为null之后就一定被标记为可回收) 
  2).作用域发生未捕获异常 
  3).程序在作用域正常执行完毕 
  4).程序执行了System.exit() 
  5).程序发生意外终止(被杀进程等)

  对象的内存分配,往大方向上讲,就是在堆上分配(但也可能经过JIT编译后被拆散为标量类型并间接地在栈上分配),对象主要分配在新生代的Eden区上,如果启动了本地线程分配缓冲,将按线程优先在TLAB上分配。少数情况下也可能会直接分配在老年代中,分配的规则并不是百分之百固定的,其细节取决于当前使用的是哪一种垃圾收集器组合,还有虚拟机中与内存相关的参数的设置。 
  分配前请先了解下新年代和老年代:

1、对象优先在Eden分配

  大多数情况下,对象在新生代Eden区中分配。当Eden区没有足够的空间进行分配时,虚拟机将发起一次Minor GC。 
  下面代码的testAllocation()方法中,尝试分配3个2MB大小和1个4MB大小的对象,在运行时通过-Xms20M、 -Xmx20M和 -Xmn10M这3个参数限制Java堆大小为20MB,且不可扩展,其中10MB分配给新生代,剩下的10MB分配给老年代。-XX:SurvivorRatio=8决定了新生代中Eden区与一个Survivor区的空间比例是8比1,从输出的结果也能清晰地看到“eden space 8192K、from space 1024K、to space 1024K”的信息,新生代总可用空间为9216KB(Eden区+1个Survivor区的总容量)。

    private static final int _1MB = 1024 * 1024;

    /**
     * VM参数:
     * -verbose:gc
     * -Xms20M (初始堆大小为20M)
     * -Xmx20M (最大堆大小为20M)
     * -Xmn10M (新生代大小,初阶可用为9M大小)
     * -XX:+PrintGCDetails (打印 GC 信息)
     * -XX:SurvivorRatio=8 (新生代中 Eden 与 Survivor 的比值)
     */
    public static void testAllocation() {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation4 = new byte[4 * _1MB];  // 出现一次Minor GC
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
    [GC [DefNew: 6651K->148K(9216K), 0.0070106 secs]6651K->6292K(19456K), 0.0070426 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]  
    Heap  def new generation   total 9216K, used 4326K [0x029d0000, 0x033d0000, 0x033d0000)
    eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000)  
    from space 1024K,  14% used [0x032d0000, 0x032f5370, 0x033d0000)  
    to   space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000)  
    tenured generation   total 10240K, used 6144K [0x033d0000, 0x03dd0000, 0x03dd0000)  
    the space 10240K,  60% used [0x033d0000, 0x039d0030, 0x039d0200, 0x03dd0000)  
    compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)  
    the space 12288K,  17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)  
    No shared spaces configured.
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

  执行testAllocation()中分配allocation4对象的语句时会发生一次Minor GC,这次GC的结果是新生代6651KB变为148KB,而总内存占用量则几乎没有减少(因为allocation1、2、3三个对象都是存活的,虚拟机几乎没有找到可回收的对象)。这次GC发生的原因是给allocation4分配内存的时候,发现Eden已经被占用了6MB,剩余空间已不足(只有2M了)以分配allocation4所需的4MB内存,因此发生Minor GC。GC期间虚拟机又发现已有的3个2MB大小的对象全部无法放入Survivor空间(Survivor空间只有1MB大小),所以只好通过分配担保机制提前转移到老年代去。 
  这次GC结束后,4MB的allocation4对象被顺利分配在Eden中。因此程序执行完的结果是Eden占用4MB(被allocation4占用),Survivor空闲,老年代被占用6MB(被allocation1、2、3占用)。通过GC日志可以证实这一点。

  Java 中的堆也是 GC 收集垃圾的主要区域。GC 分为两种:Minor GC、Full GC ( 或称为 Major GC )。 
  1).Minor GC 是发生在新生代中的垃圾收集动作,所采用的是复制算法。 
  新生代几乎是所有 Java 对象出生的地方,即 Java 对象申请的内存以及存放都是在这个地方。Java 中的大部分对象通常不需长久存活,具有朝生夕灭的性质。 
  当一个对象被判定为 “死亡” 的时候,GC 就有责任来回收掉这部分对象的内存空间。新生代是 GC 收集垃圾的频繁区域,一般回收速度也比较快。

  2).Major GC / Full GC 是发生在老年代的垃圾收集动作,所采用的是标记-清除算法。 
  现实的生活中,老年代的人通常会比新生代的人 “早死”。堆内存中的老年代(Old)不同于这个,老年代里面的对象几乎个个都是在 Survivor 区域中熬过来的,它们是不会那么容易就 “死掉” 了的。因此,Full GC 发生的次数不会有 Minor GC 那么频繁,并且做一次 Full GC 要比进行一次 Minor GC 的时间更长。MajorGC的速度一般会比Minor GC慢10倍以上。 
  出现了Major GC,经常会伴随至少一次的Minor GC(但非绝对的,在ParallelScavenge收集器的收集策略里就有直接进行Major GC的策略选择过程)。 
  另外,标记-清除算法收集垃圾的时候会产生许多的内存碎片 ( 即不连续的内存空间 ),此后需要为较大的对象分配内存空间时,若无法找到足够的连续的内存空间,就会提前触发一次 GC 的收集动作。


2、大对象直接进入老年代

  所谓大对象就是指,需要大量连续内存空间的Java对象,最典型的大对象就是那种很长的字符串及数组(笔者例子中的byte[]数组就是典型的大对象)。大对象对虚拟机的内存分配来说就是一个坏消息(替Java虚拟机抱怨一句,比遇到一个大对象更加坏的消息就是遇到一群“朝生夕灭”的“短命大对象”,写程序的时候应当避免),经常出现大对象容易导致内存还有不少空间时就提前触发垃圾收集以获取足够的连续空间来“安置”它们。

    private static final int _1MB = 1024 * 1024;

    /**
     * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M -XX:SurvivorRatio=8
     *
     * -XX:PretenureSizeThreshold=3145728
     * (该参数令大于这个设置值的对象直接在老年代中分配,
     * 这样做的目的是避免在Eden区及两个Survivor区之间发生大量的内存拷贝(
     * 复习一下:新生代采用复制算法收集内存)。)
     */
    public static void testPretenureSizeThreshold() {
        byte[] allocation;
        allocation = new byte[4 * _1MB];  //直接分配在老年代中
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
    Heap  def new generation   total 9216K, used 671K[0x029d0000, 0x033d0000, 0x033d0000)  
    eden space 8192K,   8% used [0x029d0000, 0x02a77e98, 0x031d0000)  
    from space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000)  
    to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)  
    tenured generation   total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000)  
    the space 10240K,  40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000)  
    compacting perm gen  total 12288K, used 2107K [0x03dd0000, 0x049d0000, 0x07dd0000)  
    the space 12288K,  17% used [0x03dd0000, 0x03fdefd0, 0x03fdf000, 0x049d0000)  
    No shared spaces configured.
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

  执行代码清单中的testPretenureSizeThreshold()方法后,我们看到Eden空间几乎没有被使用,而老年代10MB的空间被使用了40%,也就是4MB的allocation对象直接就分配在老年代中,这是因为PretenureSizeThreshold被设置为3MB(就是3145728B,这个参数不能与-Xmx之类的参数一样直接写3MB),因此超过3MB的对象都会直接在老年代中进行分配。 
  注意: 
  PretenureSizeThreshold参数只对Serial和ParNew两款收集器有效,Parallel Scavenge收集器不认识这个参数,Parallel Scavenge收集器一般并不需要设置。如果遇到必须使用此参数的场合,可以考虑ParNew加CMS的收集器组合。


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

  虚拟机既然采用了分代收集的思想来管理内存,那内存回收时就必须能识别哪些对象应当放在新生代,哪些对象应放在老年代中。为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并将对象年龄设为1。对象在Survivor区中每熬过一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁)时,就会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold来设置。 
  读者可以试试分别以-XX:MaxTenuringThreshold=1和-XX:MaxTenuringThreshold=15两种设置来执行代码清单3-5中的testTenuringThreshold()方法,此方法中allocation1对象需要256KB的内存空间,Survivor空间可以容纳。当MaxTenuringThreshold=1时,allocation1对象在第二次GC发生时进入老年代,新生代已使用的内存GC后会非常干净地变成0KB。而MaxTenuringThreshold=15时,第二次GC发生后,allocation1对象则还留在新生代Survivor空间,这时候新生代仍然有404KB的空间被占用。

    private static final int _1MB = 1024 * 1024;

    /**
     * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M
     * -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=1
     * -XX:+PrintTenuringDistribution
     */
    @SuppressWarnings("unused")
    public static void testTenuringThreshold() {
        byte[] allocation1, allocation2, allocation3;
        allocation1 = new byte[_1MB / 4];//需要256k内存,Survivor空间可以容纳
        // 什么时候进入老年代取决于XX:MaxTenuringThreshold设置
        allocation2 = new byte[4 * _1MB];
        allocation3 = new byte[4 * _1MB];
        allocation3 = null;
        allocation3 = new byte[4 * _1MB];
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
    [GC [DefNew  Desired Survivor size 524288 bytes, new threshold 1 (max 1)  - age   1:     414664 bytes,     414664 total   : 4859K->404K(9216K), 0.0065012 secs] 4859K->4500K(19456K), 0.0065283 secs] [Times: user=0.02 sys=0.00, real=0.02 secs]
    [GC [DefNew  Desired Survivor size 524288 bytes, new threshold 1 (max 1)  : 4500K->0K(9216K), 0.0009253 secs] 8596K->4500K(19456K), 0.0009458 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]  
    Heap  
     def new generation   total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000)  
     eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000)  
     from space 1024K,   0% used [0x031d0000,0x031d0000, 0x032d0000)  
     to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)  
    tenured generation   total 10240K, used 4500K [0x033d0000, 0x03dd0000, 0x03dd0000)  
      the space 10240K,  43% used [0x033d0000, 0x03835348, 0x03835400, 0x03dd0000)  
    compacting perm gen  total 12288K, used 2114K[0x03dd0000, 0x049d0000, 0x07dd0000)  
      the space 12288K,  17% used [0x03dd0000, 0x03fe0998, 0x03fe0a00, 0x049d0000)  
    No shared spaces configured.
以MaxTenuringThreshold=15的参数设置来运行的结果:
    [GC [DefNew  Desired Survivor size 524288 bytes, new threshold 15 (max 15)  - age   1:     414664 bytes,     414664 total  : 4859K->404K(9216K), 0.0049637 secs] 4859K->4500K(19456K), 0.0049932 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]  
    [GC [DefNew  Desired Survivor size 524288 bytes, new threshold 15 (max 15)  - age   2:     414520 bytes,     414520 total  : 4500K->404K(9216K), 0.0008091 secs] 8596K->4500K(19456K), 0.0008305 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]  
    Heap  
    def new generation   total 9216K, used 4582K [0x029d0000, 0x033d0000, 0x033d0000)  
     eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000)  
     from space 1024K,  39% used [0x031d0000, 0x03235338, 0x032d0000)  
     to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)  
    tenured generation   total 10240K, used 4096K [0x033d0000, 0x03dd0000, 0x03dd0000)  
      the space 10240K,  40% used [0x033d0000, 0x037d0010, 0x037d0200, 0x03dd0000)  
    compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)  
      the space 12288K,  17% used [0x03dd0000,0x03fe0998, 0x03fe0a00, 0x049d0000)  
    No shared spaces configured.
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

4、动态对象年龄判定

  为了能更好地适应不同程序的内存状况,虚拟机并不总是要求对象的年龄必须达到MaxTenuringThreshold才能晋升老年代,如果在Survivor空间中相同年龄所有对象大小的总和大于Survivor空间的一半,年龄大于或等于该年龄的对象就可以直接进入老年代,无须等到MaxTenuringThreshold中要求的年龄。 
  执行代码清单中的testTenuringThreshold2()方法,并设置参数-XX: MaxTenuringThreshold=15,会发现运行结果中Survivor的空间占用仍然为0%,而老年代比预期增加了6%,也就是说allocation1、allocation2对象都直接进入了老年代,而没有等到15岁的临界年龄。因为这两个对象加起来已经达到了512KB,并且它们是同年的,满足同年对象达到Survivor空间的一半规则。我们只要注释掉其中一个对象的new操作,就会发现另外一个不会晋升到老年代中去了。

    private static final int _1MB = 1024 * 1024;

    /**
     * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M
     * -XX:SurvivorRatio=8 -XX:MaxTenuringThreshold=15 
     * -XX:+PrintTenuringDistribution  
     */
    @SuppressWarnings("unused")
    public static void testTenuringThreshold2() {
        byte[] allocation1, allocation2, allocation3, allocation4;
        allocation1 = new byte[_1MB / 4];
        // allocation1+allocation2大于survivor空间的一半  
        allocation2 = new byte[_1MB / 4];
        allocation3 = new byte[4 * _1MB];
        allocation4 = new byte[4 * _1MB];
        allocation4 = null;
        allocation4 = new byte[4 * _1MB];
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
    [GC [DefNew  Desired Survivor size 524288 bytes, new threshold 1 (max 15)  - age   1:     676824 bytes,     676824 total  : 5115K->660K(9216K), 0.0050136 secs] 5115K->4756K(19456K), 0.0050443 secs] [Times: user=0.00sys=0.01, real=0.01 secs]  
    [GC [DefNew  Desired Survivor size 524288 bytes, new threshold 15 (max 15)  : 4756K->0K(9216K), 0.0010571 secs] 8852K->4756K(19456K), 0.0011009 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]  
    Heap  
    def new generation   total 9216K, used 4178K [0x029d0000, 0x033d0000, 0x033d0000)  
     eden space 8192K,  51% used [0x029d0000, 0x02de4828, 0x031d0000)  
     from space 1024K,   0% used [0x031d0000, 0x031d0000, 0x032d0000)  
     to   space 1024K,   0% used [0x032d0000, 0x032d0000, 0x033d0000)  
    tenured generation   total 10240K, used 4756K [0x033d0000, 0x03dd0000, 0x03dd0000)  
      the space 10240K,  46% used [0x033d0000, 0x038753e8, 0x03875400, 0x03dd0000)  
    compacting perm gen  total 12288K, used 2114K [0x03dd0000, 0x049d0000, 0x07dd0000)  
      the space 12288K,  17% used [0x03dd0000, 0x03fe09a0, 0x03fe0a00, 0x049d0000)  
    No shared spaces configured.
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12

5、空间分配担保

  在发生Minor GC前,虚拟机会先检查老年代最大可用的连续空间是否大于新生代所有对象总空间。 
  1).如果大于,那么Minor GC可以确保是安全的。 
  2).如果小于,虚拟机会查看HandlePromotionFailure设置值是否允许担任失败。 
   a.如果允许,那么会继续检查老年代最大可用连续空间是否大于历次晋升老年代对象的平均大小 
    ①.如果大于,将尝试着进行一次Minor GC,尽管这次Minor GC是有风险的 
    ②.如果小于,进行一次Full GC. 
   b.如果不允许,也要改为进行一次Full GC.

  前面提到过,新生代使用复制收集算法,但为了内存利用率,只使用其中一个Survivor空间来作为轮换备份,因此当出现大量对象在Minor GC后仍然存活的情况时(最极端就是内存回收后新生代中所有对象都存活),就需要老年代进行分配担保,让Survivor无法容纳的对象直接进入老年代。与生活中的贷款担保类似,老年代要进行这样的担保,前提是老年代本身还有容纳这些对象的剩余空间,一共有多少对象会活下来,在实际完成内存回收之前是无法明确知道的,所以只好取之前每一次回收晋升到老年代对象容量的平均大小值作为经验值,与老年代的剩余空间进行比较,决定是否进行Full GC来让老年代腾出更多空间。

  取平均值进行比较其实仍然是一种动态概率的手段,也就是说如果某次Minor GC存活后的对象突增,远远高于平均值的话,依然会导致担保失败(Handle Promotion Failure)。如果出现了HandlePromotionFailure失败,那就只好在失败后重新发起一次Full GC。虽然担保失败时绕的圈子是最大的,但大部分情况下都还是会将HandlePromotionFailure开关打开,避免Full GC过于频繁,参见代码。

    private static final int _1MB = 1024 * 1024;

    /**
     * VM参数:-verbose:gc -Xms20M -Xmx20M -Xmn10M
     * -XX:SurvivorRatio=8 -XX:-HandlePromotionFailure
     */
    @SuppressWarnings("unused")
    public static void testHandlePromotion() {
        byte[] allocation1, allocation2, allocation3,
                allocation4, allocation5, allocation6, allocation7;
        allocation1 = new byte[2 * _1MB];
        allocation2 = new byte[2 * _1MB];
        allocation3 = new byte[2 * _1MB];
        allocation1 = null;
        allocation4 = new byte[2 * _1MB];
        allocation5 = new byte[2 * _1MB];
        allocation6 = new byte[2 * _1MB];
        allocation4 = null;
        allocation5 = null;
        allocation6 = null;
        allocation7 = new byte[2 * _1MB];
    }
 
 
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22

  以HandlePromotionFailure = false的参数设置来运行的结果:

    [GC [DefNew: 6651K->148K(9216K), 0.0078936 secs] 6651K->4244K(19456K), 0.0079192 secs] [Times: user=0.00 sys=0.02, real=0.02 secs]  
    [GC [DefNew: 6378K->6378K(9216K), 0.0000206 secs][Tenured: 4096K->4244K(10240K), 0.0042901 secs] 10474K->4244K(19456K), [Perm : 2104K->2104K(12288K)], 0.0043613 secs] [Times: user=0.00 sys=0.00, real=0.00 secs] 
 
 
  • 1
  • 2

  以MaxTenuringThreshold= true的参数设置来运行的结果:

    [GC [DefNew: 6651K->148K(9216K), 0.0054913 secs] 6651K->4244K(19456K), 0.0055327 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]  
    [GC [DefNew: 6378K->148K(9216K), 0.0006584 secs] 10474K->4244K(19456K), 0.0006857 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
 
 
  • 1
  • 2

  在JDK6 Update24之后 ,这个测试会有差异,HandlePromotionFailure参数不会再影响到虚拟机的空间分配担保策略。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值