Java GC及JVM参数

参考资料

  1. http://www.233.com/Java/zhuanye/20101027/164729208-2.html
  2. http://www.jianshu.com/p/740f00cf03b2
  3. http://unixboy.iteye.com/blog/174173/
  4. http://uule.iteye.com/blog/2114697
  5. http://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html
  6. http://www.importnew.com/23913.html
  7. http://zhangjunhd.blog.51cto.com/113473/53092/
  8. http://ifeve.com/jvm-thread/

相关概念

  • Young(年轻代):
    1. 年轻代分三个区。一个Eden区,两个Survivor区。Eden用来存放JVM刚分配的对象。
    2. 两个Survivor空间一样大,当Eden中的对象经过垃圾回收没有被回收掉时,会在两个Survivor之间来回Copy,当满足某个条件,比如Copy次数,Survivor满,就会被Copy到Tenured。显然,Survivor只是增加了对象在年轻代中的逗留时间,增加了被垃圾回收的可能性。
  • Tenured(年老代)
    1. 年老代存放从年轻代存活的生命期较长的对象。
    2. Full GC后没有回收掉的对象将被Copy到年老代
  • Perm(持久代)
    • 用于存放静态文件,如今Java类、方法等。
    • VM运行时会用到多少持久代的空间取决于应用程序用到了多少类
    • Perm不属于堆内存,有虚拟机直接分配,但可以通过-XX:PermSize -XX:MaxPermSize(默认64M) 等参数调整其大小。
    • 持久代用完后,会抛出OutOfMemoryError "PermGen space"
  • Scavenge GC
    • 当Eden满,触发Scavenge GC,堆Eden区域进行GC,清除非存活对象,并且把尚且存活的对象移动到Survivor区。然后整理Survivor的两个区。
  • Full GC
    • 对整个堆进行整理,包括Young、Tenured和Perm。Full GC比Scavenge GC要慢,因此应该尽可能减少Full GC。有如下原因可能导致Full GC:Tenured被写满,Perm域被写满,System.gc()被显示调用
  • OutOfMemoryException
    • 何时会抛出OutOfMemoryException,并不是内存被耗空的时候才抛出 : 1. JVM98%的时间都花费在内存回收。 2. 每次回收的内存小于2%
  • 串行收集器
    • 使用单线程处理所有垃圾回收工作,因为无需多线程交互,所以效率比较高。但是,也无法使用多处理器的优势,所以此收集器适合单处理器机器。当然,此收集器也可以用在小数据量(100M左右)情况下的多处理器机器上。可以使用-XX:+UseSerialGC打开。
  • 并行收集器
    1. 对年轻代进行并行垃圾回收,因此可以减少垃圾回收时间。一般在多线程多处理器机器上使用。使用-XX:+UseParallelGC.打开。并行收集器在J2SE5.0 update6上引入,在Java SE6.0中进行了增强–可以堆年老代进行并行收集。如果年老代不使用并发收集的话,是使用单线程进行垃圾回收,因此会制约扩展能力。使用-XX:+UseParallelOldGC打开。
    2. 使用-XX:ParallelGCThreads=设置并行垃圾回收的线程数。此值可以设置与机器处理器数量相等。
    3. -XX:GCTimeRatio : 为垃圾回收时间与非垃圾回收时间的比值,通过-XX:GCTimeRatio=来设定,公式为1/(1+N)。例如,-XX:GCTimeRatio=19时,表示5%的时间用于垃圾回收。默认情况为99,即1%的时间用于垃圾回收。
    4. 适用情况:“对吞吐量有高要求”,多CPU、对应用响应时间无要求的中、大型应用。
  • 并发收集器
    1. 可以保证大部分工作都并发进行(应用不停止),垃圾回收只暂停很少的时间,此收集器适合对响应时间要求比较高的中、大规模应用。使用-XX:+UseConcMarkSweepGC打开。
    2. 并发收集器主要减少年老代的暂停时间,他在应用不停止的情况下使用独立的垃圾回收线程,跟踪可达对象。在每个年老代垃圾回收周期中,在收集初期并发收集器会对整个应用进行简短的暂停,在收集中还会再暂停一次。第二次暂停会比第一次稍长,在此过程中多个线程同时进行垃圾回收工作。
    3. 并发收集器使用处理器换来短暂的停顿时间。在一个N个处理器的系统上,并发收集部分使用K/N个可用处理器进行回收,一般情况下1<=K<=N/4。
    4. 在只有一个处理器的主机上使用并发收集器,设置为incremental mode模式也可获得较短的停顿时间。
    5. 浮动垃圾:由于在应用运行的同时进行垃圾回收,所以有些垃圾可能在垃圾回收进行完成时产生,这样就造成了“Floating Garbage”,这些垃圾需要在下次垃圾回收周期时才能回收掉。所以,并发收集器一般需要20%的预留空间用于这些浮动垃圾。
    6. Concurrent Mode Failure:并发收集器在应用运行时进行收集,所以需要保证堆在垃圾回收的这段时间有足够的空间供程序使用,否则,垃圾回收还未完成,堆空间先满了。这种情况下将会发生“并发模式失败”,此时整个应用将会暂停,进行垃圾回收。
    7. 启动并发收集器:因为并发收集在应用运行时进行收集,所以必须保证收集完成之前有足够的内存空间供程序使用,否则会出现“Concurrent Mode Failure”。通过设置-XX:CMSInitiatingOccupancyFraction=指定还有多少剩余堆时开始执行并发收集
    8. 适用情况:“对响应时间有高要求”,多CPU、对应用响应时间有较高要求的中、大型应用。举例:Web服务器/应用服务器、电信交换、集成开发环境

JVM参数

  • 堆设置

    1. -Xms512m/-Xmx1024m : JVM最小/最大可用内存
    2. -Xmn2g : 设置年轻代大小为2G。整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m
    3. -Xss128k:设置每个线程的堆栈大小。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。
    4. -XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/5
    5. -XX:SurvivorRatio=4:设置年轻代中Eden区与Survivor区的大小比值。设置为4,则两个Survivor区与一个Eden区的比值为2:4,一个Survivor区占整个年轻代的1/6,HotSpot VM里,ParallelScavenge系的GC(UseParallelGC / UseParallelOldGC)默认行为是SurvivorRatio如果不显式设置就没啥用。因为ParallelScavenge系的GC最初设计就是默认打开AdaptiveSizePolicy的,它会自动、自适应的调整各种参数,可以关闭自动调整:-XX:-UseAdaptiveSizePolicy
    6. -XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
  • 吞吐量优先的并行收集器(要求在有限的内存下处理尽可能大的数据量,时间换空间)

    1. -XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
    2. -XX:+UseParallelOldGC: 年老代用并行收集器
    3. -XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
    4. -XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。
    5. -XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。
  • 响应时间优先的并发收集器(要求尽可能快的响应,对内存有更大的要求,空间换时间)

    1. -XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。
    2. -XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。
    3. -XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
    4. -XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片
  • 垃圾回收统计信息

    1. -XX:+PrintGC
    2. -XX:+PrintGCDetails
    3. -XX:+PrintGCTimeStamps
    4. -Xloggc:filename

调优总结

  • 吞吐量优先(要求在有限的内存下处理尽可能大的数据量,时间换空间)
    1. 使用并行收集器(多个线程同时垃圾回收,暂停应用程序)
    2. 一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。
    3. 常用设置:
  • 响应时间优先的应用:

Finalizer

  1. 用户线程创建对象的时候,将重写了finalize()方法的对象由VM调用Finalizer的add()加到Finalizer的双向链表里面
  2. GC发生的时候,会调用Reference的enqueue方法,里面调用ReferenceQueue的enquere方法将对象加入到相对应的queue,也就是说FinalReference会加入到Finalizer的ReferenceQueue里面,然后VM会调用ReferenceQueue中锁的notifyAll方法,此时Finalizer线程进入可执行状态
  3. 当Finalizer线程获取到CPU时间片的时候,会调用ReferenceQueue的remove()方法,这个过程需要和ReferenceQueue的enquere争夺线程锁,也就是和GC争夺资源,还会和其他Finalizer线程争夺资源(也就是说ReferenceQueue是线程安全的)。当queue里面为空,Finalizer线程会wait
  4. 然后Finalizer线程的会调用Finalizer对象的runFinalizer()方法,里面调用Finalizer对象的remove()方法,从双向链表中将对象去处,然后调用finalize()方法。这个过程需要和其他用户线程的add()调用争夺线程锁。
  5. 用户线程在创建Finalize对象的时候要相互争夺对象锁,影响多线程效率。
  6. 可以用高优先级用户线程调用Finalizer线程的run,提高垃圾回收效率
  7. 重写了finalize()方法的对象由Finalizer线程调用其finalize()方法,然后释放引用,最后被垃圾回收器回收

堆外内存(off-heap)、堆内内存(on-heap)

  • full gc会对所有分配的堆内内存进行完整的扫描,这意味着这样一次垃圾收集对Java应用造成的影响,跟堆的大小是成正比的。过大的堆会影响Java应用的性能。一种解决方案就是使用堆外内存
  • 堆外内存特点
    1. 适合生命期中等或较长的对象
    2. 在进程间可以共享
    3. 对垃圾回收停顿的改善可以明显感觉到
  • 如果数据结构比较复杂,就要对它进行序列化和反序列化,序列化影响速度
  • java通过DirectByteBuffer的native方法分配堆外内存,通过c来分配,对外内存不受JVM管理,无法垃圾回收
  • 大缓存,命中率低的缓存都可以放在off-heap
  • -XX:MaxDirectMemorySize,设置JVM堆外内存大小

Java 8的元空间

Java 6 Perm(持久代)

  • 用于存放静态文件,如今Java类、方法等。
  • VM运行时会用到多少持久代的空间取决于应用程序用到了多少类
  • Perm不属于堆内存,有虚拟机直接分配,但可以通过-XX:PermSize -XX:MaxPermSize(默认64M) 等参数调整其大小。
  • 持久代用完后,会抛出OutOfMemoryError "PermGen space"
  • Full GC会进行持久代的回收,卸载再需要的类

Java8为什么移除持久代

  • 它的大小是在启动时固定好的——很难进行调优
  • 简化Full GC,可以在GC不进行暂停的情况下并发地释放类数据
  • 使得原来受限于持久代的一些改进未来有可能实现

元空间的特点

  • 元空间使用本地内存来表示类的元数据

  • -XX:MaxMetaspaceSize

    1. 可以设置元空间的最大值,默认是没有上限的,也就是说你的系统内存上限是多少它就是多少
    2. 如果启动后GC过于频繁,请将MaxMetaspaceSize设置得大一些
  • -XX:MetaspaceSize选项指定的是元空间的初始大小,如果没有指定的话,元空间会根据应用程序运行时的需要动态地调整大小

  • -XX:+UseCompressedClassPointers

    1. 词选项用来压缩类指针,类指针_klass在32位JVM中是4字节,在64位JVM中8字节,开启压缩的64位JVM是4字节
    2. java8 64位默认开启
  • -XX:CompressedClassSpaceSize=1G

    1. 只有当-XX:+UseCompressedClassPointers开启了才有效,默认1G
    2. 由于这个大小需要是连续的区域, 在启动的时候就固定了的,因此最好设置得大点。
  • 升级java8 64位可能遇到问题

    • Error occurred during initialization of VM
    • Could not allocate metaspace: 1073741824 bytes
    1. 有可能是因为java8 64位默认开启了类指针压缩功能,而且进程总的内存不足以分配默认的1G本地内存
    2. 解决方案
      • 增加进程总内存
      • 减少JVM内存,从而增大本地内存
      • 关闭类指针压缩功能-XX:-UseCompressedClassPointers
      • 减小类指针压缩空间-XX:CompressedClassSpaceSize=500M

调优工具

  • jmap -histo pid统计对象的数量
  • jmap -histo:live pid 触发Full GC
  • jstat -gcutil 23483 250 7 : 让JVM在控制台输出pid=23483的没有存活必要的引用情况,间隔250毫秒打印一次,一共打印7次。

Linux命令

  • cat /proc/pid/status
    1. VmSize : 整个进程使用虚拟内存大小
    2. VmRSS : 这是驻留在物理内存的一部分。它没有交换到硬盘

遇到的问题

cannot allocate memory for thread-local data: ABORT

  • Native内存空间不够
  • 增大总内存
  • 减小JVM内存

MaxTenuringThreshold of 20 is invalid; must be between 0 and 15

  • java8后进入年老代的年龄限制为0-15
  • -XX:MaxTenuringThreshold=15

调优实战

Linux进程内存查询

总结

  • Xms决定YongUsed + OldUsed大小,YongUsed + OldUsed大小决定used
  • Xms必须设置,否则会很浪费大内存服务器的内存
  • 物理内存包括JVM和非JVM
  • 由于年轻代会自动增长,JVM的最大值不要设置太大,或者-MaxNewSize 的指设小一点,可以节省内存(但是性能会下降,时间换空间)