串行GC与并行GC
一个线程做GC,不要考虑征用和锁的问题以及开销。
适用场景:单机程序,客户端代码,小内存程序(小于100M),CPU数量有限。
并发量大,或者处理大数据的服务端程序,通常用较大内存承载用户的访问。面对较大内存,对YoungGC用串行GC还可以,FullGC用串行GC回收效率远远不够。对于这种情况,线程分配的开销通常是值得的。
而且服务器一般有很多很好的CPU,并行GC可以充分利用CPU。
并行GC的悲观策略
YoungGC后,会检查Old区剩余空间大小,如果Old区剩余空间比平均每次对象“晋升”需要的空间要小的话,就会提前做一次FullGC。
由于GC操作大多是CPU操作,所以线程数不宜多于CPU数。过多线程会使GC过程锁征用开销加大。即使CPU够多,也没必要设置那么多的线程。
基本参数参考
在32位模式下,通常Heap区分配1.5G,使用ParallelOldGC。可以通过ParallelGCThreads改变GC线程数,但是GC过程大多为CPU操作,所以线程数不宜超过CPU个数,否则会使GC锁征用增大。
当有大量存活对象时,哪一种GC效果都不会太好,无论是标记,回收,还是压缩动作都会受到影响。
存活的节点越多,对象之间的空隙越小,移动这些对象的难度会越来越高。
在可控的情况下,可以扩大Survivor区大小,尽量防止对象直接进入Old区。
FullGC发生条件
Old区满:晋升的对象大小大于Old区的剩余空间。
Old剩余空间小于平均晋级空间:Old区剩余空间小于每次平均晋级的大小(每次晋级都会记录大小,这里是每次晋级大小的平均值,Old区剩余空间小于这个值,“悲观”的认为下次晋升肯定会发生FullGC)
Perm区满:Perm区域满的时候,也会发生FullGC。
系统调用:使用System.gc()的时候会发生FullGC。这时在FullGC日志中可以看到system字样。可以使用ExplicitDisable参数使其失效。当关闭显示GC后,在DirectBuffer满的时候也会FullGC。
dump:用jmap工具dump内存的时候,同样会发生一次FullGC然后再开始dump。
CMS GC
对于1~2GB的内存,如果代码没有问题,那么GC的开销时间并不多。采用并行收集的话,每次GC在毫秒的级别。对于传统web应用基本足够,因为用户的感知是秒级别的。
除非遇到大量本不该存活的对象存活着,这种情况哪一种GC都很慢。这种情况下,GC的暂停时间甚至会打到100s甚至以上。
那么如何处理大内存?
可以将大内存机器划分多个虚拟机,或者启动多个JVM。多例方式性能高于虚拟机,但是部署麻烦,资源隔离性差。
Concurrent mark sweep,并发标记、清除。它希望用户在使用过程中,暂停尽量减小(但并非完全没有暂停)。
注意:CMS GC在标记过程中用户是可以操作的,也就是说会引起内存变化,所以CMS GC会remark。
当系统有98%的时间在做GC,并且只有小于2%的内存恢复时,此时会抛出OOM异常。
一旦CMS GC失败或者promotion fail,就会引发FullGC,转入全暂停的FullGC。CMS GC只会与Serial GC兼容,就是说当CMS GC失败时,会转入串行GC(MSC)。CMS GC是配合大内存使用的,一个大内存应用发生串行GC,是我们非常不愿意看到的事情。
增量GC
一次只做一部分GC,防止做GC时间太长。这样清理一部分空间,让程序可以多利用一部分空间,下次再清理一些,程序可以再多利用一些空间。会比一次性清理所有空间,让程序在那里等着要好。
G1 GC
JVM堆被划分为多个板块。当做GC时,理论上只有访问当前板块的线程会被暂停。
垃圾多的区域优先级高,会被优先处理。垃圾多的区域存活的对象少,存活的少那么找到它们就快。对于垃圾少的区域,优先级靠后,GC很少或者永远不会去扫描到这些区域。
注意:再牛的GC算法,也无法解决代码上的问题。写代码时就有大量数据放在内存里,无论多么牛的GC都干不掉,那么最后只能宕机。
小而美的内存
小内存:程序中申请的对象不是那么大。如果一次性分配10MB内存,即使它能马上变成垃圾,可能也会在变为垃圾前先进入Old区(如果Survivor区放不下),甚至直接进入Old区(创建时Eden区放不下)。
美:像昙花一样短暂。干净利落的代码,跑的很快的程序,持有的对象编程垃圾的越快。
将不用的对象设置为null可以帮助gc(但是不是每个地方都要做到这么极致)。有时cache也是有存在必要的,对于那些反复读取的内容,创建它们需要很大代价(比如数据库的连接,由连接池管理就很好,不要每次都创建,销毁)。
大而不美:为了保存用户信息而把它们全都放在session中,又不知道什么时候该去注销掉。
stop the world
安全点:JVM编译为本地代码(汇编指令)后,会生成OopMap,用来帮助GC root的枚举。但是JVM不会为每条指令都生成OopMap,代价太大,只会在特定位置生成OopMap,这个位置叫做“安全点”。指令只要运行到安全点,就可以暂停。
抢先式中断:先强制中断所有线程,发现哪些线程没有运行到安全点,再让它们运行到安全点。该方式难度太大。
主动式中断:先将虚拟机的内存页0x160100设置为不可读的状态,然后每个线程执行到安全点的时候,判断内存页是否可读,如果不可读就停止(类似于java程序的分布式锁,每给线程都会去轮询该锁的状态来做相应的操作)。
对于正在阻塞或者sleep的线程,它们被唤醒的时候GC还在进行中就会有问题。因此有了安全区域,上述代码(不会改变引用变化的)进入安全区域,如果线程要离开这个区域,需要检查系统是否在做GC。