Java GC及JVM参数
参考资料
- http://www.233.com/Java/zhuanye/20101027/164729208-2.html
- http://www.jianshu.com/p/740f00cf03b2
- http://unixboy.iteye.com/blog/174173/
- http://uule.iteye.com/blog/2114697
- http://www.cnblogs.com/redcreen/archive/2011/05/04/2037057.html
- http://www.importnew.com/23913.html
- http://zhangjunhd.blog.51cto.com/113473/53092/
- http://ifeve.com/jvm-thread/
相关概念
- Young(年轻代):
- 年轻代分三个区。一个Eden区,两个Survivor区。Eden用来存放JVM刚分配的对象。
- 两个Survivor空间一样大,当Eden中的对象经过垃圾回收没有被回收掉时,会在两个Survivor之间来回Copy,当满足某个条件,比如Copy次数,Survivor满,就会被Copy到Tenured。显然,Survivor只是增加了对象在年轻代中的逗留时间,增加了被垃圾回收的可能性。
- Tenured(年老代)
- 年老代存放从年轻代存活的生命期较长的对象。
- 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打开。
- 并行收集器
- 对年轻代进行并行垃圾回收,因此可以减少垃圾回收时间。一般在多线程多处理器机器上使用。使用-XX:+UseParallelGC.打开。并行收集器在J2SE5.0 update6上引入,在Java SE6.0中进行了增强–可以堆年老代进行并行收集。如果年老代不使用并发收集的话,是使用单线程进行垃圾回收,因此会制约扩展能力。使用-XX:+UseParallelOldGC打开。
- 使用-XX:ParallelGCThreads=
设置并行垃圾回收的线程数。此值可以设置与机器处理器数量相等。 - -XX:GCTimeRatio : 为垃圾回收时间与非垃圾回收时间的比值,通过-XX:GCTimeRatio=
来设定,公式为1/(1+N)。例如,-XX:GCTimeRatio=19时,表示5%的时间用于垃圾回收。默认情况为99,即1%的时间用于垃圾回收。 - 适用情况:“对吞吐量有高要求”,多CPU、对应用响应时间无要求的中、大型应用。
- 并发收集器
- 可以保证大部分工作都并发进行(应用不停止),垃圾回收只暂停很少的时间,此收集器适合对响应时间要求比较高的中、大规模应用。使用-XX:+UseConcMarkSweepGC打开。
- 并发收集器主要减少年老代的暂停时间,他在应用不停止的情况下使用独立的垃圾回收线程,跟踪可达对象。在每个年老代垃圾回收周期中,在收集初期并发收集器会对整个应用进行简短的暂停,在收集中还会再暂停一次。第二次暂停会比第一次稍长,在此过程中多个线程同时进行垃圾回收工作。
- 并发收集器使用处理器换来短暂的停顿时间。在一个N个处理器的系统上,并发收集部分使用K/N个可用处理器进行回收,一般情况下1<=K<=N/4。
- 在只有一个处理器的主机上使用并发收集器,设置为incremental mode模式也可获得较短的停顿时间。
- 浮动垃圾:由于在应用运行的同时进行垃圾回收,所以有些垃圾可能在垃圾回收进行完成时产生,这样就造成了“Floating Garbage”,这些垃圾需要在下次垃圾回收周期时才能回收掉。所以,并发收集器一般需要20%的预留空间用于这些浮动垃圾。
- Concurrent Mode Failure:并发收集器在应用运行时进行收集,所以需要保证堆在垃圾回收的这段时间有足够的空间供程序使用,否则,垃圾回收还未完成,堆空间先满了。这种情况下将会发生“并发模式失败”,此时整个应用将会暂停,进行垃圾回收。
- 启动并发收集器:因为并发收集在应用运行时进行收集,所以必须保证收集完成之前有足够的内存空间供程序使用,否则会出现“Concurrent Mode Failure”。通过设置-XX:CMSInitiatingOccupancyFraction=
指定还有多少剩余堆时开始执行并发收集 - 适用情况:“对响应时间有高要求”,多CPU、对应用响应时间有较高要求的中、大型应用。举例:Web服务器/应用服务器、电信交换、集成开发环境
JVM参数
-
堆设置
- -Xms512m/-Xmx1024m : JVM最小/最大可用内存
- -Xmn2g : 设置年轻代大小为2G。整个堆大小=年轻代大小 + 年老代大小 + 持久代大小。持久代一般固定大小为64m
- -Xss128k:设置每个线程的堆栈大小。更具应用的线程所需内存大小进行调整。在相同物理内存下,减小这个值能生成更多的线程。
- -XX:NewRatio=4:设置年轻代(包括Eden和两个Survivor区)与年老代的比值(除去持久代)。设置为4,则年轻代与年老代所占比值为1:4,年轻代占整个堆栈的1/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
- -XX:MaxTenuringThreshold=0:设置垃圾最大年龄。如果设置为0的话,则年轻代对象不经过Survivor区,直接进入年老代。对于年老代比较多的应用,可以提高效率。如果将此值设置为一个较大值,则年轻代对象会在Survivor区进行多次复制,这样可以增加对象再年轻代的存活时间,增加在年轻代即被回收的概论。
-
吞吐量优先的并行收集器(要求在有限的内存下处理尽可能大的数据量,时间换空间)
- -XX:+UseParallelGC:选择垃圾收集器为并行收集器。此配置仅对年轻代有效。即上述配置下,年轻代使用并发收集,而年老代仍旧使用串行收集。
- -XX:+UseParallelOldGC: 年老代用并行收集器
- -XX:ParallelGCThreads=20:配置并行收集器的线程数,即:同时多少个线程一起进行垃圾回收。此值最好配置与处理器数目相等。
- -XX:+UseParallelOldGC:配置年老代垃圾收集方式为并行收集。JDK6.0支持对年老代并行收集。
- -XX:MaxGCPauseMillis=100:设置每次年轻代垃圾回收的最长时间,如果无法满足此时间,JVM会自动调整年轻代大小,以满足此值。
-
响应时间优先的并发收集器(要求尽可能快的响应,对内存有更大的要求,空间换时间)
- -XX:+UseAdaptiveSizePolicy:设置此选项后,并行收集器会自动选择年轻代区大小和相应的Survivor区比例,以达到目标系统规定的最低相应时间或者收集频率等,此值建议使用并行收集器时,一直打开。
- -XX:+UseConcMarkSweepGC:设置年老代为并发收集。测试中配置这个以后,-XX:NewRatio=4的配置失效了,原因不明。所以,此时年轻代大小最好用-Xmn设置。
- -XX:CMSFullGCsBeforeCompaction:由于并发收集器不对内存空间进行压缩、整理,所以运行一段时间以后会产生“碎片”,使得运行效率降低。此值设置运行多少次GC以后对内存空间进行压缩、整理。
- -XX:+UseCMSCompactAtFullCollection:打开对年老代的压缩。可能会影响性能,但是可以消除碎片
-
垃圾回收统计信息
- -XX:+PrintGC
- -XX:+PrintGCDetails
- -XX:+PrintGCTimeStamps
- -Xloggc:filename
调优总结
- 吞吐量优先(要求在有限的内存下处理尽可能大的数据量,时间换空间)
- 使用并行收集器(多个线程同时垃圾回收,暂停应用程序)
- 一般吞吐量优先的应用都有一个很大的年轻代和一个较小的年老代。原因是,这样可以尽可能回收掉大部分短期对象,减少中期的对象,而年老代尽存放长期存活对象。
- 常用设置:
- 响应时间优先的应用:
Finalizer
- 用户线程创建对象的时候,将重写了finalize()方法的对象由VM调用Finalizer的add()加到Finalizer的双向链表里面
- GC发生的时候,会调用Reference的enqueue方法,里面调用ReferenceQueue的enquere方法将对象加入到相对应的queue,也就是说FinalReference会加入到Finalizer的ReferenceQueue里面,然后VM会调用ReferenceQueue中锁的notifyAll方法,此时Finalizer线程进入可执行状态
- 当Finalizer线程获取到CPU时间片的时候,会调用ReferenceQueue的remove()方法,这个过程需要和ReferenceQueue的enquere争夺线程锁,也就是和GC争夺资源,还会和其他Finalizer线程争夺资源(也就是说ReferenceQueue是线程安全的)。当queue里面为空,Finalizer线程会wait
- 然后Finalizer线程的会调用Finalizer对象的runFinalizer()方法,里面调用Finalizer对象的remove()方法,从双向链表中将对象去处,然后调用finalize()方法。这个过程需要和其他用户线程的add()调用争夺线程锁。
- 用户线程在创建Finalize对象的时候要相互争夺对象锁,影响多线程效率。
- 可以用高优先级用户线程调用Finalizer线程的run,提高垃圾回收效率
- 重写了finalize()方法的对象由Finalizer线程调用其finalize()方法,然后释放引用,最后被垃圾回收器回收
堆外内存(off-heap)、堆内内存(on-heap)
- full gc会对所有分配的堆内内存进行完整的扫描,这意味着这样一次垃圾收集对Java应用造成的影响,跟堆的大小是成正比的。过大的堆会影响Java应用的性能。一种解决方案就是使用堆外内存
- 堆外内存特点
- 适合生命期中等或较长的对象
- 在进程间可以共享
- 对垃圾回收停顿的改善可以明显感觉到
- 如果数据结构比较复杂,就要对它进行序列化和反序列化,序列化影响速度
- 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
- 可以设置元空间的最大值,默认是没有上限的,也就是说你的系统内存上限是多少它就是多少
- 如果启动后GC过于频繁,请将MaxMetaspaceSize设置得大一些
-
-XX:MetaspaceSize选项指定的是元空间的初始大小,如果没有指定的话,元空间会根据应用程序运行时的需要动态地调整大小
-
-XX:+UseCompressedClassPointers
- 词选项用来压缩类指针,类指针_klass在32位JVM中是4字节,在64位JVM中8字节,开启压缩的64位JVM是4字节
- java8 64位默认开启
-
-XX:CompressedClassSpaceSize=1G
- 只有当-XX:+UseCompressedClassPointers开启了才有效,默认1G
- 由于这个大小需要是连续的区域, 在启动的时候就固定了的,因此最好设置得大点。
-
升级java8 64位可能遇到问题
- Error occurred during initialization of VM
- Could not allocate metaspace: 1073741824 bytes
- 有可能是因为java8 64位默认开启了类指针压缩功能,而且进程总的内存不足以分配默认的1G本地内存
- 解决方案
- 增加进程总内存
- 减少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
- VmSize : 整个进程使用虚拟内存大小
- 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进程内存查询
- 物理内存 (kb):
- ps -e -o 'pid,args,rsz,rss,uid,user,cmd' | sort -nrk3 | grep rates-redis | grep -v grep
- cat /proc/11199/status
- top
- 物理内存稍微大于used
- JVM堆内存
- JVM默认Xms是1/64服务器内存,Xmx是1/4服务器内存
- 默认情况下NewSize:OldSize=1:2
- /opt/citimkts/java/1.8.0_45l64/bin/jmap -heap 11199
- Xms
- NewSize + OldSize
- Xmx
- MaxNewSize + MaxOldSize
- http://cvatap3d.nam.nsroot.net:33359/actuator/metrics/jvm.memory.max?tag=area:heap
- used
- http://cvatap3d.nam.nsroot.net:33359/actuator/metrics/jvm.memory.committed?tag=area:heap
总结
- Xms决定YongUsed + OldUsed大小,YongUsed + OldUsed大小决定used
- Xms必须设置,否则会很浪费大内存服务器的内存
- 物理内存包括JVM和非JVM
- 由于年轻代会自动增长,JVM的最大值不要设置太大,或者-MaxNewSize 的指设小一点,可以节省内存(但是性能会下降,时间换空间)