IT技术博客大学习 共学习 共进步
全部 移动开发 后端 数据库 AI 算法 安全 DevOps 前端 设计 开发者

JVM的GC简介和实例

搜索技术博客-淘宝 2013-07-15 13:02:02 累计浏览 3,900 次
本机暂存

   本文是一次内部分享中总结了jvm gc的分类和一些实例, 内容是introduction级别的,供初学人士参考.

   成文仓促,难免有些错误,如果有大牛发现,请留言,我一定及时更正,谢谢!

   JVM内存布局主要包含下面几个部分:

  • Java Virtual Machine Stack: 也就是我们常见的局部变量栈,线程私有,保存线程执行的局部变量表、操作栈、动态连接等。

  • Java Heap:我们最常打交道的内存区域,几乎所有对象的实例都在这个区域分配。所谓的GC基本上也就是跟这个区域打交道。

  • Method Area:包含被虚拟机加载的类、常量、静态变量等数据。

  •    Hotspot虚拟机使用分代收集算法,将Java Heap根据对象的存活周期分为多个区域:新生代、老生代和永生代。

       新生代和老生代位于Java heap中,是垃圾收集器主要处理的内存区域。

       永生代则基本上等价于Method Area,也就是说其中包含的数据在jvm进程存活期间会一直存在,一般不会发生变化。

       java堆内存的布局如下图所示:

       jvm堆布局

       使用jstat可以查看某个java进程的内存状况:

chendeMacBook-Air:~ eleforest$ jstat -gc 16136
 S0C    S1C    S0U    S1U      EC       EU        OC         OU       PC     PU    YGC     YGCT    FGC    FGCT     GCT
1024.0 1024.0  0.0    0.0    8192.0   2867.9   10240.0      0.0     21248.0 2637.2      0    0.000   0      0.000    0.000

   其中各个指标介绍如下:(单位为KB)

  • S0C,S1C,S0U,S1U: 0/1幸存区(survivor)容量(C:Capacity)/使用量(U:Used)。

  • EC,EU: Eden(伊甸)区容量/用量。Eden和survivor两个区域位于新生代,由于新生代GC一般是使用复制算法进行清理,因此按照复制算法的原理将新生代分成了3个区域:Eden、Survivor0、Survivor1。Hotspot虚拟机的3个空间缺省配比为:8:1:1,jvm只会使用eden和1个survivor作为新生代空间.当新生代空间不足时发生minor gc,此时根据复制算法, jvm会首先 1)将eden和from survivor中存活的对象拷贝到to survior中,然后2)释放eden和from中的所有需要回收对象,最后3)调换from/to survior,jvm将eden和新的from survior作为新生代。当然上述minor gc顺利执行还取决于很多因素,这里只描述了最理想化的状态。

  • OC,OU: Old(老生代)容量/用量。老生代常用的垃圾收集器有CMS、Serial Old、Parallel Old等

  • PC,PU: Perm(永生代)容量/用量。

  • YGC/YGCT: Young GC次数和总耗费时间。Young GC也就是Minor GC,新生代中内存不够时触发,通常采用复制算法进行,回收速度较快,对系统的影响较小。

  • FGC/FGCT:Full GC次数和总耗费时间。Full GC是在java heap空间不足(包括New和Old区域)时触发,会分别清理新生代、老生代,通常耗时较长,对系统有较大影响,应该尽量避免。

  • GCT:GC总耗时。

  •    常用的垃圾收集器包括下面几个

  • Serial:最基本,历史最悠久的收集器,单线程收集垃圾内存,在新生代采用复制算法,在老生代使用标记-整理算法

  • ParNew:Serial的多线程版本,主要用于新生代收集。与CMS收集器配合成为现在最常用的server收集器

  • Parallel Scavenge:也是一个并行收集器,使用与ParNew完全不同的收集策略,具体的差别还在研究中

  • CMS:Concurrent Mark Sweep收集器,大名鼎鼎,其目标是获取最短回收停顿时间,是server模式下最常用的收集器

  • G1:最新的收集器,木有用过啊

  •    下面将会用一段简单的程序演示jvm在配置使用不同的收集器情况下,GC行为的不同点,通过GC的行为能够了解到不同收集器的收集策略和行为。代码非常简单:

    //jvm basic args:-Xmx20M -Xms20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8
    public class Main {
        public static void main(String[] args) throws Exception {
        byte[] alloc1,alloc2,alloc3,alloc4;
        alloc1 = new byte[2*1024*1024];
        Thread.sleep(2000);
        alloc2 = new byte[2*1024*1024];
        Thread.sleep(2000);
        alloc3 = new byte[2*1024*1024];
        Thread.sleep(2000);
        alloc4 = new byte[2*1024*1024];
        Thread.sleep(2000);
        }
    }

       其中上例中的jvm参数解释如下:

    Xmx最大堆容量,包含了新生代和老生代的堆容量
    Xms最小堆容量,此时配置与Xmx一样,避免了申请空间时的堆扩展
    Xmn新生代容量,包含eden,survivor1,survivor2三个区域
    PrintGCDetails让jvm在每次发生gc的时候打印日志,利于分析gc的原因和状况
    SurvivorRatio新生代中eden的比例,如果设置为8,意味着新生代中eden占据80%的空间,两个survivor分别占据10%

       测试环境为mac os 10.8,jdk版本如下:

    chendeMacBook-Air:~ eleforest$ java -version
    java version "1.7.0_09"
    Java(TM) SE Runtime Environment (build 1.7.0_09-b05)
    Java HotSpot(TM) 64-Bit Server VM (build 23.5-b02, mixed mode)
  • 示例1:让jvm自动选择收集器

  •    直接运行上述代码,用jstat观察gc情况如下:

    chendeMacBook-Air:~ eleforest$ jstat -gc 21729 1000
     S0C    S1C    S0U    S1U      EC       EU        OC         OU       PC     PU    YGC     YGCT    FGC    FGCT     GCT
    1024.0 1024.0  0.0    0.0    8192.0   819.9    10240.0      0.0     21248.0 2637.2      0    0.000   0      0.000    0.000
    1024.0 1024.0  0.0    0.0    8192.0   2867.9   10240.0      0.0     21248.0 2637.2      0    0.000   0      0.000    0.000
    1024.0 1024.0  0.0    0.0    8192.0   2867.9   10240.0      0.0     21248.0 2637.2      0    0.000   0      0.000    0.000
    1024.0 1024.0  0.0    0.0    8192.0   4915.9   10240.0      0.0     21248.0 2637.2      0    0.000   0      0.000    0.000
    1024.0 1024.0  0.0    0.0    8192.0   4915.9   10240.0      0.0     21248.0 2637.2      0    0.000   0      0.000    0.000
    1024.0 1024.0  0.0    0.0    8192.0   6963.9   10240.0      0.0     21248.0 2637.2      0    0.000   0      0.000    0.000
    1024.0 1024.0  0.0    0.0    8192.0   6963.9   10240.0      0.0     21248.0 2637.2      0    0.000   0      0.000    0.000
    1024.0 1024.0  0.0   292.9   8192.0   2375.9   10240.0     6144.0   21248.0 2640.3      1    0.007   0      0.000    0.007
    1024.0 1024.0  0.0   292.9   8192.0   2375.9   10240.0     6144.0   21248.0 2640.3      1    0.007   0      0.000    0.007

       由上述的结果可见,程序启动时,Eden使用了819.9K的空间(我现在还不知道819k是什么东西的开销),S1、S2、老生代均没有占用,永生代则使用了2.6MB空间,其中包含了包含被虚拟机加载的类、常量、静态变量等数据。

       随后连续三次申请了2MB的空间,这些数据都被放到了Eden区域,这就是jvm内存分配的第一个原则:对象优先在Eden分配,这个原则只在Eden空间足够,且申请的内存小于jvm参数PretenureSizeThreshold设置值时生效(根据采用的收集器不同,还会有很多不同情况)

       注意看第四次申请2MB空间,此时由于Eden空间无法容纳新的数组,因此发生了一次Minor GC,具体的GC log如下所示:

    [GC [DefNew: 6963K->292K(9216K), 0.0065350 secs] 6963K->6436K(19456K), 0.0065940 secs] [Times: user=0.01 sys=0.00, real=0.00 secs]
    Heap
     def new generation   total 9216K, used 2832K [0x0000000112230000, 0x0000000112c30000, 0x0000000112c30000)
      eden space 8192K,  31% used [0x0000000112230000, 0x00000001124aaf60, 0x0000000112a30000)
      from space 1024K,  28% used [0x0000000112b30000, 0x0000000112b793b0, 0x0000000112c30000)
      to   space 1024K,   0% used [0x0000000112a30000, 0x0000000112a30000, 0x0000000112b30000)
     tenured generation   total 10240K, used 6144K [0x0000000112c30000, 0x0000000113630000, 0x0000000113630000)
       the space 10240K,  60% used [0x0000000112c30000, 0x0000000113230030, 0x0000000113230200, 0x0000000113630000)
     compacting perm gen  total 21248K, used 2647K [0x0000000113630000, 0x0000000114af0000, 0x0000000118830000)
       the space 21248K,  12% used [0x0000000113630000, 0x00000001138c5ec0, 0x00000001138c6000, 0x0000000114af0000)
    No shared spaces configured.

       其中第一行中的"DefNew"代表使用的收集器是Serial收集器,这次Minor GC使用copy算法,做了下面几件事情:

  • 检索heap中的对象,将还能通过GC roots能够遍历到的对象copy到to区中

  • 如果需要copy的对象没法进入from区中,则将其晋升到老年代,本例中即发生了这种情况,3个2MB的数组全部晋升到老生代(OU:6144)

  • 清理eden和from中无用的垃圾

  • 互换from和to空间

  •    比较有意思的是,在我的机器上重新再跑一次示例程序,发生了不一致的gc行为:

    [GC [PSYoungGen: 6963K->384K(9216K)] 6963K->6528K(19456K), 0.0052500 secs] [Times: user=0.01 sys=0.00, real=0.01 secs]
    [Full GC [PSYoungGen: 384K->0K(9216K)] [ParOldGen: 6144K->6436K(10240K)] 6528K->6436K(19456K) [PSPermGen: 2637K->2635K(21248K)], 0.0157270 secs] [Times: user=0.04 sys=0.00, real=0.02 secs]
    HeapA
     PSYoungGen      total 9216K, used 2539K [0x00000001106d0000, 0x00000001110d0000, 0x00000001110d0000)
      eden space 8192K, 31% used [0x00000001106d0000,0x000000011094af60,0x0000000110ed0000)
      from space 1024K, 0% used [0x0000000110ed0000,0x0000000110ed0000,0x0000000110fd0000)
      to   space 1024K, 0% used [0x0000000110fd0000,0x0000000110fd0000,0x00000001110d0000)
     ParOldGen       total 10240K, used 6436K [0x000000010fcd0000, 0x00000001106d0000, 0x00000001106d0000)
      object space 10240K, 62% used [0x000000010fcd0000,0x0000000110319278,0x00000001106d0000)
     PSPermGen       total 21248K, used 2645K [0x000000010aad0000, 0x000000010bf90000, 0x000000010fcd0000)
      object space 21248K, 12% used [0x000000010aad0000,0x000000010ad65688,0x000000010bf90000)

       GC log第一行的PSYoungGen意味着这次运行中jvm自动选择了Parallel Scavenge收集器,GC行为发生了变化,同样的内存请求,PS收集器除了一次Minor GC以外,还发生了一次Full GC。PS收集器的实现与serial不一致,其行为模式还需要进一步研究.

       比较吊诡的是jvm的自动选择行为,我阅读了openjdk的源码,版本为:openjdk-7-fcs-src-b147-27_jun_2011

       其中关于jvm自动选择gc的代码如下:

    if (os::is_server_class_machine() && !force_client_mode ) {
      // If no other collector is requested explicitly,
      // let the VM select the collector based on
      // machine class and automatic selection policy.
      if (!UseSerialGC &&
          !UseConcMarkSweepGC &&
          !UseG1GC &&
          !UseParNewGC &&
          !DumpSharedSpaces &&
          FLAG_IS_DEFAULT(UseParallelGC)) {
        if (should_auto_select_low_pause_collector()) {//如果需要低时延收集器,选择cms
          FLAG_SET_ERGO(bool, UseConcMarkSweepGC, true);
        } else {//否则缺省使用ps收集器
          FLAG_SET_ERGO(bool, UseParallelGC, true);
        }
        no_shared_spaces();
      }
    }

       如上所示,jvm在没有明确设置gc时会采用parallel scavenge作为缺省收集器。因此我机器上jvm自动选择gc的行为还需要进一步研究。

  • 示例2:使用ParNew收集器

  •    调整jvm的参数,添加-XX:+UseParNewGC,告诉jvm选择使用ParNew收集器,此时执行的结果与示例1中使用serial收集器的行为完全一样。这里不再赘述

  • 示例3:使用CMS收集器

  •    调整jvm参数为:

    -Xmx20M -Xms20M -Xmn10M -XX:+PrintGCDetails -XX:SurvivorRatio=8 -XX:+UseConcMarkSweepGC</pre>
    此时启动示例程序,我们会看到如下的结果:
    <pre class="brush:shell">chendeMacBook-Air:~ eleforest$ jstat -gc 21729 1000
     S0C    S1C    S0U    S1U      EC       EU        OC         OU       PC     PU    YGC     YGCT    FGC    FGCT     GCT
    1024.0 1024.0  0.0    0.0    8192.0   820.2     8192.0      0.0     21248.0 2638.2      0    0.000   0      0.000    0.000
    1024.0 1024.0  0.0    0.0    8192.0   2868.2    8192.0      0.0     21248.0 2638.2      0    0.000   0      0.000    0.000
    1024.0 1024.0  0.0    0.0    8192.0   2868.2    8192.0      0.0     21248.0 2638.2      0    0.000   0      0.000    0.000
    1024.0 1024.0  0.0    0.0    8192.0   4916.3    8192.0      0.0     21248.0 2638.2      0    0.000   0      0.000    0.000
    1024.0 1024.0  0.0    0.0    8192.0   4916.3    8192.0      0.0     21248.0 2638.2      0    0.000   0      0.000    0.000
    1024.0 1024.0  0.0    0.0    8192.0   6964.3    8192.0      0.0     21248.0 2638.2      0    0.000   0      0.000    0.000
    1024.0 1024.0  0.0    0.0    8192.0   6964.3    8192.0      0.0     21248.0 2638.2      0    0.000   0      0.000    0.000
    1024.0 1024.0  0.0   320.1   8192.0   2375.9    8192.0     6146.1   21248.0 2641.2      1    0.009   2      0.001    0.010
    1024.0 1024.0  0.0   320.1   8192.0   2375.9    8192.0     6146.1   21248.0 2641.2      1    0.009   2      0.001    0.010
    1024.0 1024.0  0.0   320.1   8192.0   2375.9    8192.0     6146.1   21248.0 2641.2      1    0.009   4      0.002    0.011
    1024.0 1024.0  0.0   320.1   8192.0   2375.9    8192.0     6146.1   21248.0 2641.2      1    0.009   4      0.002    0.011
    1024.0 1024.0  0.0   320.1   8192.0   2375.9    8192.0     6146.1   21248.0 2641.2      1    0.009   6      0.003    0.012

       也就是说到第四个2MB申请,老生代里使用6MB的数据之后,jvm还进行了6次full gc,这是由于cms特殊性导致的:cms为了保证进行gc时应用的低时延,要求在老生代中剩余充足的空间以备应用使用。这个特性可以用下列参数进行调整和限制

    -XX:CMSInitiatingOccupancyFraction=80 -XX:+UseCMSInitiatingOccupancyOnly

       其中CMSInitiatingOccupancyFraction的缺省为68%。在我们的示例中,OU已经超过了这个限制,jvm试图去清理老生代,因此发生了多次full gc。

       通过修改CMSInitiatingOccupancyFraction为80或者更高值,再次执行示例程序后不会再发生fullGC。

       为了使应用平顺,CMS收集器的使用需要小心的调整堆空间的大小,太小的老生代可能会起到相反的效果,过高的CMSInitiatingOccupancyFraction也会导致回收数据时使应用无法正常工作。

       以上便是我在这篇博客中想要分享的内容,做一些记录,也分享出来。

       但是如分享中所说的,还有以下问题还没有搞清楚:

  • PS收集器的行为,触发full gc的条件

  • jvm自动选择收集器的策略

  • G1收集器的使用

  • 同分类推荐文章

    1. 等了十年的 Go 链式管道,终于来了:seq 让你像写 Scala 一样写 Go (2026-06-25 18:38:18)
    2. Go 实验特性详解 (2026-06-21 10:05:27)
    3. amd64 微架构级别对 Go 程序性能提升多少? (2026-06-21 09:38:49)

    查看更多 后端 文章 →

    建议继续学习

    1. SmartSprites - 命令行形式的CSS Sprites生成器 (累计阅读 123,894)
    2. Java开发岗位面试题归类汇总 (累计阅读 22,155)
    3. android 开发入门 (累计阅读 19,527)
    4. Linux内存点滴 用户进程内存空间 (累计阅读 13,228)
    5. 我的PHP,Python和Ruby之路 (累计阅读 13,146)
    6. HashMap解决hash冲突的方法 (累计阅读 12,652)
    7. Linux Used内存到底哪里去了? (累计阅读 11,866)
    8. Linux操作系统的内存使用方法详细解析 (累计阅读 10,148)
    9. 几个内存相关面试题(c/c++) (累计阅读 9,443)
    10. 一个大二学生有关如何成为一名软件工程师的疑问及答复 (累计阅读 9,178)