Java的垃圾回收器

原文在 http://www.oracle.com/technetwork/java/javase/gc-tuning-6-140523.html ,下面是个人的一些理解,可能有不对的地方。

  • 如果不指定-client或-server参数,JVM会根据系统状况自动判断使用client还是server模式,具体见这里。有趣的是,AMD64的CPU默认就是server模式(经测试,windows下仍然默认client模式),其他CPU时,除windows默认为client模式外,其他>=2C2G的电脑都会默认为server模式。
  • 如果不指定使用哪种垃圾回收器,JVM会根据应用程序运行的硬件情况(CPU/内存)来自动决定使用哪种GC、堆栈大小以及运行时编译器,也就是说,如果运行应用程序时不指定GC的参数,那么不同的硬件上使用的垃圾回收器可能是不同的。
  • 可以通过指定应用允许的最大暂停时间(full GC时程序会失去响应)和吞吐量来调节GC,而不是仅仅通过指定堆栈大小等参数来调节。
  • JVM中内存区域分为新生代、年老代和持久代,其中新生代又分为eden和两个 survivor 区,如下图(适用于 serial collector ):

大多数对象创建时,先存放在eden区,当eden区满了的时候,执行一次部分GC,将存活的对象拷贝到survivor 区。
两个survivor 区中,其中有一个始终为空的状态,执行部分GC时,另一个survivor 区以及eden区中的live对象会被全部拷贝到这个区,这样来回移动移动数次,仍然存活的对象将拷贝到年老代。至于一个对象需要拷贝多少次才能移动到年老代,是在每次GC时由JVM决定的,JVM决定这个值的大小,以确保survivor区处于半满状态(其实根据实际监控来看,应该是保持在大半满靠全满状态),通过XX:+PrintTenuringDistribution可以查看该数值。在Jconsole中,查看survivor 区的时候,只能看到一个区的大小,这是因为永远只有一个survivor 区处于可用状态。
持久代中主要存放程序定义的类、方法等,不属于堆(也有人说是一个特殊的堆,不过从jconsole上看,应该是Non-heap memory),不参与GC。
在程序运行过程中,Eden和survivor区的capacity大小在每次执行GC后都可能发生变化。
上图中Virtual区是指,当-Xms的值比-Xmx的值小时,JVM预留的一部分尚未交付给自己使用的内存。
  • 在启动参数中加上 -verbose:gc 可以打印出每次GC释放的空间以及耗时。
  • JVM GC相关参数:
  • -XX:MinHeapFreeRatio=<minimum>  -XX:MaxHeapFreeRatio=<maximum>   -Xms<min>  -Xmx<max> 当某一代的内存超出了Min/MaxHeapFreeRatio的比例时(默认为40%-70%),JVM会自动伸缩该代内存大小以确保在指定比例范围内,但自动伸缩不会超出该代允许的内存大小范围。由于系统默认的-Xmx和-Xmx都比较小,对于服务器程序,强烈建议启动时指明-Xms和-Xmx的值,并且这两个值相等,这样可以避免程序自己决定内存大小带来的开销,当然,如果这个值设置得不合适,JVM就无能为力了。
  • – XX:NewRatio=<3> 指定新生代和年老代在堆内存中的比例(1:3),即新生代占总heap区的1/4。 NewSize 和 MaxNewSize 可用来指定新生代的实际内存大小。
  • -XX:SurvivorRatio=<6> 指定eden区和每个survivor区的比例(1:6),即每个survivor占新生代的1/8(这里感觉有点奇怪,感觉应该是survivor比eden=1:6,但是官方文档这么说: sets the ratio between eden and a survivor space to 1:6 )。一般来说,调节这个参数对性能调优不是太重要。如果survivor区太小,GC时不够用来装来自eden区的live对象,那么这些对象会直接溢出到年老代,如果这个区太大,那么将会浪费掉一部分内存。
垃圾回收器的类别:
  • serial collector 使用单线程来完成GC操作。由于不涉及到线程之间的通信,所以非常高效。如果只有一个CPU核心,或者程序使用的内存不会超过100M时,建议使用这种GC。这种GC不能有效利用多核,当系统只有单核CPU时,JVM会默认使用这类GC。估计现在已经很少见了。
  • parallel collector ( throughput collector ) 在小GC时(新生代GC),可以显著减少GC的开销。在多核或者多线程的硬件环境下,当应用使用的内存为中到大的规模时,应该使用这类GC。当系统有多核的时候,JVM会默认使用这种GC。默认情况下,这类GC仅在小GC时使用多线程,JDK6中,可以使用 -XX:+UseParallelOldGC 的参数来让FULL GC(Old区GC)也使用多线程。使用的线程数等于系统的核数,或者用- XX:ParallelGCThreads=<N> 来指定。由于GC时可能有部分对象从新生代移动到年老代,所以每个GC线程都会预留一定的年老代空间,这样可能会产生年老代碎片,减少GC线程数可以减少这种碎片化的影响,增大年老代空间。
  • concurrent collector 通过牺牲应用性能来缩短GC时的服务停顿。使用 -XX:+UseConcMarkSweepGC 参数打开。
垃圾回收器的选择:
  • 如果应用程序占用的内存不超过100M: -XX:+UseSerialGC
  • 如果应用程序运行在单核服务器上并且对应用暂停服务没啥要求:  -XX:+UseSerialGC
  • 如果应用程序的峰值处理能力最重要,并且对应用暂停服务一秒种或者更长时间可以接受:  -XX:+UseParallelGC  -XX:+UseParallelOldGC 此类情况多出现在后台跑的业务中,非用户交互的系统。
  • 如果希望尽量缩短应用程序的暂停响应时间: -XX:+UseConcMarkSweepGC 此类情况较为常见,主要为涉及到用户交互的系统。由于可以通过多台服务负载均衡来弥补性能损失,而且可以保证服务不会在那里傻呆着几秒钟甚至几十秒钟来GC而没反应。
当使用并行GC时,JVM中内存分区和串行不同,如下图:
JVM的下一代G1垃圾回收器会逐渐取代传统的CMS垃圾回收器,但是JDK1.7.2目前还没有使用G1作为默认的垃圾回收器。G1的内存分布和上面的两种分布有非常大的不同,有兴趣的可以去看看。
系统的剩余可用内存空间可能会对垃圾回收性能产生严重影响。我有两台硬件完全相同的服务器,部署完全相同的应用,负载基本相同,但是垃圾回收的性能有本质差别,1台135次FULL GC耗时1分钟,另外一台82次FULL GC耗时34分钟。现在能找到比较明显的不同就是,一台剩余物理内存900M左右,另外一台剩余物理内存40M左右,不知道是不是由于这个原因造成了GC性能如此显著的区别。

发表评论

电子邮件地址不会被公开。 必填项已用*标注